Repository navigation
Typed tree for a .fsx omits bare top-level expressions #332
Description
Summary
For a .fsx passed with --script, the typed tree an analyzer receives as ctx.TypedTree does not contain bare top-level expressions. Writing the same expression with an explicit do makes it appear, and in a .fs file inside a project both forms are present.
Since most of a script is bare top-level expressions, every TypedTree-based analyzer silently reports nothing for the bulk of a script. It is not a crash and not a warning: the analyzer just returns no messages, so a --script run looks clean.
Repro
Using fsharp-analyzers 0.39.0 and G-Research.FSharp.Analyzers 0.25.0, whose GRA-STRING-001 is a ctx.TypedTree analyzer (String.EndsWith with a single argument).
// missed.fsx
printfn "%b" ("foo".EndsWith("p"))// found.fsx
do printfn "%b" ("foo".EndsWith("p"))ANALYZERS=~/.nuget/packages/g-research.fsharp.analyzers/0.25.0/analyzers/dotnet/fs
dotnet fsharp-analyzers --script missed.fsx --analyzers-path $ANALYZERS --treat-as-error "GRA-*" # exit 0, no messages
dotnet fsharp-analyzers --script found.fsx --analyzers-path $ANALYZERS --treat-as-error "GRA-*" # GRA-STRING-001The same two lines in a .fs file analyzed with --project are both reported, so the difference is the script path rather than the do keyword itself.
.fs via --project |
.fsx via --script |
|
|---|---|---|
let x = "foo".EndsWith("p") |
reported | reported |
do printfn "%b" ("foo".EndsWith("p")) |
reported | reported |
printfn "%b" ("foo".EndsWith("p")) |
reported | not reported |
Where it seems to go wrong
Not in TASTCollecting: visitDeclaration handles the case already.
| FSharpImplementationFileDeclaration.InitAction e -> visitExpr f eI instrumented WalkCall in an analyzer to print every call it visits. For missed.fsx the only call that reaches it is Microsoft.FSharp.Core.Operators.raise, and System.String.EndsWith never appears. So the declaration is missing from the FSharpImplementationFileContents handed to the analyzer, rather than being skipped while walking it. That points at the script path in the CLI, loadScripts / GetProjectOptionsFromScript in ProjectLoading.fs and how the results are turned into a context.
Why it matters
Analyzing scripts is otherwise a nice win, and the untyped-tree analyzers do work on them. But a repository that adds --script to its CI gets a green check that only covers part of what it covers for projects, with nothing in the output to say so.
Activity
Annotate the `string` calls the scripts make
nojaf commentedon Sep 11, 2026
Two corrections to the issue above, both from re-running the repro with the published
G-Research.FSharp.Analyzers0.25.0,fsharp-analyzers0.39.0 and .NET SDK 10.0.401.1. The
dorow in my table is wrong. An explicitdois not reported in a.fsxeither, in any position in the file. My originalfound.fsxclaim does not hold up.2. It is not only
InitAction. A call inside an ordinary top-levelletbinding can be missing too, so the surface is wider than "bare top-level expressions".One file, analyzed both ways (the
.fsxis the same text without themoduleline):GRA-STRING-001is actx.TypedTreeanalyzer forString.EndsWithwith a single argument,GRA-TYPE-ANNOTATE-001actx.TypedTreeanalyzer for the one-argumentstringfunction..fsvia--project.fsxvia--scriptlet x = "foo".EndsWith("p")do printfn "%b" ("foo".EndsWith("p"))printfn "%b" ("bar".EndsWith("p"))let f (y : int) = string yRow 1 fires for a script, including for
letbindings in the middle of a longer file, so the script typed tree does contain top-levelletdeclarations. But no form ofstringis ever reported under--script:string (x : int),string (x : System.DateTime),string (x : obj)and a genericlet f x = string xare all silent as a script and all reported as part of a project.Some things it is not:
TypeAnnotateStringFunctionAnalyzerprints under#if DEBUGboth when it maps a call range to an unexpectedSynExprand when it finds none at all. With a Debug build of the analyzers, neither print appears for the script, soWalkCallnever receives theMicrosoft.FSharp.Core.Operators.stringcall in the first place.<Optimize>true</Optimize>to the project still reports all four.One thread worth pulling:
stringisinlineandString.EndsWithis not. If the script checking path ends up expanding inline applications before theFSharpImplementationFileContentsare produced, the call node analyzers match on would be gone, which fits both thestringresult and the fact that a plain method call survives.