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
11 changes: 7 additions & 4 deletions src/Compilers/CSharp/Portable/Parser/DirectiveParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

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

It looks like the directive is the first non-whitespace character on the line in this case.

Copy link
Member

Choose a reason for hiding this comment

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

Agree, that this is somewhat confusing, though I'm not totally sure its worth fixing.

// #!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");
}
}
}
Expand Down Expand Up @@ -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();
}
}
10 changes: 5 additions & 5 deletions src/Compilers/CSharp/Test/Syntax/Parsing/ScriptParsingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down