Skip to content

Commit

Permalink
Add parameters in scope in nameof in certain attributes (#60642)
Browse files Browse the repository at this point in the history
  • Loading branch information
jcouv authored Apr 15, 2022
1 parent 8fa902f commit 1270eeb
Show file tree
Hide file tree
Showing 7 changed files with 1,374 additions and 201 deletions.
26 changes: 23 additions & 3 deletions docs/compilers/CSharp/Compiler Breaking Changes - DotNet 7.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
***Introduced in .NET SDK 7.0.400, Visual Studio 2022 version 17.3.***

When the language version is C# 11 or later, a `nameof` operator in an attribute on a method
brings the type parameters of that method in scope. The same applies for local functions.
brings the type parameters of that method in scope. The same applies for local functions.
A `nameof` operator in an attribute on a method, its type parameters or parameters brings
the parameters of that method in scope. The same applies to local functions, lambdas,
delegates and indexers.

For instance, this will now be an error:
For instance, these will now be errors:
```csharp
class C
{
Expand All @@ -20,11 +23,28 @@ class C
}
```
```csharp
class C
{
class parameter
{
internal const string Constant = """";
}
[MyAttribute(nameof(parameter.Constant))]
void M(int parameter) { }
}
```

Possible workarounds are:

1. Rename the type parameter to avoid shadowing the name from outer scope.
1. Rename the type parameter or parameter to avoid shadowing the name from outer scope.
1. Use a string literal instead of the `nameof` operator.
1. Downgrade the `<LangVersion>` element to 9.0 or earlier.

Note: The break will also apply to C# 10 and earlier when .NET 7 ships, but is
currently scoped down to users of LangVer=preview.
Tracked by https://github.com/dotnet/roslyn/issues/60640

## UTF8 String Literal conversion

***Introduced in .NET SDK 6.0.400, Visual Studio 2022 version 17.3.***
Expand Down
93 changes: 87 additions & 6 deletions src/Compilers/CSharp/Portable/Binder/LocalBinderFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,18 +199,31 @@ public override void VisitOperatorDeclaration(OperatorDeclarationSyntax node)
Visit(node.ExpressionBody);
}

#nullable enable
public override void VisitInvocationExpression(InvocationExpressionSyntax node)
{
if (node.MayBeNameofOperator())
{
var oldEnclosing = _enclosing;
WithTypeParametersBinder withTypeParametersBinder = ((_enclosing.Flags & BinderFlags.InContextualAttributeBinder) != 0) &&
_enclosing.Compilation.IsFeatureEnabled(MessageID.IDS_FeatureExtendedNameofScope)
? getExtraWithTypeParametersBinder(_enclosing, getAttributeTarget(_enclosing))
: null;

WithTypeParametersBinder? withTypeParametersBinder;
WithParametersBinder? withParametersBinder;
// The LangVer check will be removed before shipping .NET 7.
// Tracked by https://github.com/dotnet/roslyn/issues/60640
if (((_enclosing.Flags & BinderFlags.InContextualAttributeBinder) != 0) && _enclosing.Compilation.IsFeatureEnabled(MessageID.IDS_FeatureExtendedNameofScope))
{
var attributeTarget = getAttributeTarget(_enclosing);
withTypeParametersBinder = getExtraWithTypeParametersBinder(_enclosing, attributeTarget);
withParametersBinder = getExtraWithParametersBinder(_enclosing, attributeTarget);
}
else
{
withTypeParametersBinder = null;
withParametersBinder = null;
}

var argumentExpression = node.ArgumentList.Arguments[0].Expression;
var possibleNameofBinder = new NameofBinder(argumentExpression, _enclosing, withTypeParametersBinder);
var possibleNameofBinder = new NameofBinder(argumentExpression, _enclosing, withTypeParametersBinder, withParametersBinder);
AddToMap(node, possibleNameofBinder);

_enclosing = possibleNameofBinder;
Expand All @@ -231,9 +244,77 @@ static Symbol getAttributeTarget(Binder current)
return contextualAttributeBinder.AttributeTarget;
}

static WithTypeParametersBinder getExtraWithTypeParametersBinder(Binder enclosing, Symbol target)
static WithTypeParametersBinder? getExtraWithTypeParametersBinder(Binder enclosing, Symbol target)
=> target.Kind == SymbolKind.Method ? new WithMethodTypeParametersBinder((MethodSymbol)target, enclosing) : null;

// We're bringing parameters in scope inside `nameof` in attributes on methods, their type parameters and parameters.
// This also applies to local functions, lambdas, indexers and delegates.
static WithParametersBinder? getExtraWithParametersBinder(Binder enclosing, Symbol target)
{
var parameters = target switch
{
SourcePropertyAccessorSymbol { MethodKind: MethodKind.PropertySet } setter => getSetterParameters(setter),
MethodSymbol methodSymbol => methodSymbol.Parameters,
ParameterSymbol parameter => getAllParameters(parameter),
TypeParameterSymbol typeParameter => getMethodParametersFromTypeParameter(typeParameter),
PropertySymbol property => property.Parameters,
NamedTypeSymbol namedType when namedType.IsDelegateType() => getDelegateParameters(namedType),
_ => default
};

return parameters.IsDefaultOrEmpty
? null
: new WithParametersBinder(parameters, enclosing);
}

static ImmutableArray<ParameterSymbol> getAllParameters(ParameterSymbol parameter)
{
switch (parameter.ContainingSymbol)
{
case MethodSymbol method:
return method.Parameters;
case PropertySymbol property:
return property.Parameters;
default:
Debug.Assert(false);
return default;
}
}

static ImmutableArray<ParameterSymbol> getMethodParametersFromTypeParameter(TypeParameterSymbol typeParameter)
{
switch (typeParameter.ContainingSymbol)
{
case MethodSymbol method:
return method.Parameters;
case NamedTypeSymbol namedType when namedType.IsDelegateType():
return getDelegateParameters(namedType);
default:
Debug.Assert(false);
return default;
}
}

static ImmutableArray<ParameterSymbol> getDelegateParameters(NamedTypeSymbol delegateType)
{
Debug.Assert(delegateType.IsDelegateType());
if (delegateType.DelegateInvokeMethod is { } invokeMethod)
{
return invokeMethod.Parameters;
}

Debug.Assert(false);
return default;
}

static ImmutableArray<ParameterSymbol> getSetterParameters(SourcePropertyAccessorSymbol setter)
{
var parameters = setter.Parameters;
Debug.Assert(parameters[^1] is SynthesizedAccessorValueParameterSymbol);
return parameters.RemoveAt(parameters.Length - 1);
}
}
#nullable disable

public override void VisitSimpleLambdaExpression(SimpleLambdaExpressionSyntax node)
{
Expand Down
34 changes: 32 additions & 2 deletions src/Compilers/CSharp/Portable/Binder/NameofBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ internal sealed class NameofBinder : Binder
{
private readonly SyntaxNode _nameofArgument;
private readonly WithTypeParametersBinder? _withTypeParametersBinder;
private readonly WithParametersBinder? _withParametersBinder;
private ThreeState _lazyIsNameofOperator;

internal NameofBinder(SyntaxNode nameofArgument, Binder next, WithTypeParametersBinder? withTypeParametersBinder)
internal NameofBinder(SyntaxNode nameofArgument, Binder next, WithTypeParametersBinder? withTypeParametersBinder, WithParametersBinder? withParametersBinder)
: base(next)
{
_nameofArgument = nameofArgument;
_withTypeParametersBinder = withTypeParametersBinder;
_withParametersBinder = withParametersBinder;
}

private bool IsNameofOperator
Expand All @@ -49,14 +51,42 @@ private bool IsNameofOperator

internal override void LookupSymbolsInSingleBinder(LookupResult result, string name, int arity, ConsList<TypeSymbol> basesBeingResolved, LookupOptions options, Binder originalBinder, bool diagnose, ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
{
bool foundParameter = false;
if (_withParametersBinder is not null && IsNameofOperator)
{
_withParametersBinder.LookupSymbolsInSingleBinder(result, name, arity, basesBeingResolved, options, originalBinder, diagnose, ref useSiteInfo);
if (!result.IsClear)
{
if (result.IsMultiViable)
{
return;
}

foundParameter = true;
}
}

if (_withTypeParametersBinder is not null && IsNameofOperator)
{
_withTypeParametersBinder.LookupSymbolsInSingleBinder(result, name, arity, basesBeingResolved, options, originalBinder, diagnose, ref useSiteInfo);
if (foundParameter)
{
var tmp = LookupResult.GetInstance();
_withTypeParametersBinder.LookupSymbolsInSingleBinder(tmp, name, arity, basesBeingResolved, options, originalBinder, diagnose, ref useSiteInfo);
result.MergeEqual(tmp);
}
else
{
_withTypeParametersBinder.LookupSymbolsInSingleBinder(result, name, arity, basesBeingResolved, options, originalBinder, diagnose, ref useSiteInfo);
}
}
}

internal override void AddLookupSymbolsInfoInSingleBinder(LookupSymbolsInfo info, LookupOptions options, Binder originalBinder)
{
if (_withParametersBinder is not null && IsNameofOperator)
{
_withParametersBinder.AddLookupSymbolsInfoInSingleBinder(info, options, originalBinder);
}
if (_withTypeParametersBinder is not null && IsNameofOperator)
{
_withTypeParametersBinder.AddLookupSymbolsInfoInSingleBinder(info, options, originalBinder);
Expand Down
3 changes: 2 additions & 1 deletion src/Compilers/CSharp/Portable/Binder/WithParametersBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ namespace Microsoft.CodeAnalysis.CSharp
{
/// <summary>
/// Binder used to place the parameters of a method, property, indexer, or delegate
/// in scope when binding &lt;param&gt; tags inside of XML documentation comments.
/// in scope when binding &lt;param&gt; tags inside of XML documentation comments
/// and `nameof` in certain attribute positions.
/// </summary>
internal sealed class WithParametersBinder : Binder
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1274,7 +1274,7 @@ private AttributeSemanticModel CreateModelForAttribute(Binder enclosingBinder, A
var attributeType = (NamedTypeSymbol)enclosingBinder.BindType(attribute.Name, BindingDiagnosticBag.Discarded, out aliasOpt).Type;

// For attributes where a nameof could introduce some type parameters, we need to track the attribute target
Symbol attributeTarget = GetAttributeTargetForExtraTypeParameters(attribute.Parent.Parent);
Symbol attributeTarget = getAttributeTarget(attribute.Parent.Parent);

return AttributeSemanticModel.Create(
this,
Expand All @@ -1284,16 +1284,22 @@ private AttributeSemanticModel CreateModelForAttribute(Binder enclosingBinder, A
attributeTarget,
enclosingBinder.WithAdditionalFlags(BinderFlags.AttributeArgument),
containingModel?.GetRemappedSymbols());
}

Symbol GetAttributeTargetForExtraTypeParameters(SyntaxNode targetSyntax)
{
return targetSyntax switch
Symbol getAttributeTarget(SyntaxNode targetSyntax)
{
MethodDeclarationSyntax methodDeclaration => GetDeclaredMemberSymbol(methodDeclaration),
LocalFunctionStatementSyntax localFunction => GetMemberModel(localFunction)?.GetDeclaredLocalFunction(localFunction),
_ => null
};
return targetSyntax switch
{
BaseMethodDeclarationSyntax methodDeclaration => GetDeclaredMemberSymbol(methodDeclaration),
LocalFunctionStatementSyntax localFunction => GetMemberModel(localFunction)?.GetDeclaredLocalFunction(localFunction),
ParameterSyntax parameterSyntax => ((Symbols.PublicModel.ParameterSymbol)GetDeclaredSymbol(parameterSyntax)).UnderlyingSymbol,
TypeParameterSyntax typeParameterSyntax => ((Symbols.PublicModel.TypeParameterSymbol)GetDeclaredSymbol(typeParameterSyntax)).UnderlyingSymbol,
IndexerDeclarationSyntax indexerSyntax => ((Symbols.PublicModel.PropertySymbol)GetDeclaredSymbol(indexerSyntax)).UnderlyingSymbol,
AccessorDeclarationSyntax accessorSyntax => ((Symbols.PublicModel.MethodSymbol)GetDeclaredSymbol(accessorSyntax)).UnderlyingSymbol,
AnonymousFunctionExpressionSyntax anonymousFunction => ((Symbols.PublicModel.Symbol)GetSymbolInfo(anonymousFunction).Symbol).UnderlyingSymbol,
DelegateDeclarationSyntax delegateSyntax => ((Symbols.PublicModel.NamedTypeSymbol)GetDeclaredSymbol(delegateSyntax)).UnderlyingSymbol,
_ => null
};
}
}

private FieldSymbol GetDeclaredFieldSymbol(VariableDeclaratorSyntax variableDecl)
Expand Down
68 changes: 0 additions & 68 deletions src/Compilers/CSharp/Test/Semantic/Semantics/LambdaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6310,42 +6310,6 @@ class A : Attribute { }
);
}

[Fact]
public void ParameterScope_NotInMethodAttributeNameOf()
{
var comp = CreateCompilation(@"
class C
{
void M()
{
var _ =
[My(nameof(parameter))] // 1
void(int parameter) => { };
}
[My(nameof(parameter))] // 2
void M2(int parameter) { }
}
public class MyAttribute : System.Attribute
{
public MyAttribute(string name1) { }
}
");
comp.VerifyDiagnostics(
// (8,24): error CS0103: The name 'parameter' does not exist in the current context
// [My(nameof(parameter))] // 1
Diagnostic(ErrorCode.ERR_NameNotInContext, "parameter").WithArguments("parameter").WithLocation(8, 24),
// (12,16): error CS0103: The name 'parameter' does not exist in the current context
// [My(nameof(parameter))] // 2
Diagnostic(ErrorCode.ERR_NameNotInContext, "parameter").WithArguments("parameter").WithLocation(12, 16)
);

VerifyParameter(comp, 0);
VerifyParameter(comp, 1);
}

/// <summary>
/// Look for usages of "parameter" and verify the index-th one.
/// </summary>
Expand Down Expand Up @@ -6538,38 +6502,6 @@ public MyAttribute(string name1) { }
VerifyParameter(comp, 1);
}

[Fact]
public void ParameterScope_NotInParameterAttributeNameOf()
{
var comp = CreateCompilation(@"
class C
{
void M()
{
var _ = void ([My(nameof(parameter))] int parameter) => throw null;
}
void M2([My(nameof(parameter))] int parameter) => throw null;
}
public class MyAttribute : System.Attribute
{
public MyAttribute(string name1) { }
}
");
comp.VerifyDiagnostics(
// (6,34): error CS0103: The name 'parameter' does not exist in the current context
// var _ = void ([My(nameof(parameter))] int parameter) => throw null;
Diagnostic(ErrorCode.ERR_NameNotInContext, "parameter").WithArguments("parameter").WithLocation(6, 34),
// (9,24): error CS0103: The name 'parameter' does not exist in the current context
// void M2([My(nameof(parameter))] int parameter) => throw null;
Diagnostic(ErrorCode.ERR_NameNotInContext, "parameter").WithArguments("parameter").WithLocation(9, 24)
);

VerifyParameter(comp, 0);
VerifyParameter(comp, 1);
}

[Fact]
public void ParameterScope_InParameterDefaultValueNameOf()
{
Expand Down
Loading

0 comments on commit 1270eeb

Please sign in to comment.