Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Produce specific error for misplaced 'record' keyword #59622

Merged
merged 7 commits into from
Mar 1, 2022
Merged
Show file tree
Hide file tree
Changes from 2 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
59 changes: 36 additions & 23 deletions src/Compilers/CSharp/Portable/Parser/LanguageParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

namespace Microsoft.CodeAnalysis.CSharp.Syntax.InternalSyntax
{
using System.Diagnostics.CodeAnalysis;
Copy link
Member

@333fred 333fred Feb 24, 2022

Choose a reason for hiding this comment

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

Top of the file? #Resolved

using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Syntax.InternalSyntax;

Expand Down Expand Up @@ -1542,31 +1543,21 @@ private void CheckForVersionSpecificModifiers(SyntaxListBuilder modifiers, Synta

private TypeDeclarationSyntax ParseClassOrStructOrInterfaceDeclaration(SyntaxList<AttributeListSyntax> attributes, SyntaxListBuilder modifiers)
{
Debug.Assert(this.CurrentToken.Kind == SyntaxKind.ClassKeyword ||
this.CurrentToken.Kind == SyntaxKind.StructKeyword ||
this.CurrentToken.Kind == SyntaxKind.InterfaceKeyword ||
CurrentToken.ContextualKind == SyntaxKind.RecordKeyword);
Debug.Assert(this.CurrentToken.Kind is SyntaxKind.ClassKeyword or SyntaxKind.StructKeyword or SyntaxKind.InterfaceKeyword ||
this.CurrentToken.ContextualKind == SyntaxKind.RecordKeyword);

// "top-level" expressions and statements should never occur inside an asynchronous context
Debug.Assert(!IsInAsync);

var keyword = ConvertToKeyword(this.EatToken());

var outerSaveTerm = _termState;
SyntaxToken? recordModifier = null;
if (keyword.Kind == SyntaxKind.RecordKeyword)

if (tryScanRecordStart(out var keyword, out var recordModifier))
{
_termState |= TerminatorState.IsEndOfRecordSignature;
recordModifier = eatRecordModifierIfAvailable();
}
else if (keyword.Kind is SyntaxKind.StructKeyword or SyntaxKind.ClassKeyword &&
CurrentToken.ContextualKind == SyntaxKind.RecordKeyword &&
IsFeatureEnabled(MessageID.IDS_FeatureRecordStructs) &&
PeekToken(1).Kind is SyntaxKind.IdentifierToken)
else
{
// Provide a specific diagnostic on `struct record S` or `class record C`
var misplacedRecord = this.AddError(this.EatToken(), ErrorCode.ERR_MisplacedRecord);
keyword = AddTrailingSkippedSyntax(keyword, misplacedRecord);
keyword = ConvertToKeyword(this.EatToken());
}

var saveTerm = _termState;
Expand Down Expand Up @@ -1684,17 +1675,39 @@ private TypeDeclarationSyntax ParseClassOrStructOrInterfaceDeclaration(SyntaxLis
}
}

SyntaxToken? eatRecordModifierIfAvailable()
bool tryScanRecordStart([NotNullWhen(true)] out SyntaxToken? keyword, out SyntaxToken? recordModifier)
{
Debug.Assert(keyword.Kind == SyntaxKind.RecordKeyword);
if (CurrentToken.Kind is SyntaxKind.ClassKeyword or SyntaxKind.StructKeyword)
if (this.CurrentToken.ContextualKind == SyntaxKind.RecordKeyword)
{
var result = EatToken();
result = CheckFeatureAvailability(result, MessageID.IDS_FeatureRecordStructs);
return result;
keyword = ConvertToKeyword(this.EatToken());
recordModifier = this.CurrentToken.Kind is SyntaxKind.ClassKeyword or SyntaxKind.StructKeyword
? CheckFeatureAvailability(EatToken(), MessageID.IDS_FeatureRecordStructs)
: null;

return true;
}

return null;
if (this.CurrentToken.Kind is SyntaxKind.StructKeyword or SyntaxKind.ClassKeyword &&
this.PeekToken(1).ContextualKind == SyntaxKind.RecordKeyword &&
IsFeatureEnabled(MessageID.IDS_FeatureRecordStructs) &&
PeekToken(2).Kind is SyntaxKind.IdentifierToken)
{
// Provide a specific diagnostic on `struct record S` or `class record C`
var misplacedToken = this.EatToken();

// Parse out 'record' but place 'struct/class' as leading skipped trivia on it.
keyword = AddLeadingSkippedSyntax(
this.AddError(ConvertToKeyword(this.EatToken()), ErrorCode.ERR_MisplacedRecord),
misplacedToken);

// Treat `struct record` as a RecordStructDeclaration, and `class record` as a RecordDeclaration.
recordModifier = SyntaxFactory.MissingToken(misplacedToken.Kind);
return true;
}

keyword = null;
recordModifier = null;
return false;
}

static TypeDeclarationSyntax constructTypeDeclaration(ContextAwareSyntax syntaxFactory, SyntaxList<AttributeListSyntax> attributes, SyntaxListBuilder modifiers, SyntaxToken keyword, SyntaxToken? recordModifier,
Expand Down
134 changes: 38 additions & 96 deletions src/Compilers/CSharp/Test/Syntax/Parsing/RecordParsing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3152,67 +3152,38 @@ public void RecordStructParsing_WrongOrder_CSharp10()
UsingTree(text, options: TestOptions.Regular10,
// (1,8): error CS9012: Unexpected keyword 'record'. Did you mean 'record struct' or 'record class'?
// struct record C(int X, int Y);
Diagnostic(ErrorCode.ERR_MisplacedRecord, "record").WithLocation(1, 8),
// (1,16): error CS1514: { expected
// struct record C(int X, int Y);
Diagnostic(ErrorCode.ERR_LbraceExpected, "(").WithLocation(1, 16),
// (1,16): error CS1513: } expected
// struct record C(int X, int Y);
Diagnostic(ErrorCode.ERR_RbraceExpected, "(").WithLocation(1, 16),
// (1,16): error CS8803: Top-level statements must precede namespace and type declarations.
// struct record C(int X, int Y);
Diagnostic(ErrorCode.ERR_TopLevelStatementAfterNamespaceOrType, "(int X, int Y);").WithLocation(1, 16)
);
Diagnostic(ErrorCode.ERR_MisplacedRecord, "record").WithLocation(1, 8));

N(SyntaxKind.CompilationUnit);
{
N(SyntaxKind.StructDeclaration);
N(SyntaxKind.RecordStructDeclaration);
{
N(SyntaxKind.StructKeyword);
N(SyntaxKind.RecordKeyword);
M(SyntaxKind.StructKeyword);
N(SyntaxKind.IdentifierToken, "C");
M(SyntaxKind.OpenBraceToken);
M(SyntaxKind.CloseBraceToken);
}
N(SyntaxKind.GlobalStatement);
{
N(SyntaxKind.ExpressionStatement);
N(SyntaxKind.ParameterList);
{
N(SyntaxKind.TupleExpression);
N(SyntaxKind.OpenParenToken);
N(SyntaxKind.Parameter);
{
N(SyntaxKind.OpenParenToken);
N(SyntaxKind.Argument);
N(SyntaxKind.PredefinedType);
{
N(SyntaxKind.DeclarationExpression);
{
N(SyntaxKind.PredefinedType);
{
N(SyntaxKind.IntKeyword);
}
N(SyntaxKind.SingleVariableDesignation);
{
N(SyntaxKind.IdentifierToken, "X");
}
}
N(SyntaxKind.IntKeyword);
}
N(SyntaxKind.CommaToken);
N(SyntaxKind.Argument);
N(SyntaxKind.IdentifierToken, "X");
}
N(SyntaxKind.CommaToken);
N(SyntaxKind.Parameter);
{
N(SyntaxKind.PredefinedType);
{
N(SyntaxKind.DeclarationExpression);
{
N(SyntaxKind.PredefinedType);
{
N(SyntaxKind.IntKeyword);
}
N(SyntaxKind.SingleVariableDesignation);
{
N(SyntaxKind.IdentifierToken, "Y");
}
}
N(SyntaxKind.IntKeyword);
}
N(SyntaxKind.CloseParenToken);
N(SyntaxKind.IdentifierToken, "Y");
}
N(SyntaxKind.SemicolonToken);
N(SyntaxKind.CloseParenToken);
}
N(SyntaxKind.SemicolonToken);
}
N(SyntaxKind.EndOfFileToken);
}
Expand Down Expand Up @@ -3378,67 +3349,38 @@ public void RecordClassParsing_WrongOrder_CSharp10()
UsingTree(text, options: TestOptions.Regular10,
// (1,7): error CS9012: Unexpected keyword 'record'. Did you mean 'record struct' or 'record class'?
// class record C(int X, int Y);
Diagnostic(ErrorCode.ERR_MisplacedRecord, "record").WithLocation(1, 7),
// (1,15): error CS1514: { expected
// class record C(int X, int Y);
Diagnostic(ErrorCode.ERR_LbraceExpected, "(").WithLocation(1, 15),
// (1,15): error CS1513: } expected
// class record C(int X, int Y);
Diagnostic(ErrorCode.ERR_RbraceExpected, "(").WithLocation(1, 15),
// (1,15): error CS8803: Top-level statements must precede namespace and type declarations.
// class record C(int X, int Y);
Diagnostic(ErrorCode.ERR_TopLevelStatementAfterNamespaceOrType, "(int X, int Y);").WithLocation(1, 15)
);
Diagnostic(ErrorCode.ERR_MisplacedRecord, "record").WithLocation(1, 7));

N(SyntaxKind.CompilationUnit);
{
N(SyntaxKind.ClassDeclaration);
N(SyntaxKind.RecordDeclaration);
{
N(SyntaxKind.ClassKeyword);
N(SyntaxKind.RecordKeyword);
M(SyntaxKind.ClassKeyword);
N(SyntaxKind.IdentifierToken, "C");
M(SyntaxKind.OpenBraceToken);
M(SyntaxKind.CloseBraceToken);
}
N(SyntaxKind.GlobalStatement);
{
N(SyntaxKind.ExpressionStatement);
N(SyntaxKind.ParameterList);
{
N(SyntaxKind.TupleExpression);
N(SyntaxKind.OpenParenToken);
N(SyntaxKind.Parameter);
{
N(SyntaxKind.OpenParenToken);
N(SyntaxKind.Argument);
N(SyntaxKind.PredefinedType);
{
N(SyntaxKind.DeclarationExpression);
{
N(SyntaxKind.PredefinedType);
{
N(SyntaxKind.IntKeyword);
}
N(SyntaxKind.SingleVariableDesignation);
{
N(SyntaxKind.IdentifierToken, "X");
}
}
N(SyntaxKind.IntKeyword);
}
N(SyntaxKind.CommaToken);
N(SyntaxKind.Argument);
N(SyntaxKind.IdentifierToken, "X");
}
N(SyntaxKind.CommaToken);
N(SyntaxKind.Parameter);
{
N(SyntaxKind.PredefinedType);
{
N(SyntaxKind.DeclarationExpression);
{
N(SyntaxKind.PredefinedType);
{
N(SyntaxKind.IntKeyword);
}
N(SyntaxKind.SingleVariableDesignation);
{
N(SyntaxKind.IdentifierToken, "Y");
}
}
N(SyntaxKind.IntKeyword);
}
N(SyntaxKind.CloseParenToken);
N(SyntaxKind.IdentifierToken, "Y");
}
N(SyntaxKind.SemicolonToken);
N(SyntaxKind.CloseParenToken);
}
N(SyntaxKind.SemicolonToken);
}
N(SyntaxKind.EndOfFileToken);
}
Expand Down