diff --git a/src/Compilers/CSharp/Portable/Parser/DirectiveParser.cs b/src/Compilers/CSharp/Portable/Parser/DirectiveParser.cs index 4c94b14373785..95278d9d69b9a 100644 --- a/src/Compilers/CSharp/Portable/Parser/DirectiveParser.cs +++ b/src/Compilers/CSharp/Portable/Parser/DirectiveParser.cs @@ -108,8 +108,14 @@ public CSharpSyntaxNode ParseDirective( break; default: - if (contextualKind == SyntaxKind.ExclamationToken && hashPosition == 0 && !hash.HasTrailingTrivia) + if (contextualKind == SyntaxKind.ExclamationToken) { + // Always parse as a shebang directive, but report an error if not at position 0 + if (hashPosition != 0 || hash.HasTrailingTrivia) + { + hash = this.AddError(hash, ErrorCode.ERR_BadDirectivePlacement); + } + result = this.ParseShebangDirective(hash, this.EatToken(SyntaxKind.ExclamationToken), isActive); } else if (contextualKind == SyntaxKind.ColonToken && !hash.HasTrailingTrivia) @@ -679,9 +685,6 @@ private DirectiveTriviaSyntax ParsePragmaDirective(SyntaxToken hash, SyntaxToken private DirectiveTriviaSyntax ParseShebangDirective(SyntaxToken hash, SyntaxToken exclamation, bool isActive) { - // Shebang directives must appear at the first position in the file - // (before all other directives), so they should always be active. - Debug.Assert(isActive); return SyntaxFactory.ShebangDirectiveTrivia(hash, exclamation, this.ParseEndOfDirectiveWithOptionalPreprocessingMessage(), isActive); } diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/IgnoredDirectiveParsingTests.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/IgnoredDirectiveParsingTests.cs index aff2e711954b2..401b3ec3e6139 100644 --- a/src/Compilers/CSharp/Test/Syntax/Parsing/IgnoredDirectiveParsingTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/IgnoredDirectiveParsingTests.cs @@ -171,26 +171,22 @@ public void ShebangNotFirst(bool script, bool featureFlag) VerifyTrivia(); UsingTree(source, options, - // (1,2): error CS1024: Preprocessor directive expected + // (1,2): error CS1040: Preprocessor directives must appear as the first non-whitespace character on a line // #!xyz - Diagnostic(ErrorCode.ERR_PPDirectiveExpected, "#").WithLocation(1, 2)); + Diagnostic(ErrorCode.ERR_BadDirectivePlacement, "#").WithLocation(1, 2)); N(SyntaxKind.CompilationUnit); { N(SyntaxKind.EndOfFileToken); { L(SyntaxKind.WhitespaceTrivia, " "); - L(SyntaxKind.BadDirectiveTrivia); + L(SyntaxKind.ShebangDirectiveTrivia); { N(SyntaxKind.HashToken); - M(SyntaxKind.IdentifierToken); + N(SyntaxKind.ExclamationToken); N(SyntaxKind.EndOfDirectiveToken); { - L(SyntaxKind.SkippedTokensTrivia); - { - N(SyntaxKind.ExclamationToken); - N(SyntaxKind.IdentifierToken, "xyz"); - } + L(SyntaxKind.PreprocessingMessageTrivia, "xyz"); } } } @@ -556,4 +552,136 @@ public void NoColon() } EOF(); } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/78054")] + public void ShebangCorrectlyPlaced() + { + var source = """ + #!/usr/bin/env dotnet + Console.WriteLine("Hello"); + """; + + VerifyTrivia(); + UsingTree(source, TestOptions.Regular); + + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.GlobalStatement); + { + N(SyntaxKind.ExpressionStatement); + { + N(SyntaxKind.InvocationExpression); + { + N(SyntaxKind.SimpleMemberAccessExpression); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "Console"); + { + L(SyntaxKind.ShebangDirectiveTrivia); + { + N(SyntaxKind.HashToken); + N(SyntaxKind.ExclamationToken); + N(SyntaxKind.EndOfDirectiveToken); + { + L(SyntaxKind.PreprocessingMessageTrivia, "/usr/bin/env dotnet"); + T(SyntaxKind.EndOfLineTrivia, "\n"); + } + } + } + } + N(SyntaxKind.DotToken); + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "WriteLine"); + } + } + N(SyntaxKind.ArgumentList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.Argument); + { + N(SyntaxKind.StringLiteralExpression); + { + N(SyntaxKind.StringLiteralToken, "\"Hello\""); + } + } + N(SyntaxKind.CloseParenToken); + } + } + N(SyntaxKind.SemicolonToken); + } + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/78054")] + public void ShebangWithTriviaInBetween() + { + var source = """ + # !xyz + """; + + VerifyTrivia(); + UsingTree(source, TestOptions.Regular, + // (1,1): error CS1040: Preprocessor directives must appear as the first non-whitespace character on a line + // # !xyz + Diagnostic(ErrorCode.ERR_BadDirectivePlacement, "#").WithLocation(1, 1)); + + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.EndOfFileToken); + { + L(SyntaxKind.ShebangDirectiveTrivia); + { + N(SyntaxKind.HashToken); + { + T(SyntaxKind.WhitespaceTrivia, " "); + } + N(SyntaxKind.ExclamationToken); + N(SyntaxKind.EndOfDirectiveToken); + { + L(SyntaxKind.PreprocessingMessageTrivia, "xyz"); + } + } + } + } + EOF(); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/78054")] + public void ShebangIncorrectlyPlaced() + { + var source = """ + // Comment + #!xyz + """; + + VerifyTrivia(); + UsingTree(source, TestOptions.Regular, + // (2,1): error CS1040: Preprocessor directives must appear as the first non-whitespace character on a line + // #!xyz + Diagnostic(ErrorCode.ERR_BadDirectivePlacement, "#").WithLocation(2, 1)); + + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.EndOfFileToken); + { + L(SyntaxKind.SingleLineCommentTrivia, "// Comment"); + L(SyntaxKind.EndOfLineTrivia, "\n"); + L(SyntaxKind.ShebangDirectiveTrivia); + { + N(SyntaxKind.HashToken); + N(SyntaxKind.ExclamationToken); + N(SyntaxKind.EndOfDirectiveToken); + { + L(SyntaxKind.PreprocessingMessageTrivia, "xyz"); + } + } + } + } + EOF(); + } } diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/ScriptParsingTests.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/ScriptParsingTests.cs index 4ca42f0fd2622..4723eaa9c9492 100644 --- a/src/Compilers/CSharp/Test/Syntax/Parsing/ScriptParsingTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/ScriptParsingTests.cs @@ -9672,16 +9672,16 @@ public void Shebang() public void ShebangNotFirstCharacter() { ParseAndValidate(" #!/usr/bin/env csi", TestOptions.Script, - new ErrorDescription { Code = (int)ErrorCode.ERR_PPDirectiveExpected, Line = 1, Column = 2 }); + new ErrorDescription { Code = (int)ErrorCode.ERR_BadDirectivePlacement, Line = 1, Column = 2 }); ParseAndValidate("\n#!/usr/bin/env csi", TestOptions.Script, - new ErrorDescription { Code = (int)ErrorCode.ERR_PPDirectiveExpected, Line = 2, Column = 1 }); + new ErrorDescription { Code = (int)ErrorCode.ERR_BadDirectivePlacement, Line = 2, Column = 1 }); ParseAndValidate("\r\n#!/usr/bin/env csi", TestOptions.Script, - new ErrorDescription { Code = (int)ErrorCode.ERR_PPDirectiveExpected, Line = 2, Column = 1 }); + new ErrorDescription { Code = (int)ErrorCode.ERR_BadDirectivePlacement, Line = 2, Column = 1 }); ParseAndValidate("#!/bin/sh\r\n#!/usr/bin/env csi", TestOptions.Script, - new ErrorDescription { Code = (int)ErrorCode.ERR_PPDirectiveExpected, Line = 2, Column = 1 }); + new ErrorDescription { Code = (int)ErrorCode.ERR_BadDirectivePlacement, Line = 2, Column = 1 }); ParseAndValidate("a #!/usr/bin/env csi", TestOptions.Script, new ErrorDescription { Code = (int)ErrorCode.ERR_BadDirectivePlacement, Line = 1, Column = 3 }); @@ -9698,7 +9698,7 @@ public void ShebangNoBang() public void ShebangSpaceBang() { ParseAndValidate("# !/usr/bin/env csi", TestOptions.Script, - new ErrorDescription { Code = (int)ErrorCode.ERR_PPDirectiveExpected, Line = 1, Column = 1 }); + new ErrorDescription { Code = (int)ErrorCode.ERR_BadDirectivePlacement, Line = 1, Column = 1 }); } [Fact] diff --git a/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxNodeTests.cs b/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxNodeTests.cs index 913605abf6fbc..a8b3dc5a2e8e6 100644 --- a/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxNodeTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxNodeTests.cs @@ -383,9 +383,8 @@ public void TestContainsDirective() testContainsHelper1("#undef x", SyntaxKind.UndefDirectiveTrivia); testContainsHelper1("#warning", SyntaxKind.WarningDirectiveTrivia); - // #! is special and is only recognized at start of a file and nowhere else. testContainsHelper2(new[] { SyntaxKind.ShebangDirectiveTrivia }, SyntaxFactory.ParseCompilationUnit("#!command", options: TestOptions.Script)); - testContainsHelper2(new[] { SyntaxKind.BadDirectiveTrivia }, SyntaxFactory.ParseCompilationUnit(" #!command", options: TestOptions.Script)); + testContainsHelper2(new[] { SyntaxKind.ShebangDirectiveTrivia }, SyntaxFactory.ParseCompilationUnit(" #!command", options: TestOptions.Script)); testContainsHelper2(new[] { SyntaxKind.ShebangDirectiveTrivia }, SyntaxFactory.ParseCompilationUnit("#!command", options: TestOptions.Regular)); testContainsHelper2([SyntaxKind.IgnoredDirectiveTrivia], SyntaxFactory.ParseCompilationUnit("#:x")); diff --git a/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs b/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs index a84ea085613c6..ddfc4449da288 100644 --- a/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs +++ b/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs @@ -1528,8 +1528,7 @@ public async Task ShebangNotAsFirstCommentInScript(TestHost testHost) var expected = new[] { - PPKeyword("#"), - PPText("!/usr/bin/env scriptcs"), + Comment("#!/usr/bin/env scriptcs"), Identifier("System"), Operators.Dot, Identifier("Console"),