Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,31 +26,6 @@ public override void Initialize(AnalysisContext context)
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))
Expand All @@ -64,68 +39,37 @@ public override void Initialize(AnalysisContext context)
return;
}

var location = root.GetFirstToken(includeZeroWidth: true).GetLocation();
var includeDirective = root.GetLeadingTrivia().FirstOrDefault(IsIncludeDirective);
if (includeDirective == default)
{
return;
}

var location = includeDirective.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.
/// Based on <see href="https://github.com/dotnet/roslyn/blob/0504782ef845507260874f2efc253b36d1775685/src/Compilers/Core/Portable/SourceGeneration/GeneratedCodeUtilities.cs">GeneratedCodeUtilities</see>.
/// </summary>
private static bool IsGeneratedCode(SyntaxTree tree, AnalyzerConfigOptionsProvider optionsProvider)
private static bool IsIncludeDirective(SyntaxTrivia trivia)
{
if (optionsProvider.GetOptions(tree)
.TryGetValue("generated_code", out var generatedValue) &&
generatedValue.Equals("true", StringComparison.OrdinalIgnoreCase))
{
return true;
}
const string include = "include";

var filePath = tree.FilePath;
if (!string.IsNullOrEmpty(filePath))
var structure = trivia.GetStructure();
if (structure is null)
{
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;
}
return false;
}

// Check for <auto-generated> or <autogenerated> comment at the top of the file.
foreach (var trivia in tree.GetRoot().GetLeadingTrivia())
var content = structure.ChildTokens().FirstOrDefault(static token => token.IsKind(SyntaxKind.StringLiteralToken));
if (!content.IsKind(SyntaxKind.StringLiteralToken))
{
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;
}

return false;
var trimmedContent = content.Text.AsSpan().TrimStart();
return trimmedContent.StartsWith(include, StringComparison.Ordinal) &&
(trimmedContent.Length == include.Length || char.IsWhiteSpace(trimmedContent[include.Length]));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Testing;
using VerifyCS = Test.Utilities.CSharpSecurityCodeFixVerifier<
Microsoft.NetCore.CSharp.Analyzers.Usage.CSharpMissingShebangInFileBasedProgram,
Expand All @@ -15,14 +16,17 @@ public class MissingShebangInFileBasedProgramTests
[Fact]
public async Task EntryPointWithoutShebang_MultipleFiles_WarningAsync()
{
// Entry point file without shebang, multiple files - warning expected.
// Entry point file without shebang and a #:include file - warning expected.
await new VerifyCS.Test
{
TestState =
{
Sources =
{
("Test0.cs", """class Program { static void Main() { } }"""),
("Test0.cs", """
#:include Util.cs
class Program { static void Main() { } }
"""),
("Util.cs", """class Util { public static string Greet() => "hello"; }"""),
},
AnalyzerConfigFiles = { ("/.globalconfig", GlobalConfig) },
Expand All @@ -31,6 +35,25 @@ public async Task EntryPointWithoutShebang_MultipleFiles_WarningAsync()
new DiagnosticResult(MissingShebangInFileBasedProgram.Rule).WithLocation("Test0.cs", 1, 1),
},
},
SolutionTransforms = { EnableFileBasedProgramFeature },
}.RunAsync();
}

[Fact]
public async Task ExtraCompileFileNotFromIncludeDirective_NoDiagnosticAsync()
{
// A second Compile item from other MSBuild code does not require a shebang.
await new VerifyCS.Test
{
TestState =
{
Sources =
{
("Test0.cs", """class Program { static void Main() { } }"""),
("Util.cs", """class Util { public static string Greet() => "hello"; }"""),
},
AnalyzerConfigFiles = { ("/.globalconfig", GlobalConfig) },
},
}.RunAsync();
}

Expand Down Expand Up @@ -73,7 +96,10 @@ public async Task EntryPointWithoutShebang_CodeFixAddsShebangAsync()
{
Sources =
{
("Test0.cs", """class Program { static void Main() { } }"""),
("Test0.cs", """
#:include Util.cs
class Program { static void Main() { } }
"""),
("Util.cs", """class Util { public static string Greet() => "hello"; }"""),
},
AnalyzerConfigFiles = { ("/.globalconfig", GlobalConfig) },
Expand All @@ -88,23 +114,14 @@ public async Task EntryPointWithoutShebang_CodeFixAddsShebangAsync()
{
("Test0.cs", """
#!/usr/bin/env dotnet
#:include Util.cs
class Program { static void Main() { } }
"""),
("Util.cs", """class Util { public static string Greet() => "hello"; }"""),
},
},
CodeFixTestBehaviors = CodeFixTestBehaviors.SkipLocalDiagnosticCheck,
SolutionTransforms =
{
(solution, projectId) =>
{
// Enable #! shebang support in the parser.
var parseOptions = (CSharpParseOptions)solution.GetProject(projectId)!.ParseOptions!;
return solution.WithProjectParseOptions(projectId,
parseOptions.WithFeatures(parseOptions.Features.Concat(
[new KeyValuePair<string, string>("FileBasedProgram", "true")])));
},
},
SolutionTransforms = { EnableFileBasedProgramFeature },
}.RunAsync();
}

Expand All @@ -120,22 +137,14 @@ public async Task EntryPointWithShebang_MultipleFiles_NoDiagnosticAsync()
{
("Test0.cs", """
#!/usr/bin/env dotnet
#:include Util.cs
class Program { static void Main() { } }
"""),
("Util.cs", """class Util { public static string Greet() => "hello"; }"""),
},
AnalyzerConfigFiles = { ("/.globalconfig", GlobalConfig) },
},
SolutionTransforms =
{
(solution, projectId) =>
{
var parseOptions = (CSharpParseOptions)solution.GetProject(projectId)!.ParseOptions!;
return solution.WithProjectParseOptions(projectId,
parseOptions.WithFeatures(parseOptions.Features.Concat(
[new KeyValuePair<string, string>("FileBasedProgram", "true")])));
},
},
SolutionTransforms = { EnableFileBasedProgramFeature },
}.RunAsync();
}

Expand All @@ -160,8 +169,7 @@ public async Task EmptyEntryPointFilePath_NoDiagnosticAsync()
[Fact]
public async Task GeneratedCodeFile_NoDiagnosticAsync()
{
// Entry point file without shebang, but the second file is generated code (.g.cs),
// so there is effectively only one non-generated file - no diagnostic.
// Entry point file without shebang, but no #:include directive - no diagnostic.
await new VerifyCS.Test
{
TestState =
Expand All @@ -179,8 +187,7 @@ public async Task GeneratedCodeFile_NoDiagnosticAsync()
[Fact]
public async Task AutoGeneratedComment_NoDiagnosticAsync()
{
// Entry point file without shebang, but the second file has an <auto-generated> comment,
// so there is effectively only one non-generated file - no diagnostic.
// Entry point file without shebang, but no #:include directive - no diagnostic.
await new VerifyCS.Test
{
TestState =
Expand All @@ -202,15 +209,17 @@ public async Task AutoGeneratedComment_NoDiagnosticAsync()
[Fact]
public async Task GeneratedCodePlusRealFile_WarningAsync()
{
// Entry point file without shebang, a real second file, and a generated file.
// Two non-generated files exist, so a warning is expected.
// Entry point file without shebang and a #:include directive - warning expected.
await new VerifyCS.Test
{
TestState =
{
Sources =
{
("Test0.cs", """class Program { static void Main() { } }"""),
("Test0.cs", """
#:include Util.cs
class Program { static void Main() { } }
"""),
("Util.cs", """class Util { }"""),
("Test1.g.cs", """class Generated { }"""),
},
Expand All @@ -220,6 +229,7 @@ public async Task GeneratedCodePlusRealFile_WarningAsync()
new DiagnosticResult(MissingShebangInFileBasedProgram.Rule).WithLocation("Test0.cs", 1, 1),
},
},
SolutionTransforms = { EnableFileBasedProgramFeature },
}.RunAsync();
}

Expand All @@ -235,6 +245,7 @@ public async Task ShebangNotAtPositionZero_WarningAsync()
Sources =
{
("Test0.cs", """
#:include Util.cs
class Foo { }
#!/usr/bin/env dotnet
class Program { static void Main() { } }
Expand All @@ -246,20 +257,19 @@ class Program { static void Main() { } }
{
new DiagnosticResult(MissingShebangInFileBasedProgram.Rule).WithLocation("Test0.cs", 1, 1),
// Preprocessor directives must appear as the first non-whitespace character on a line
DiagnosticResult.CompilerError("CS1040").WithSpan("Test0.cs", 2, 1, 2, 2),
},
},
SolutionTransforms =
{
(solution, projectId) =>
{
var parseOptions = (CSharpParseOptions)solution.GetProject(projectId)!.ParseOptions!;
return solution.WithProjectParseOptions(projectId,
parseOptions.WithFeatures(parseOptions.Features.Concat(
[new KeyValuePair<string, string>("FileBasedProgram", "true")])));
DiagnosticResult.CompilerError("CS1040").WithSpan("Test0.cs", 3, 1, 3, 2),
},
},
SolutionTransforms = { EnableFileBasedProgramFeature },
}.RunAsync();
}

private static Solution EnableFileBasedProgramFeature(Solution solution, ProjectId projectId)
{
var parseOptions = (CSharpParseOptions)solution.GetProject(projectId)!.ParseOptions!;
return solution.WithProjectParseOptions(projectId,
parseOptions.WithFeatures(parseOptions.Features.Concat(
[new KeyValuePair<string, string>("FileBasedProgram", "true")])));
}
}
}
Loading
Loading