Extend scope of type parameters in attributes#40525
Extend scope of type parameters in attributes#40525jcouv merged 10 commits intodotnet:features/nameof-parameterfrom
Conversation
6bd8e4e to
5076a55
Compare
5ab7314 to
14578af
Compare
fb7b673 to
fd18833
Compare
| { | ||
| if ((_lazyIsInsideNameof & _lazyIsInsideNameof_IsInitialized) == 0) | ||
| { | ||
| bool isInsideNameof = !NextRequired.InvocableNameofInScope() || base.IsInsideNameof; |
There was a problem hiding this comment.
!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
There was a problem hiding this comment.
Thanks for the clarification. Makes sense
| } | ||
|
|
||
| protected override SyntaxNode EnclosingNameofArgument => _nameofArgument; | ||
| protected override SyntaxNode? EnclosingNameofArgument => IsInsideNameof ? _nameofArgument : base.EnclosingNameofArgument; |
|
|
||
| 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) |
|
|
||
| var tree = comp.SyntaxTrees.Single(); | ||
| var parentModel = comp.GetSemanticModel(tree); | ||
| var localFuncPosition = tree.GetText().ToString().IndexOf("[My(a)]", StringComparison.Ordinal); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I see. We can try that, although it seems strange to speculate with an attribute inside an attribute.
There was a problem hiding this comment.
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.
|
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; |
There was a problem hiding this comment.
I think we already have ThreeState type that we use in cases like this #Closed
|
@dotnet/roslyn-compiler for second review. Thanks |
RikkiGibson
left a comment
There was a problem hiding this comment.
Implementation LGTM with some minor comments, haven't looked at tests yet.
|
|
||
| do | ||
| { | ||
| if (current is ContextualAttributeBinder contextualAttributeBinder) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Should we file a "Design Debt" issue to track replacing LookupOptions.MustNotBeMethodTypeParameter with a "keep the WithTypeParametersBinder on the side" strategy? #Resolved
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
Just curious, did you compare the pattern you used here with whatever is used already for doc comments? #Resolved
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Yes, a nameof inside a nameof.
|
|
||
| Possible workarounds are: | ||
|
|
||
| 1. Rename the type parameter to avoid shadowing the name from outer scope. |
There was a problem hiding this comment.
2 nits:
Constantis not accessible from the attribute, perhaps it should be madepublicto avoid confusion.- Qualifying the name of the constant also seems like an option here. e.g.
[MyAttribute(nameof(C.TParameter.Constant))].
|
@AlekseyTs @RikkiGibson Please take another look. Thanks |
|
@jcouv It looks like you need to merge |
|
|
||
| binder = binder.Next; | ||
| } | ||
| while (binder != null); |
| parentModel = comp.GetSemanticModel(tree); | ||
|
|
||
| VerifyTParameterSpeculation(parentModel, localFuncPosition, attr, found: false); | ||
| VerifyTParameterSpeculation(parentModel, methodPosition, attr, found: false); |
|
Done with review pass (commit 7). Other than the question about test behavior, LGTM |
| "; | ||
| // C# 10 | ||
| var comp = CreateCompilation(source, parseOptions: TestOptions.Regular10); | ||
| //comp.VerifyDiagnostics( |
Also fixes the type parameter scoping on records (fixes #60379)
Fixes #60194
This PR adds type parameters inside
nameofin 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= Twill 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.VisitInvocationExpressionadds aNameofBinderfor some invocation expressions (that pass syntactic checkMayBeNameofOperator).NameofBinderis a no-op when it detects a proper "nameof" method in its enclosing scope.When the
NameofBinderis chained on aContextualAttributeBinder, it pulls in additional identifiers in scope from type parameters on attribute target (given byContextualAttributeBinder). 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.TryBindNameofOperatorgets a binder from binder factories, so that the above scopes kick in.SyntaxTreeSemanticModel.CreateModelForAttributeadds aContextualAttributebinder (with proper attribute target) in theRootBinderfor theAttributeSemanticModelit creates.MemberSemanticModel.GetEnclosingBinderInternalWithinRootadds aNameofBinderon some invocation expressions.BinderFactoryVisitor.VisitTypeDeclarationCoreandSyntaxTreeSemanticModel.GetDeclarationNamerelate to records.Relates to #40524 (test plan)