Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions documentation/general/dotnet-run-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ Additionally, the implicit project file has the following customizations:
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.


- `FileBasedProgram` property is set to `true` and can be used by SDK targets to detect file-based apps.

- `DisableDefaultItemsInProjectFolder` property is set to `true` which results in `EnableDefaultItems=false` by default
Expand Down Expand Up @@ -270,6 +272,11 @@ Along with `#:`, the language also ignores `#!` which could be then used for [sh
Console.WriteLine("Hello");
```

When a file-based program uses [`#:include`](#multiple-files) directives to include additional files,
the entry point file should start with `#!` to clearly distinguish it from included files.
This helps IDEs to properly handle multi-file scenarios and discover entry points.
The analyzer **CA2266** reports a warning if the entry point file is missing the shebang line in this scenario.

## Implementation

The build is performed using MSBuild APIs on in-memory project files.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ private string GetGeneratedMSBuildEditorConfigContent()
build_property.InvariantGlobalization =
build_property.PlatformNeutralAssembly =
build_property.EnforceExtendedAnalyzerRules =
build_property.EntryPointFilePath = {EntryPointFileFullPath}
build_property._SupportedPlatformList = Linux,macOS,Windows
build_property.RootNamespace = {FileNameWithoutExtension}
build_property.ProjectDir = {BaseDirectoryWithTrailingSeparator}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.NetCore.Analyzers;
using Microsoft.NetCore.Analyzers.Usage;

namespace Microsoft.NetCore.CSharp.Analyzers.Usage
{
[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
public sealed class CSharpMissingShebangInFileBasedProgramFixer : MissingShebangInFileBasedProgramFixer
{
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
if (root is null)
{
return;
}

var codeAction = CodeAction.Create(
MicrosoftNetCoreAnalyzersResources.MissingShebangInFileBasedProgramCodeFixTitle,
async ct =>
{
var options = await context.Document.GetOptionsAsync(ct).ConfigureAwait(false);
var eol = options.GetOption(FormattingOptions.NewLine, LanguageNames.CSharp);
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.

var newRoot = root.ReplaceToken(firstToken, newFirstToken)
.WithAdditionalAnnotations(Formatter.Annotation);
return context.Document.WithSyntaxRoot(newRoot);
},
MicrosoftNetCoreAnalyzersResources.MissingShebangInFileBasedProgramCodeFixTitle);
context.RegisterCodeFix(codeAction, context.Diagnostics);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.NetCore.Analyzers.Usage;

namespace Microsoft.NetCore.CSharp.Analyzers.Usage
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class CSharpMissingShebangInFileBasedProgram : MissingShebangInFileBasedProgram
{
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

context.RegisterCompilationStartAction(context =>
{
var entryPointFilePath = context.Options.GetMSBuildPropertyValue(
MSBuildPropertyOptionNames.EntryPointFilePath, context.Compilation);
if (string.IsNullOrEmpty(entryPointFilePath))
{
return;
}

// Count non-generated trees upfront so we can report directly
// from a SyntaxTreeAction without needing CompilationEnd.
// We avoid CompilationEnd so diagnostics appear as live IDE diagnostics.
// We replicate Roslyn's generated code detection here because
// Compilation.SyntaxTrees is the raw set (unlike RegisterSyntaxTreeAction
// which gets automatic filtering via ConfigureGeneratedCodeAnalysis).
int nonGeneratedTreeCount = 0;
foreach (var tree in context.Compilation.SyntaxTrees)
{
if (IsGeneratedCode(tree, context.Options.AnalyzerConfigOptionsProvider))
{
continue;
}

nonGeneratedTreeCount++;
}

// Only report when there are multiple non-generated files
// (i.e., #:include directives are used).
// Single-file programs don't need a shebang to distinguish the entry point.
if (nonGeneratedTreeCount <= 1)
{
return;
}

context.RegisterSyntaxTreeAction(context =>
{
if (!context.Tree.FilePath.Equals(entryPointFilePath, StringComparison.Ordinal))
{
return;
}

var root = context.Tree.GetRoot(context.CancellationToken);
if (root.GetLeadingTrivia().Any(SyntaxKind.ShebangDirectiveTrivia))
{
return;
}

var location = root.GetFirstToken(includeZeroWidth: true).GetLocation();
context.ReportDiagnostic(location.CreateDiagnostic(Rule));
});
});
}

/// <summary>
/// Replicates Roslyn's generated code detection which checks:
/// 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.

{
if (optionsProvider.GetOptions(tree)
.TryGetValue("generated_code", out var generatedValue) &&
generatedValue.Equals("true", StringComparison.OrdinalIgnoreCase))
{
return true;
}

var filePath = tree.FilePath;
if (!string.IsNullOrEmpty(filePath))
{
var fileName = Path.GetFileName(filePath);
if (fileName.StartsWith("TemporaryGeneratedFile_", StringComparison.OrdinalIgnoreCase))
{
return true;
}

var nameWithoutExtension = Path.GetFileNameWithoutExtension(fileName);
if (nameWithoutExtension.EndsWith(".designer", StringComparison.OrdinalIgnoreCase) ||
nameWithoutExtension.EndsWith(".generated", StringComparison.OrdinalIgnoreCase) ||
nameWithoutExtension.EndsWith(".g", StringComparison.OrdinalIgnoreCase) ||
nameWithoutExtension.EndsWith(".g.i", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}

// Check for <auto-generated> or <autogenerated> comment at the top of the file.
foreach (var trivia in tree.GetRoot().GetLeadingTrivia())
{
switch (trivia.Kind())
{
case SyntaxKind.SingleLineCommentTrivia:
case SyntaxKind.MultiLineCommentTrivia:
var text = trivia.ToString();
if (text.Contains("<auto-generated") || text.Contains("<autogenerated"))
{
return true;
}
break;
case SyntaxKind.WhitespaceTrivia:
case SyntaxKind.EndOfLineTrivia:
continue;
default:
return false;
}
}

return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2742,6 +2742,18 @@ Comparing a span to 'null' or 'default' might not do what you intended. 'default
|CodeFix|True|
---

## [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.

|Item|Value|
|-|-|
|Category|Usage|
|Enabled|True|
|Severity|Warning|
|CodeFix|True|
---

## [CA2300](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2300): Do not use insecure deserializer BinaryFormatter

The method '{0}' is insecure when deserializing untrusted data. If you need to instead detect BinaryFormatter deserialization without a SerializationBinder set, then disable rule CA2300, and enable rules CA2301 and CA2302.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,25 @@
]
}
},
"CA2266": {
"id": "CA2266",
"shortDescription": "File-based program entry point should start with '#!'",
"fullDescription": "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.",
"defaultLevel": "warning",
"helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2266",
"properties": {
"category": "Usage",
"isEnabledByDefault": true,
"typeName": "CSharpMissingShebangInFileBasedProgram",
"languages": [
"C#"
],
"tags": [
"Telemetry",
"EnabledRuleInAggressiveMode"
]
}
},
"CA2352": {
"id": "CA2352",
"shortDescription": "Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks",
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
; Please do not edit this file manually, it should only be updated through code fix application.

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
CA2266 | Usage | Warning | MissingShebangInFileBasedProgram
Original file line number Diff line number Diff line change
Expand Up @@ -2216,4 +2216,16 @@ Widening and user defined conversions are not supported with generic types.</val
<data name="DoNotUseThreadVolatileReadWriteCodeFixTitle" xml:space="preserve">
<value>Replace obsolete call</value>
</data>
<data name="MissingShebangInFileBasedProgramTitle" xml:space="preserve">
<value>File-based program entry point should start with '#!'</value>
</data>
<data name="MissingShebangInFileBasedProgramDescription" xml:space="preserve">
<value>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.</value>
</data>
<data name="MissingShebangInFileBasedProgramMessage" xml:space="preserve">
<value>File-based program entry point should start with '#!'</value>
</data>
<data name="MissingShebangInFileBasedProgramCodeFixTitle" xml:space="preserve">
<value>Add '#!' (shebang)</value>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System.Collections.Immutable;
using Microsoft.CodeAnalysis.CodeFixes;

namespace Microsoft.NetCore.Analyzers.Usage
{
public abstract class MissingShebangInFileBasedProgramFixer : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(MissingShebangInFileBasedProgram.RuleId);

public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System.Collections.Immutable;
using Analyzer.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Microsoft.NetCore.Analyzers.Usage
{
using static MicrosoftNetCoreAnalyzersResources;

public abstract class MissingShebangInFileBasedProgram : DiagnosticAnalyzer
{
internal const string RuleId = "CA2266";

internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
RuleId,
CreateLocalizableResourceString(nameof(MissingShebangInFileBasedProgramTitle)),
CreateLocalizableResourceString(nameof(MissingShebangInFileBasedProgramMessage)),
DiagnosticCategory.Usage,
RuleLevel.BuildWarning,
CreateLocalizableResourceString(nameof(MissingShebangInFileBasedProgramDescription)),
isPortedFxCopRule: false,
isDataflowRule: false,
isReportedAtCompilationEnd: false);

public sealed override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading