-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Handle CallerArgumentExpression in attributes #53535
Changes from 7 commits
2b111de
91d65f2
c5c8b12
2a6ceb7
a434aa5
446bfd1
d945e3c
0e19e35
0bce268
413c81a
68c8078
146b609
6131eb4
c05caed
4b3d333
a8ba7d1
d103a43
2194bc9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -241,7 +241,7 @@ private CSharpAttributeData GetAttribute(BoundAttribute boundAttribute, BindingD | |||||
else | ||||||
{ | ||||||
constructorArguments = GetRewrittenAttributeConstructorArguments(out constructorArgumentsSourceIndices, attributeConstructor, | ||||||
constructorArgsArray, boundAttribute.ConstructorArgumentNamesOpt, (AttributeSyntax)boundAttribute.Syntax, diagnostics, ref hasErrors); | ||||||
constructorArgsArray, boundAttribute.ConstructorArgumentNamesOpt, (AttributeSyntax)boundAttribute.Syntax, boundAttribute.ConstructorArgumentsToParamsOpt, diagnostics, ref hasErrors); | ||||||
} | ||||||
|
||||||
CompoundUseSiteInfo<AssemblySymbol> useSiteInfo = GetNewCompoundUseSiteInfo(diagnostics); | ||||||
|
@@ -593,6 +593,7 @@ private ImmutableArray<TypedConstant> GetRewrittenAttributeConstructorArguments( | |||||
ImmutableArray<TypedConstant> constructorArgsArray, | ||||||
ImmutableArray<string> constructorArgumentNamesOpt, | ||||||
AttributeSyntax syntax, | ||||||
ImmutableArray<int> argumentsToParams, | ||||||
BindingDiagnosticBag diagnostics, | ||||||
ref bool hasErrors) | ||||||
{ | ||||||
|
@@ -663,15 +664,15 @@ private ImmutableArray<TypedConstant> GetRewrittenAttributeConstructorArguments( | |||||
// consumed a named argument. For the latter case, argsConsumedCount stays same. | ||||||
int matchingArgumentIndex; | ||||||
reorderedArgument = GetMatchingNamedOrOptionalConstructorArgument(out matchingArgumentIndex, constructorArgsArray, | ||||||
constructorArgumentNamesOpt, parameter, firstNamedArgIndex, argumentsCount, ref argsConsumedCount, syntax, diagnostics); | ||||||
constructorArgumentNamesOpt, parameter, firstNamedArgIndex, argumentsCount, ref argsConsumedCount, syntax, argumentsToParams, diagnostics); | ||||||
|
||||||
sourceIndices = sourceIndices ?? CreateSourceIndicesArray(i, parameterCount); | ||||||
sourceIndices[i] = matchingArgumentIndex; | ||||||
} | ||||||
} | ||||||
else | ||||||
{ | ||||||
reorderedArgument = GetDefaultValueArgument(parameter, syntax, diagnostics); | ||||||
reorderedArgument = GetDefaultValueArgument(parameter, syntax, argumentsToParams, diagnostics); | ||||||
sourceIndices = sourceIndices ?? CreateSourceIndicesArray(i, parameterCount); | ||||||
} | ||||||
|
||||||
|
@@ -727,6 +728,7 @@ private TypedConstant GetMatchingNamedOrOptionalConstructorArgument( | |||||
int argumentsCount, | ||||||
ref int argsConsumedCount, | ||||||
AttributeSyntax syntax, | ||||||
ImmutableArray<int> argumentsToParams, | ||||||
BindingDiagnosticBag diagnostics) | ||||||
{ | ||||||
int index = GetMatchingNamedConstructorArgumentIndex(parameter.Name, constructorArgumentNamesOpt, startIndex, argumentsCount); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
|
@@ -744,7 +746,7 @@ private TypedConstant GetMatchingNamedOrOptionalConstructorArgument( | |||||
else | ||||||
{ | ||||||
matchingArgumentIndex = -1; | ||||||
return GetDefaultValueArgument(parameter, syntax, diagnostics); | ||||||
return GetDefaultValueArgument(parameter, syntax, argumentsToParams, diagnostics); | ||||||
} | ||||||
} | ||||||
|
||||||
|
@@ -775,7 +777,7 @@ private static int GetMatchingNamedConstructorArgumentIndex(string parameterName | |||||
return argIndex; | ||||||
} | ||||||
|
||||||
private TypedConstant GetDefaultValueArgument(ParameterSymbol parameter, AttributeSyntax syntax, BindingDiagnosticBag diagnostics) | ||||||
private TypedConstant GetDefaultValueArgument(ParameterSymbol parameter, AttributeSyntax syntax, ImmutableArray<int> argumentsToParams, BindingDiagnosticBag diagnostics) | ||||||
{ | ||||||
var parameterType = parameter.Type; | ||||||
ConstantValue? defaultConstantValue = parameter.IsOptional ? parameter.ExplicitDefaultConstantValue : ConstantValue.NotAvailable; | ||||||
|
@@ -819,6 +821,14 @@ private TypedConstant GetDefaultValueArgument(ParameterSymbol parameter, Attribu | |||||
kind = TypedConstantKind.Primitive; | ||||||
defaultValue = ((ContextualAttributeBinder)this).AttributedMember.GetMemberCallerName(); | ||||||
} | ||||||
else if (!IsEarlyAttributeBinder && syntax.ArgumentList is not null && | ||||||
getCallerArgumentArgumentIndex(parameter, argumentsToParams) is int argumentIndex && argumentIndex > -1 && argumentIndex < syntax.ArgumentList.Arguments.Count) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Indeed, but it's probably that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @AlekseyTs Fixed. Thanks! |
||||||
{ | ||||||
CheckFeatureAvailability(syntax.ArgumentList, MessageID.IDS_FeatureCallerArgumentExpression, diagnostics); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
parameterType = Compilation.GetSpecialType(SpecialType.System_String); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I followed the same pattern used by other caller member info above. But I'll look into that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @AlekseyTs It's not guaranteed to be a string. It can be an object, and in such case if we pulled it from the ParameterSymbol, emit will fail with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In this case, I think we shouldn't be asking for the type this way. Binder should have a helper that is going to report an error if the requested type is missing or otherwise bad. I think we should use that helper here instead. Other similar places in this function should probably be adjusted in the same fashion as well. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @AlekseyTs The diagnostic is reported elsewhere. This is not done in binder so that the error is still produced even if there is no usage of the method having the argument expression parameter (i.e, the diagnostic is produced at the declaration, not the call site). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think we are talking about different diagnostics. I am talking about "bad" string type. It might not be part of the signature. The error is not about missing conversion. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @AlekseyTs I'm not sure I understand this (sorry for that). Does this suggest to use the following helper?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes. Binder.GetSpecialType helper that takes diagnostics |
||||||
kind = TypedConstantKind.Primitive; | ||||||
defaultValue = syntax.ArgumentList.Arguments[argumentIndex].Expression.ToString(); | ||||||
} | ||||||
else if (defaultConstantValue == ConstantValue.NotAvailable) | ||||||
{ | ||||||
// There is no constant value given for the parameter in source/metadata. | ||||||
|
@@ -874,6 +884,13 @@ private TypedConstant GetDefaultValueArgument(ParameterSymbol parameter, Attribu | |||||
{ | ||||||
return new TypedConstant(parameterType, kind, defaultValue); | ||||||
} | ||||||
|
||||||
static int getCallerArgumentArgumentIndex(ParameterSymbol parameter, ImmutableArray<int> argumentsToParams) | ||||||
{ | ||||||
return argumentsToParams.IsDefault | ||||||
? parameter.CallerArgumentExpressionParameterIndex | ||||||
: argumentsToParams.IndexOf(parameter.CallerArgumentExpressionParameterIndex); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suppose There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @AlekseyTs I'm not sure what exactly we want to test here. Extra argument or unmatched named argument would produce an error elsewhere, so I assume such a test shouldn't be affected by the logic here? Please correct me. I'm certainly open to add more tests, but I'm not getting which scenario I should try to hit in the test. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'll try to explain why I am asking for more testing here. The logic here as it stands right now feels fragile to me. In particular, if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @AlekseyTs Thanks a ton! Yes I wrote the code under the assumption that the array won't contain -1. |
||||||
} | ||||||
} | ||||||
|
||||||
private static TypedConstant GetParamArrayArgument(ParameterSymbol parameter, ImmutableArray<TypedConstant> constructorArgsArray, | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -414,6 +414,220 @@ public static void M( | |
value"); | ||
} | ||
|
||
[ConditionalFact(typeof(CoreClrOnly))] | ||
public void TestArgumentExpressionInAttributeConstructor() | ||
{ | ||
string source = @" | ||
using System; | ||
using System.Reflection; | ||
using System.Runtime.CompilerServices; | ||
|
||
public class MyAttribute : Attribute | ||
{ | ||
public MyAttribute(string s, [CallerArgumentExpression(""s"")] string x = """") => Console.WriteLine($""'{s}', '{x}'""); | ||
} | ||
|
||
[My(""Hello"")] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider also testing what do we get from GetAttributes API on the calss. #Closed |
||
public class Program | ||
{ | ||
static void Main() | ||
{ | ||
typeof(Program).GetCustomAttribute(typeof(MyAttribute)); | ||
} | ||
} | ||
"; | ||
|
||
var compilation = CreateCompilation(source, targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularPreview); | ||
compilation.VerifyDiagnostics(); | ||
CompileAndVerify(compilation, expectedOutput: "'Hello', '\"Hello\"'"); | ||
var namedType = compilation.GetTypeByMetadataName("Program").GetPublicSymbol(); | ||
var attributeArguments = namedType.GetAttributes().Single().ConstructorArguments; | ||
Assert.Equal(2, attributeArguments.Length); | ||
Assert.Equal("Hello", attributeArguments[0].Value); | ||
Assert.Equal("\"Hello\"", attributeArguments[1].Value); | ||
} | ||
|
||
[ConditionalFact(typeof(CoreClrOnly))] | ||
public void TestArgumentExpressionInAttributeConstructor_NamedArgument() | ||
{ | ||
string source = @" | ||
using System; | ||
using System.Reflection; | ||
using System.Runtime.CompilerServices; | ||
|
||
public class MyAttribute : Attribute | ||
{ | ||
public MyAttribute(string s, [CallerArgumentExpression(""s"")] string x = """") => Console.WriteLine($""'{s}', '{x}'""); | ||
} | ||
|
||
[My(s:""Hello"")] | ||
public class Program | ||
{ | ||
static void Main() | ||
{ | ||
typeof(Program).GetCustomAttribute(typeof(MyAttribute)); | ||
} | ||
} | ||
"; | ||
|
||
var compilation = CreateCompilation(source, targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularPreview); | ||
compilation.VerifyDiagnostics(); | ||
CompileAndVerify(compilation, expectedOutput: "'Hello', '\"Hello\"'"); | ||
var namedType = compilation.GetTypeByMetadataName("Program").GetPublicSymbol(); | ||
var attributeArguments = namedType.GetAttributes().Single().ConstructorArguments; | ||
Assert.Equal(2, attributeArguments.Length); | ||
Assert.Equal("Hello", attributeArguments[0].Value); | ||
Assert.Equal("\"Hello\"", attributeArguments[1].Value); | ||
} | ||
|
||
[ConditionalFact(typeof(CoreClrOnly))] | ||
public void TestArgumentExpressionInAttributeConstructor_NamedArgumentsSameOrder() | ||
{ | ||
string source = @" | ||
using System; | ||
using System.Reflection; | ||
using System.Runtime.CompilerServices; | ||
|
||
public class MyAttribute : Attribute | ||
{ | ||
public MyAttribute(string s, string s2, [CallerArgumentExpression(""s"")] string x = """") => Console.WriteLine($""'{s}', '{s2}', '{x}'""); | ||
} | ||
|
||
[My(s:""Hello"", s2:""World"")] | ||
public class Program | ||
{ | ||
static void Main() | ||
{ | ||
typeof(Program).GetCustomAttribute(typeof(MyAttribute)); | ||
} | ||
} | ||
"; | ||
|
||
var compilation = CreateCompilation(source, targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularPreview); | ||
compilation.VerifyDiagnostics(); | ||
CompileAndVerify(compilation, expectedOutput: "'Hello', 'World', '\"Hello\"'"); | ||
var namedType = compilation.GetTypeByMetadataName("Program").GetPublicSymbol(); | ||
var attributeArguments = namedType.GetAttributes().Single().ConstructorArguments; | ||
Assert.Equal(3, attributeArguments.Length); | ||
Assert.Equal("Hello", attributeArguments[0].Value); | ||
Assert.Equal("World", attributeArguments[1].Value); | ||
Assert.Equal("\"Hello\"", attributeArguments[2].Value); | ||
} | ||
|
||
[ConditionalFact(typeof(CoreClrOnly))] | ||
public void TestArgumentExpressionInAttributeConstructor_NamedArgumentsOutOfOrder() | ||
{ | ||
string source = @" | ||
using System; | ||
using System.Reflection; | ||
using System.Runtime.CompilerServices; | ||
|
||
public class MyAttribute : Attribute | ||
{ | ||
public MyAttribute(string s, string s2, [CallerArgumentExpression(""s"")] string x = """") => Console.WriteLine($""'{s}', '{s2}', '{x}'""); | ||
} | ||
|
||
[My(s2:""World"", s:""Hello"")] | ||
public class Program | ||
{ | ||
static void Main() | ||
{ | ||
typeof(Program).GetCustomAttribute(typeof(MyAttribute)); | ||
} | ||
} | ||
"; | ||
|
||
var compilation = CreateCompilation(source, targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularPreview); | ||
compilation.VerifyDiagnostics(); | ||
CompileAndVerify(compilation, expectedOutput: "'Hello', 'World', '\"Hello\"'"); | ||
var namedType = compilation.GetTypeByMetadataName("Program").GetPublicSymbol(); | ||
var attributeArguments = namedType.GetAttributes().Single().ConstructorArguments; | ||
Assert.Equal(3, attributeArguments.Length); | ||
Assert.Equal("Hello", attributeArguments[0].Value); | ||
Assert.Equal("World", attributeArguments[1].Value); | ||
Assert.Equal("\"Hello\"", attributeArguments[2].Value); | ||
} | ||
|
||
[ConditionalFact(typeof(CoreClrOnly))] | ||
public void TestArgumentExpressionInAttributeConstructor_Complex() | ||
{ | ||
string source = @" | ||
using System; | ||
using System.Runtime.CompilerServices; | ||
|
||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] | ||
public class MyAttribute : Attribute | ||
{ | ||
public MyAttribute([CallerArgumentExpression(""param2"")] string param1 = ""param1_default"", [CallerArgumentExpression(""param1"")] string param2 = ""param2_default"") => Console.WriteLine($""param1: {param1}, param2: {param2}""); | ||
} | ||
|
||
[My] | ||
[My()] | ||
[My(""param1_value"")] | ||
[My(param1: ""param1_value"")] | ||
[My(param2: ""param2_value"")] | ||
[My(param1: ""param1_value"", param2: ""param2_value"")] | ||
[My(param2: ""param2_value"", param1: ""param1_value"")] | ||
public class Program | ||
{ | ||
static void Main() | ||
{ | ||
typeof(Program).GetCustomAttributes(typeof(MyAttribute), false); | ||
} | ||
} | ||
"; | ||
|
||
var compilation = CreateCompilation(source, targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularPreview); | ||
compilation.VerifyDiagnostics(); | ||
CompileAndVerify(compilation, expectedOutput: | ||
@"param1: param1_default, param2: param2_default | ||
param1: param1_default, param2: param2_default | ||
param1: param1_value, param2: ""param1_value"" | ||
param1: param1_value, param2: ""param1_value"" | ||
param1: ""param2_value"", param2: param2_value | ||
param1: param1_value, param2: param2_value | ||
param1: param1_value, param2: param2_value"); | ||
} | ||
|
||
[ConditionalFact(typeof(CoreClrOnly))] | ||
public void TestArgumentExpressionInAttributeConstructor_NamedAndOptionalParameters() | ||
{ | ||
string source = @" | ||
using System; | ||
using System.Reflection; | ||
using System.Runtime.CompilerServices; | ||
|
||
class MyAttribute : Attribute | ||
{ | ||
public MyAttribute(int a = 1, int b = 2, int c = 3, [CallerArgumentExpression(""a"")] string expr_a = """", [CallerArgumentExpression(""b"")] string expr_b = """", [CallerArgumentExpression(""c"")] string expr_c = """") | ||
{ | ||
Console.WriteLine($""'{a}', '{b}', '{c}', '{expr_a}', '{expr_b}', '{expr_c}'""); | ||
A = a; | ||
B = b; | ||
C = c; | ||
} | ||
|
||
public int X; | ||
public int A; | ||
public int B; | ||
public int C; | ||
} | ||
|
||
[My(0+0, c:1+1, X=2+2)] | ||
class Program | ||
{ | ||
static void Main() | ||
{ | ||
typeof(Program).GetCustomAttribute(typeof(MyAttribute)); | ||
_ = new MyAttribute(0+0, c: 1+1); | ||
} | ||
}"; | ||
var compilation = CreateCompilation(source, targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularPreview); | ||
compilation.VerifyDiagnostics(); | ||
CompileAndVerify(compilation, expectedOutput: @"'0', '2', '2', '0+0', '', '1+1' | ||
'0', '2', '2', '0+0', '', '1+1'"); | ||
} | ||
|
||
[Fact] | ||
public void TestCallerInfoAttributesWithSaneDefaultValues() | ||
{ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels like we should be able to completely get rid of this field in favor of using
ConstructorArgumentsToParamsOpt
. However, it is probably better to follow up on that separately.