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

Report diagnostic for field and value identifiers in property accessors where the meaning would change as contextual keywords #73570

Merged
merged 16 commits into from
Jun 5, 2024
Merged
9 changes: 9 additions & 0 deletions src/Compilers/CSharp/Portable/CSharpResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -6865,6 +6865,12 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
<data name="INF_TooManyBoundLambdas_Title" xml:space="preserve">
<value>Compiling requires binding the lambda expression many times. Consider declaring the lambda expression with explicit parameter types, or if the containing method call is generic, consider using explicit type arguments.</value>
</data>
<data name="INF_IdentifierConflictWithContextualKeyword" xml:space="preserve">
<value>'{0}' is a contextual keyword, with a specific meaning, starting in language version {1}. Use '@{0}' to avoid a breaking change when compiling with language version {1} or later.</value>
</data>
<data name="INF_IdentifierConflictWithContextualKeyword_Title" xml:space="preserve">
<value>Identifier is a contextual keyword, with a specific meaning, in a later language version.</value>
</data>
<data name="ERR_EqualityContractRequiresGetter" xml:space="preserve">
<value>Record equality contract property '{0}' must have a get accessor.</value>
</data>
Expand Down Expand Up @@ -7920,6 +7926,9 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
<data name="IDS_FeatureRefUnsafeInIteratorAsync" xml:space="preserve">
<value>ref and unsafe in async and iterator methods</value>
</data>
<data name="IDS_FeatureFieldAndValueKeywords" xml:space="preserve">
<value>field and value keywords</value>
</data>
<data name="ERR_RefLocalAcrossAwait" xml:space="preserve">
<value>A 'ref' local cannot be preserved across 'await' or 'yield' boundary.</value>
</data>
Expand Down
118 changes: 117 additions & 1 deletion src/Compilers/CSharp/Portable/Compiler/MethodCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1782,10 +1782,20 @@ syntaxNode is ConstructorDeclarationSyntax constructorSyntax &&
assertBindIdentifierTargets(inMethodBinder, identifierMap, methodBody, diagnostics);
#endif

var compilation = bodyBinder.Compilation;

if (method.MethodKind is MethodKind.PropertyGet or MethodKind.PropertySet or MethodKind.EventAdd or MethodKind.EventRemove)
{
var requiredVersion = MessageID.IDS_FeatureFieldAndValueKeywords.RequiredVersion();
if (requiredVersion > compilation.LanguageVersion)
{
ReportFieldOrValueContextualKeywordConflicts(method, methodBody, diagnostics);
}
}

BoundNode methodBodyForSemanticModel = methodBody;
NullableWalker.SnapshotManager? snapshotManager = null;
ImmutableDictionary<Symbol, Symbol>? remappedSymbols = null;
var compilation = bodyBinder.Compilation;

nullableInitialState = getInitializerState(methodBody);

Expand Down Expand Up @@ -2149,6 +2159,112 @@ static void assertBindIdentifierTargets(InMethodBinder? inMethodBinder, Concurre
#endif
}

/// <summary>
/// Report a diagnostic for any 'field' or 'value' identifier in the bound tree where the
/// meaning will change when the identifier is considered a contextual keyword.
/// </summary>
private static void ReportFieldOrValueContextualKeywordConflicts(MethodSymbol method, BoundNode node, BindingDiagnosticBag diagnostics)
{
Debug.Assert(method.MethodKind is MethodKind.PropertyGet or MethodKind.PropertySet or MethodKind.EventAdd or MethodKind.EventRemove);
Debug.Assert(method.AssociatedSymbol is PropertySymbol or EventSymbol);

PooledDictionary<SyntaxNode, SyntaxToken>? valueIdentifiers = null;

foreach (var token in node.Syntax.DescendantTokens())
{
Debug.Assert(token.Parent is { });
if (token.Kind() != SyntaxKind.IdentifierToken)
{
continue;
}
var syntax = token.Parent;
switch (syntax)
{
case IdentifierNameSyntax identifierName when identifierName.Identifier == token:
case GenericNameSyntax genericName when genericName.Identifier == token:
case TupleElementSyntax tupleElement when tupleElement.Identifier == token:
case FromClauseSyntax fromClause when fromClause.Identifier == token:
case LetClauseSyntax letClause when letClause.Identifier == token:
case JoinClauseSyntax joinClause when joinClause.Identifier == token:
case JoinIntoClauseSyntax joinIntoClause when joinIntoClause.Identifier == token:
case QueryContinuationSyntax queryContinuation when queryContinuation.Identifier == token:
case LocalFunctionStatementSyntax localFunctionStatement when localFunctionStatement.Identifier == token:
case VariableDeclaratorSyntax variableDeclarator when variableDeclarator.Identifier == token:
case SingleVariableDesignationSyntax singleVariable when singleVariable.Identifier == token:
case LabeledStatementSyntax labeledStatement when labeledStatement.Identifier == token:
case ForEachStatementSyntax forEachStatement when forEachStatement.Identifier == token:
case CatchDeclarationSyntax catchDeclaration when catchDeclaration.Identifier == token:
case TypeParameterSyntax typeParameter when typeParameter.Identifier == token:
case ParameterSyntax parameter when parameter.Identifier == token:
case AttributeTargetSpecifierSyntax targetSpecifier when targetSpecifier.Identifier == token:
switch (token.Text)
{
case "field":
if (method.AssociatedSymbol is PropertySymbol { IsIndexer: false })
{
// Report "field" conflict with keyword.
reportConflict(syntax, token, diagnostics);
}
break;
case "value":
if (method.MethodKind is MethodKind.PropertySet or MethodKind.EventAdd or MethodKind.EventRemove)
{
// Record the potential "value" conflict, and report conflicts later,
// after dropping any that refer to the implicit parameter.
valueIdentifiers ??= PooledDictionary<SyntaxNode, SyntaxToken>.GetInstance();
valueIdentifiers.Add(syntax, token);
}
break;
}
break;
default:
Copy link
Member

Choose a reason for hiding this comment

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

TBH i find hte approach taken in this pr extremely surprising. It's not how i would have expected this to be done.

Instead, i would expect us to treat his how we treat query-keywords or the await-keyword. Specifically, the parser should enter into a new contextual parsing state when we enter into these specific contexts.

Within that appropriate context, we then have the following behavior:

  1. in c# 13, the word becomes a keyword, and the new semantics take effect.
  2. prior to c# 12, when eating an identifier, if we have one of these keywords, we convert it to an identifier, and report the diagnostics about the potential future issues.

I think we also need this for incremental parsing to work properly. If we reinterpret the statements from an accessor into a different context (or vice-versa), then we need to reparse since field/value will change from keywords to identifiers.

--

TLDR: as a contextual keyword, this should work the same way as async/await and query-keywords.

Copy link
Member

Choose a reason for hiding this comment

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

Note: i was able to get us additional bits on NodeFlags. So we can store this info there as well for incremental parsing purposes. A

Copy link
Member Author

Choose a reason for hiding this comment

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

  1. prior to c# 12, when eating an identifier, if we have one of these keywords, we convert it to an identifier, and report the diagnostics about the potential future issues.

The context from the parser might help, but the diagnostic may need to be reported in binding since we only want a diagnostic when the identifier would bind differently as a keyword.

Copy link
Member

Choose a reason for hiding this comment

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

my approach supports that. these would be identifiers whose contextual kind would either be 'nothing' (which means it's a legal usage of field/value as an identifier), or it has a contextual kind of SyntaxKind.FieldKeyword or SyntaxKind.ValueKeyword. IN that case, in binding you report a warning saying this will change meaning.

basically, the parser has the core logic for figuring out if it's an identifier or keyword. if it's a true identifier, great, you get an identifier and everything works as normal. If it's contextual keyword, then in C# 13 you get a keyword and the new semantics. In C# 12 you get an identifier, marked as being a contextual keyword, and the binder warns you.

Note: i don't actually see why it as to be the binder that needs to care. Sicne C# 13 will have these rules set up at the syntactic level, it feels fine for the parser to warn here for me. But i'm fine with it beign the binder if you prefer that.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think what Chuck is getting at is that, for example, there's no problem with using identifier 'value' to reference the setter value parameter. The parsing will technically change from C# 12 to 13, but it will still refer to the same thing in its keyword form.

So in order to know whether to give diagnostics, compiler needs to know whether 'value' is referring to that parameter, or referring to something else such as a 'value' local declared in a nested function.

FWIW, in the case I just described, I feel ok with saying you did something really weird and you don't get to have this warning. It's the .01% case.

Maybe we should only warn for use of identifier 'field' in property accessors and just do it in the parser.

Copy link
Member

Choose a reason for hiding this comment

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

my approach would work for this afaict. the node indicates the information needed by the binder to warn.

// The cases above should be the complete set of identifiers
// expected in an accessor with -langversion:12 or earlier.
Debug.Assert(false);
break;
}
}

if (valueIdentifiers is { })
{
// Remove references to the implicit "value" parameter.
var checker = new ValueIdentifierChecker(valueIdentifiers);
checker.Visit(node);
// Report "value" conflicts.
foreach (var pair in valueIdentifiers)
{
reportConflict(pair.Key, pair.Value, diagnostics);
}
valueIdentifiers.Free();
}

static void reportConflict(SyntaxNode syntax, SyntaxToken identifierToken, BindingDiagnosticBag diagnostics)
{
var requiredVersion = MessageID.IDS_FeatureFieldAndValueKeywords.RequiredVersion();
diagnostics.Add(ErrorCode.INF_IdentifierConflictWithContextualKeyword, syntax, identifierToken.Text, requiredVersion.ToDisplayString());
}
}

private sealed class ValueIdentifierChecker : BoundTreeWalkerWithStackGuardWithoutRecursionOnTheLeftOfBinaryOperator
{
private readonly Dictionary<SyntaxNode, SyntaxToken> _valueIdentifiers;

public ValueIdentifierChecker(Dictionary<SyntaxNode, SyntaxToken> valueIdentifiers)
{
_valueIdentifiers = valueIdentifiers;
}

public override BoundNode? VisitParameter(BoundParameter node)
{
if (node.ParameterSymbol is SynthesizedAccessorValueParameterSymbol { Name: "value" })
{
_valueIdentifiers.Remove(node.Syntax);
}

return base.VisitParameter(node);
}
}

#if DEBUG
private static bool IsEmptyRewritePossible(BoundNode node)
{
Expand Down
1 change: 1 addition & 0 deletions src/Compilers/CSharp/Portable/Errors/ErrorCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2324,6 +2324,7 @@ internal enum ErrorCode
ERR_RefStructDoesNotSupportDefaultInterfaceImplementationForMember = 9245,
ERR_BadNonVirtualInterfaceMemberAccessOnAllowsRefLike = 9246,
ERR_BadAllowByRefLikeEnumerator = 9247,
INF_IdentifierConflictWithContextualKeyword = 9248,

// Note: you will need to do the following after adding errors:
// 1) Update ErrorFacts.IsBuildOnlyDiagnostic (src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs)
Expand Down
1 change: 1 addition & 0 deletions src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2450,6 +2450,7 @@ or ErrorCode.ERR_NotRefStructConstraintNotSatisfied
or ErrorCode.ERR_RefStructDoesNotSupportDefaultInterfaceImplementationForMember
or ErrorCode.ERR_BadNonVirtualInterfaceMemberAccessOnAllowsRefLike
or ErrorCode.ERR_BadAllowByRefLikeEnumerator
or ErrorCode.INF_IdentifierConflictWithContextualKeyword
=> false,
};
#pragma warning restore CS8524 // The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value.
Expand Down
2 changes: 2 additions & 0 deletions src/Compilers/CSharp/Portable/Errors/MessageID.cs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ internal enum MessageID
IDS_FeatureRefUnsafeInIteratorAsync = MessageBase + 12843,

IDS_FeatureRefStructInterfaces = MessageBase + 12844,
IDS_FeatureFieldAndValueKeywords = MessageBase + 12845,
}

// Message IDs may refer to strings that need to be localized.
Expand Down Expand Up @@ -472,6 +473,7 @@ internal static LanguageVersion RequiredVersion(this MessageID feature)
case MessageID.IDS_FeatureParamsCollections:
case MessageID.IDS_FeatureRefUnsafeInIteratorAsync:
case MessageID.IDS_FeatureRefStructInterfaces:
case MessageID.IDS_FeatureFieldAndValueKeywords:
return LanguageVersion.Preview;

// C# 12.0 features.
Expand Down

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

15 changes: 15 additions & 0 deletions src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf

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

15 changes: 15 additions & 0 deletions src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf

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

15 changes: 15 additions & 0 deletions src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf

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

15 changes: 15 additions & 0 deletions src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf

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

Loading
Loading