Skip to content

Warn for missing shebang#53614

Merged
jjonescz merged 16 commits intodotnet:release/10.0.3xxfrom
jjonescz:sprint-shebang-warn-2
Apr 9, 2026
Merged

Warn for missing shebang#53614
jjonescz merged 16 commits intodotnet:release/10.0.3xxfrom
jjonescz:sprint-shebang-warn-2

Conversation

@jjonescz
Copy link
Copy Markdown
Member

@jjonescz jjonescz added the Area-run-file Items related to the "dotnet run <file>" effort label Mar 27, 2026
@jjonescz jjonescz marked this pull request as ready for review March 28, 2026 13:30
@jjonescz jjonescz requested a review from a team as a code owner March 28, 2026 13:30
Copilot AI review requested due to automatic review settings March 28, 2026 13:30
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new analyzer warning (CA2266) and code fix to help IDEs reliably identify the entry point in multi-file file-based programs by requiring a #! shebang on the entry-point file, and wires up the SDK to provide the entry point file path to analyzers.

Changes:

  • Add CA2266 analyzer + code fix for missing shebang in multi-file file-based programs (based on EntryPointFilePath).
  • Plumb EntryPointFilePath through the file-based virtual project and generated editorconfig so analyzers can identify the entry point.
  • Add SDK/integration tests, analyzer unit tests, docs, and resource/localization updates for the new rule.

Reviewed changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
test/dotnet.Tests/CommandTests/Run/RunFileTests.cs Adds an end-to-end dotnet run Program.cs test validating CA2266 behavior (only for #:include entry points missing a shebang) and updates expected virtual project content.
src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs Adds <EntryPointFilePath> to the generated virtual project so the entry point path can be surfaced to analyzers.
src/Microsoft.CodeAnalysis.NetAnalyzers/tests/Microsoft.CodeAnalysis.NetAnalyzers.UnitTests/Microsoft.NetCore.Analyzers/Usage/MissingShebangInFileBasedProgramTests.cs Adds analyzer/codefix unit tests for CA2266.
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler/Options/MSBuildPropertyOptionNames.cs Adds EntryPointFilePath to the known MSBuild property option names.
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt Extends the Usage range to include CA2266.
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf Adds new localized resource entries for CA2266 strings (pending translation).
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf Adds new localized resource entries for CA2266 strings (pending translation).
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf Adds new localized resource entries for CA2266 strings (pending translation).
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf Adds new localized resource entries for CA2266 strings (pending translation).
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf Adds new localized resource entries for CA2266 strings (pending translation).
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf Adds new localized resource entries for CA2266 strings (pending translation).
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf Adds new localized resource entries for CA2266 strings (pending translation).
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf Adds new localized resource entries for CA2266 strings (pending translation).
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf Adds new localized resource entries for CA2266 strings (pending translation).
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf Adds new localized resource entries for CA2266 strings (pending translation).
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf Adds new localized resource entries for CA2266 strings (pending translation).
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf Adds new localized resource entries for CA2266 strings (pending translation).
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf Adds new localized resource entries for CA2266 strings (pending translation).
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Usage/MissingShebangInFileBasedProgram.cs Introduces the shared diagnostic descriptor for CA2266.
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Usage/MissingShebangInFileBasedProgram.Fixer.cs Adds the shared abstract code fix provider base for CA2266.
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx Adds CA2266 title/message/description and code fix title strings.
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/AnalyzerReleases.Unshipped.md Registers CA2266 as a new unshipped rule.
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.sarif Adds CA2266 metadata to the SARIF rule set.
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.md Adds documentation entry for CA2266.
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Usage/CSharpMissingShebangInFileBasedProgram.cs Implements the C# analyzer logic for CA2266 (checks entry point tree for shebang; reports only when multiple non-generated trees exist).
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Usage/CSharpMissingShebangInFileBasedProgram.Fixer.cs Implements the C# code fix to prepend #!/usr/bin/env dotnet with appropriate EOL.
src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.Generated.cs Exposes EntryPointFilePath to analyzers via generated editorconfig build_property.
documentation/general/dotnet-run-file.md Documents the new EntryPointFilePath exposure and the CA2266 behavior for multi-file file-based programs.

@jjonescz jjonescz requested a review from a team March 28, 2026 13:36
Interlocked.CompareExchange(ref pendingDiagnostic, location.CreateDiagnostic(Rule), null);
});

context.RegisterCompilationEndAction(context =>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CompilationEnd we don't run in the IDE as live diagnostics; I'm not sure if that's problematic for your scenarios or not. This might be rewritable to some code that would just look at the initial compilation and find the tree that matches the entry point file path, and then you could move the reporting of the diagnostic to when nonGeneratedTreeCount == 2. Somebody else might have a better idea here too...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I haven't realized CompilationEnd doesn't run in IDE. Changed.

string? directoryPath = AppContext.GetData("EntryPointFileDirectoryPath") as string;
```

- `EntryPointFilePath` property is set to the entry-point file path and is made visible to analyzers via `CompilerVisibleProperty`.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a part of me that really feels like this should be something more like a CompilationOption rather than just being passed along as an MSBuild property but I'm not really sure what we'd actually gain with that.

Copy link
Copy Markdown
Member Author

@jjonescz jjonescz Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, we have the feature flag FileBasedProgram (which causes roslyn to allow #: and #! directives). We could pass the entry point file path as its value I guess. Feature flags are part of parse options. But I also don't know what we'd gain with that.

Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

csc has the -main switch today, but, it takes a type name, not a file path..

-main:<type>                  Specify the type that contains the entry point
                              (ignore all other possible entry points) (Short
                              form: -m)

I think that using a compiler-visible msbuild property is the right notch on the dial for the time being.

}
}

return Environment.NewLine;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have a better answer for this, since there's still .editorconfig settings and the editor to use here as a hint. Looks like for the file header fix we do this:
https://github.com/dotnet/roslyn/blob/106890564f33f6642b625e99bed73b3e9a15244e/src/Analyzers/Core/CodeFixes/FileHeaders/AbstractFileHeaderCodeFixProvider.cs#L48-L50

@JoeRobich or @akhera99 any ideas here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to say you can annotate the node and it will be formatted when applied.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to say you can annotate the node and it will be formatted when applied.

It doesn't look like that's happening for newlines inside a trivia like this though.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like for the file header fix we do this:

Updated to do a similar thing here.

var eol = GetEndOfLine(root.SyntaxTree.GetText());
var shebangTrivia = SyntaxFactory.ParseLeadingTrivia("#!/usr/bin/env dotnet" + eol);
var firstToken = root.GetFirstToken(includeZeroWidth: true);
var newFirstToken = firstToken.WithLeadingTrivia(shebangTrivia.AddRange(firstToken.LeadingTrivia));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where should this go relative to copyright headers?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#! always needs to be first otherwise it wouldn't be recognized by shells. I remember we already discussed this and the copyright header analyzers should be already handling #! correctly.

{
Interlocked.Increment(ref nonGeneratedTreeCount);

if (!context.Tree.FilePath.Equals(entryPointFilePath, StringComparison.OrdinalIgnoreCase))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "IgnoreCase" here may be problematic on Linux/Mac, since file names are case sensitive there.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the file names should be consistent since they come from the same tooling so we can just use Ordinal everywhere.

/// the <c>generated_code</c> analyzer config option,
/// common file name patterns, and <c>&lt;auto-generated&gt;</c> comment headers.
/// </summary>
private static bool IsGeneratedCode(SyntaxTree tree, AnalyzerConfigOptionsProvider optionsProvider)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like hardcoding this knowledge here, but don't know how to better do this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first I thought: just indicate that the analyzer shouldn't run on generated code, and see if you get called back for a given syntax tree or not. But I think that doesn't work, since it would need the "compilation end" callback to run.

I think copying the implementation like you did is the best we can do for now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend adding a permalink here to the location in Roslyn this was based on though.

}

// Only report when there are multiple non-generated files
// (i.e., #:include directives are used).
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this has the effect of also detecting when a Directory.Build.props adds a compile item? Is that right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if it is a goal to handle that case, then, it should have a test

}

var root = context.Tree.GetRoot(context.CancellationToken);
if (root.GetLeadingTrivia().Any(SyntaxKind.ShebangDirectiveTrivia))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we counting on the fact that if the #! appears somewhere besides position 0, then the compiler reports an error?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'll add a test to demonstrate.


## [CA2266](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2266): File-based program entry point should start with '#!'

When a file-based program consists of multiple files, the entry point file should start with a shebang ('#!') line to clearly distinguish it from other included files.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It occurs to me that we may also want this when #:project or #:ref are used, so that find all references on symbols in the referenced projects works properly.

However, I don't wish to block on that.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, but I think that can be implemented as a follow up (filed #53749), since this PR should unblock us removing the experimental opt-in from #:include, so it'd be good to merge just that part first.

Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall LGTM just had some minor comments.

ExpectedDiagnostics =
{
new DiagnosticResult(MissingShebangInFileBasedProgram.Rule).WithLocation("Test0.cs", 1, 1),
// Preprocessor directives must appear as the first non-whitespace character on a line
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a tracking bug somewhere for this nonsensical error? #! is the first non-whitespace character on its line, that's not the issue.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed dotnet/roslyn#83111, thanks.

@jjonescz
Copy link
Copy Markdown
Member Author

jjonescz commented Apr 9, 2026

/ba-g unrelated test failures, same as in #53759

@jjonescz jjonescz merged commit 815727a into dotnet:release/10.0.3xx Apr 9, 2026
25 of 28 checks passed
@jjonescz jjonescz deleted the sprint-shebang-warn-2 branch April 9, 2026 09:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-run-file Items related to the "dotnet run <file>" effort

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants