Skip to content

Typed tree for a .fsx omits bare top-level expressions #332

Closed
fsprojects/fantomas
#3468

Description

@nojaf

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-001

The 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.

https://github.com/ionide/FSharp.Analyzers.SDK/blob/v0.39.0/src/FSharp.Analyzers.SDK/TASTCollecting.fs#L493

| FSharpImplementationFileDeclaration.InitAction e -> visitExpr f e

I 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

  1. nojaf commented on Sep 11, 2026

    @nojaf
    ContributorAuthor

    Two corrections to the issue above, both from re-running the repro with the published G-Research.FSharp.Analyzers 0.25.0, fsharp-analyzers 0.39.0 and .NET SDK 10.0.401.

    1. The do row in my table is wrong. An explicit do is not reported in a .fsx either, in any position in the file. My original found.fsx claim does not hold up.

    2. It is not only InitAction. A call inside an ordinary top-level let binding can be missing too, so the surface is wider than "bare top-level expressions".

    One file, analyzed both ways (the .fsx is the same text without the module line):

    module M
    let x = "foo".EndsWith("p")
    do printfn "%b" ("foo".EndsWith("p"))
    printfn "%b" ("bar".EndsWith("p"))
    let f (y : int) = string y

    GRA-STRING-001 is a ctx.TypedTree analyzer for String.EndsWith with a single argument, GRA-TYPE-ANNOTATE-001 a ctx.TypedTree analyzer for the one-argument string function.

    construct .fs via --project .fsx via --script
    let x = "foo".EndsWith("p") reported reported
    do printfn "%b" ("foo".EndsWith("p")) reported not reported
    printfn "%b" ("bar".EndsWith("p")) reported not reported
    let f (y : int) = string y reported not reported

    Row 1 fires for a script, including for let bindings in the middle of a longer file, so the script typed tree does contain top-level let declarations. But no form of string is ever reported under --script: string (x : int), string (x : System.DateTime), string (x : obj) and a generic let f x = string x are all silent as a script and all reported as part of a project.

    Some things it is not:

    • Not the analyzer's parse tree lookup. TypeAnnotateStringFunctionAnalyzer prints under #if DEBUG both when it maps a call range to an unexpected SynExpr and when it finds none at all. With a Debug build of the analyzers, neither print appears for the script, so WalkCall never receives the Microsoft.FSharp.Core.Operators.string call in the first place.
    • Not optimization settings. Adding <Optimize>true</Optimize> to the project still reports all four.

    One thread worth pulling: string is inline and String.EndsWith is not. If the script checking path ends up expanding inline applications before the FSharpImplementationFileContents are produced, the call node analyzers match on would be gone, which fits both the string result and the fact that a plain method call survives.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions