Skip to content

Extend scope of type parameters in attributes#40525

Merged
jcouv merged 10 commits intodotnet:features/nameof-parameterfrom
jcouv:nameof-scope
Apr 8, 2022
Merged

Extend scope of type parameters in attributes#40525
jcouv merged 10 commits intodotnet:features/nameof-parameterfrom
jcouv:nameof-scope

Conversation

@jcouv
Copy link
Member

@jcouv jcouv commented Dec 20, 2019

Also fixes the type parameter scoping on records (fixes #60379)
Fixes #60194

This PR adds type parameters inside nameof in attributes on methods (and local functions).

Some context on current rules:
Type parameters are in scope in within a method declaration (attributes on type parameters, on parameters, in parameter default values, and obviously in parameter types and type constraints), but we hide them when binding identifiers, except inside nameof, or when binding types (ie. sizeof/typeof/is/as/…).
So = default(T) and = nameof(T) will bind to type parameter, whereas = T will bind to something else (like a const from outer scope).
But type parameters were not in scope in attributes on the method itself at all. (note: :this contrasts with attributes on a type, where the type parameters are simply in scope)

Overview:

  • LocalBinderFactory.VisitInvocationExpression adds a NameofBinder for some invocation expressions (that pass syntactic check MayBeNameofOperator).
    NameofBinder is a no-op when it detects a proper "nameof" method in its enclosing scope.
    When the NameofBinder is chained on a ContextualAttributeBinder, it pulls in additional identifiers in scope from type parameters on attribute target (given by ContextualAttributeBinder). Since type parameters are in scope (albeit filtered out) in most locations we need them to be (attributes on types, type parameter, parameters), we just need to add them in attributes on methods.
  • Binder.TryBindNameofOperator gets a binder from binder factories, so that the above scopes kick in.
  • SyntaxTreeSemanticModel.CreateModelForAttribute adds a ContextualAttribute binder (with proper attribute target) in the RootBinder for the AttributeSemanticModel it creates.
  • MemberSemanticModel.GetEnclosingBinderInternalWithinRoot adds a NameofBinder on some invocation expressions.
  • Changes to BinderFactoryVisitor.VisitTypeDeclarationCore and SyntaxTreeSemanticModel.GetDeclarationName relate to records.

Relates to #40524 (test plan)

@jcouv jcouv self-assigned this Dec 20, 2019
@jcouv jcouv added this to the Compiler.Net5 milestone Dec 20, 2019
@jcouv jcouv force-pushed the nameof-scope branch 2 times, most recently from 6bd8e4e to 5076a55 Compare December 30, 2019 23:13
@jcouv jcouv added the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label Jan 6, 2020
@jaredpar jaredpar modified the milestones: Compiler.Net5, 16.8 Jun 23, 2020
@jinujoseph jinujoseph modified the milestones: 16.8, 16.9 Nov 12, 2020
Base automatically changed from master to main March 3, 2021 23:52
@jinujoseph jinujoseph modified the milestones: 16.9, 17.0 Jun 1, 2021
@jinujoseph jinujoseph modified the milestones: 17.0, 17.1 Dec 10, 2021
@jcouv jcouv force-pushed the nameof-scope branch 3 times, most recently from 5ab7314 to 14578af Compare February 22, 2022 23:35
@jcouv jcouv force-pushed the nameof-scope branch 3 times, most recently from fb7b673 to fd18833 Compare March 14, 2022 20:42
{
if ((_lazyIsInsideNameof & _lazyIsInsideNameof_IsInitialized) == 0)
{
bool isInsideNameof = !NextRequired.InvocableNameofInScope() || base.IsInsideNameof;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 6, 2022

Choose a reason for hiding this comment

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

!NextRequired.InvocableNameofInScope() || base.IsInsideNameof

In combination with other logic in this class, this logic feels incorrect. I think our intent is to optimistically add NameofBinder into the chain. Later on, we are supposed to check if the specific invocation expression is supposed to be treated as a "nameof expression". Only in that case the binder is supposed to have any impact and handle any requests. In all other cases, it shouldn't handle any requests and pretend as if it is not in the chain by simply delegating to the Next. So, for the purpose of determining whether this binder should have any meaningful impact, asking what base thinks is incorrect because the base answers question about different nodes, if any. It feels like we need a property that will answer the question whether this binder is a "true" nameof binder. And that property should drive the behavior instead of the overridden IsInsideNameof because it doesn't provide information specific to this binder. #Closed

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for the clarification. Makes sense

}

protected override SyntaxNode EnclosingNameofArgument => _nameofArgument;
protected override SyntaxNode? EnclosingNameofArgument => IsInsideNameof ? _nameofArgument : base.EnclosingNameofArgument;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 6, 2022

Choose a reason for hiding this comment

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

IsInsideNameof

The logic doesn't look correct now because IsInsideNameof can return true even when !NextRequired.InvocableNameofInScope() is false. #Closed


internal override void LookupSymbolsInSingleBinder(LookupResult result, string name, int arity, ConsList<TypeSymbol> basesBeingResolved, LookupOptions options, Binder originalBinder, bool diagnose, ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
{
if (_withTypeParametersBinder is not null && IsInsideNameof)
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 6, 2022

Choose a reason for hiding this comment

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

IsInsideNameof

The logic doesn't look correct now because IsInsideNameof can return true even when !NextRequired.InvocableNameofInScope() is false. #Closed


var tree = comp.SyntaxTrees.Single();
var parentModel = comp.GetSemanticModel(tree);
var localFuncPosition = tree.GetText().ToString().IndexOf("[My(a)]", StringComparison.Ordinal);
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 6, 2022

Choose a reason for hiding this comment

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

tree.GetText().ToString().IndexOf("[My(a)]", StringComparison.Ordinal);

I usually locate the syntax node or token and get its SpanStart rather than doing textual search in a string. #Closed

Copy link
Member Author

Choose a reason for hiding this comment

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

This pattern is also used in a number of places and is unambiguous. I'll leave as-is.

var methodPosition = tree.GetText().ToString().IndexOf("[My(b)]", StringComparison.Ordinal);

var attr = parseAttributeSyntax("[My(nameof(TParameter))]", TestOptions.Regular10);
VerifyTParameterSpeculation(parentModel, localFuncPosition, attr, found: false);
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 6, 2022

Choose a reason for hiding this comment

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

VerifyTParameterSpeculation(parentModel, localFuncPosition, attr, found: false);

I do not think this tests scenario 2B because the location is supposed to be inside an existing attribute. The location used here is not inside the existing attribute #Closed

Copy link
Member Author

Choose a reason for hiding this comment

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

I'd wrote down the following:

Scenario 2B:
TryGetSpeculativeSemanticModel with a new attribute

The reason for this is that the only TryGetSpeculativeSemanticModel overloads take statements, attributes, type syntaxes, etc, but I didn't find one that just takes a plain expressions.

Copy link
Contributor

Choose a reason for hiding this comment

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

The reason for this is that the only TryGetSpeculativeSemanticModel overloads take statements, attributes, type syntaxes, etc, but I didn't find one that just takes a plain expressions.

I am not following. I am talking about position where to speculate, not about with what syntax to speculate.

Copy link
Member Author

Choose a reason for hiding this comment

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

I see. We can try that, although it seems strange to speculate with an attribute inside an attribute.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't know. One might arrive to that by thinking: "I want to see what happens if I replace existing attribute application with a new one." And passes location related to the attribute about to be replaced.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added a test

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 4), tests are not looked at.

// One bit encodes whether this was computed, the other bit encodes a boolean value
private int _lazyIsInsideNameof;
private const byte _lazyIsInsideNameof_IsInitialized = 1 << 0;
private const byte _lazyIsInsideNameof_Value = 1 << 1;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 6, 2022

Choose a reason for hiding this comment

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

I think we already have ThreeState type that we use in cases like this #Closed

@jcouv jcouv requested a review from AlekseyTs April 6, 2022 17:10
@jcouv
Copy link
Member Author

jcouv commented Apr 6, 2022

@dotnet/roslyn-compiler for second review. Thanks

@RikkiGibson RikkiGibson self-assigned this Apr 6, 2022
Copy link
Member

@RikkiGibson RikkiGibson left a comment

Choose a reason for hiding this comment

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

Implementation LGTM with some minor comments, haven't looked at tests yet.


do
{
if (current is ContextualAttributeBinder contextualAttributeBinder)
Copy link
Member

@RikkiGibson RikkiGibson Apr 7, 2022

Choose a reason for hiding this comment

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

A similar traversal is also done in GetForwardedToAssembly in Binder_Symbols.cs. Should we introduce a virtual method on Binder for this? It's fine if the answer is "no", thought I would check either way. #Pending

/// To do so, it works together with <see cref="ContextualAttributeBinder"/>.
///
/// For other attributes (on types, type parameters or parameters) we use a WithTypeParameterBinder directly
/// in the binder chain and some filtering (<see cref="LookupOptions.MustNotBeMethodTypeParameter"/>) to keep
Copy link
Member

@RikkiGibson RikkiGibson Apr 7, 2022

Choose a reason for hiding this comment

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

Should we file a "Design Debt" issue to track replacing LookupOptions.MustNotBeMethodTypeParameter with a "keep the WithTypeParametersBinder on the side" strategy? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

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

No. I tried doing that last week, but we reached the conclusion that it would be too much of a breaking change. So we have to keep the current behavior.

/// Creates an AttributeSemanticModel that allows asking semantic questions about an attribute node.
/// </summary>
public static AttributeSemanticModel Create(SyntaxTreeSemanticModel containingSemanticModel, AttributeSyntax syntax, NamedTypeSymbol attributeType, AliasSymbol aliasOpt, Binder rootBinder, ImmutableDictionary<Symbol, Symbol> parentRemappedSymbolsOpt)
public static AttributeSemanticModel Create(SyntaxTreeSemanticModel containingSemanticModel, AttributeSyntax syntax, NamedTypeSymbol attributeType, AliasSymbol aliasOpt, Symbol attributeTarget, Binder rootBinder, ImmutableDictionary<Symbol, Symbol> parentRemappedSymbolsOpt)
Copy link
Member

@RikkiGibson RikkiGibson Apr 7, 2022

Choose a reason for hiding this comment

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

It looks like nullable annotations are enabled in this file. Should we use the type Symbol? attributeTarget here? #Pending

// body unless the position is either:
// a) in a type-only context inside an expression, or
// b) inside of an XML name attribute in an XML doc comment.
// b) inside of an XML name attribute in an XML doc comment,
Copy link
Member

@RikkiGibson RikkiGibson Apr 7, 2022

Choose a reason for hiding this comment

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

Just curious, did you compare the pattern you used here with whatever is used already for doc comments? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

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

For cref and such, BinderFactoryVisitor just uses a binder for a position inside the member declaration. Then no filter is used. See MakeCrefBinder for example.


internal override bool IsInsideNameof => IsNameofOperator || base.IsInsideNameof;

protected override SyntaxNode? EnclosingNameofArgument => IsNameofOperator ? _nameofArgument : base.EnclosingNameofArgument;
Copy link
Member

@RikkiGibson RikkiGibson Apr 7, 2022

Choose a reason for hiding this comment

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

It looks like this implementation might be important in a nested scenario of some kind? Basically, I wasn't sure if it matters that we delegate to the base here when this is not a nameof operator, versus just returning null when this is not a nameof operator. #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, a nameof inside a nameof.


Possible workarounds are:

1. Rename the type parameter to avoid shadowing the name from outer scope.
Copy link
Member

Choose a reason for hiding this comment

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

2 nits:

  1. Constant is not accessible from the attribute, perhaps it should be made public to avoid confusion.
  2. Qualifying the name of the constant also seems like an option here. e.g. [MyAttribute(nameof(C.TParameter.Constant))].

@jcouv
Copy link
Member Author

jcouv commented Apr 7, 2022

@AlekseyTs @RikkiGibson Please take another look. Thanks

@AlekseyTs
Copy link
Contributor

@jcouv It looks like you need to merge main into the feature branch in order to overcome the Test_Windows_Desktop_Spanish_Release_64 failure.


binder = binder.Next;
}
while (binder != null);
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 7, 2022

Choose a reason for hiding this comment

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

while (binder != null);

Would it be worth adding an assert after the loop? It looks like we expect to never reach that point. #Closed

parentModel = comp.GetSemanticModel(tree);

VerifyTParameterSpeculation(parentModel, localFuncPosition, attr, found: false);
VerifyTParameterSpeculation(parentModel, methodPosition, attr, found: false);
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 7, 2022

Choose a reason for hiding this comment

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

VerifyTParameterSpeculation

I'd like to better understand why we are failing to locate the type parameter here. Perhaps we can chat about this offline #Closed

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 7). Other than the question about test behavior, LGTM

@jcouv jcouv requested a review from AlekseyTs April 7, 2022 20:07
";
// C# 10
var comp = CreateCompilation(source, parseOptions: TestOptions.Regular10);
//comp.VerifyDiagnostics(
Copy link
Contributor

Choose a reason for hiding this comment

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

//comp.VerifyDiagnostics(

Consider removing or uncommenting.

Copy link
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

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

LGTM (commit 8)

@jcouv jcouv enabled auto-merge (squash) April 8, 2022 03:21
@jcouv jcouv merged commit 8fa902f into dotnet:features/nameof-parameter Apr 8, 2022
@jcouv jcouv deleted the nameof-scope branch April 8, 2022 03:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants