-
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
InterpolatedStringHandlerArgumentAttribute Decoding #53932
InterpolatedStringHandlerArgumentAttribute Decoding #53932
Conversation
@chsienki @AlekseyTs for review. |
if (PEModule.CrackStringArrayInAttributeValue(out additionalLanguageNames, ref argsReader)) | ||
{ | ||
if (additionalLanguageNames.Length == 0) | ||
{ | ||
return SpecializedCollections.SingletonEnumerable(firstLanguageName); | ||
return firstLanguageName == null ? SpecializedCollections.EmptyEnumerable<string>() : SpecializedCollections.SingletonEnumerable(firstLanguageName); | ||
} |
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.
Previously this would return an array of one null string in the case it read null from the language, or the additional languages with null at the end. I suspect it makes no difference, but want to check that behavior change doesn't break anything. #Resolved
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.
Users already had to deal with the empty list as a possible return. I don't think this really introduces anything new.
break; | ||
|
||
default: | ||
var param = parameters.FirstOrDefault(static (p, name) => string.Equals(p.Name, name, StringComparison.Ordinal), name); |
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.
Given that this is the only place we're using FirstOrDefault, is it worth just putting the implementation here, avoiding the lambda, and probably isnt any more lines of code as you don't need the below checks? #Resolved
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.
I don't think so. I prefer it the way I wrote it.
ArrayBuilder<ParameterSymbol?> parameters; | ||
if (constructorArgument.Kind == TypedConstantKind.Primitive) | ||
{ | ||
if (decodeName(constructorArgument, ref arguments) is (int ordinal, var parameter)) |
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.
Consider using is not
and inverting the conditional.
if (decodeName(constructorArgument, ref arguments) is not (int ordinal, var parameter))
{
return;
}
parameterOrdinals = ImmutableArray.Create(ordinal);
parameters = ArrayBuilder<ParameterSymbol?>.GetInstance(1);
parameters.Add(parameter);
``` #Resolved
return (-1, null); | ||
} | ||
|
||
ImmutableArray<ParameterSymbol> parameters = ContainingSymbol switch |
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.
Could do this once, outside the function #Resolved
@@ -1048,6 +1057,199 @@ bool needsReporting() | |||
} | |||
} | |||
|
|||
#nullable enable | |||
private void DecodeInterpolatedStringHandlerArgumentAttribute(ref DecodeWellKnownAttributeArguments<AttributeSyntax, CSharpAttributeData, AttributeLocation> arguments, Binder binder, BindingDiagnosticBag diagnostics) |
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.
I guess i'm suprised that we would need the binder in order to decode the attribute. I see below that we're doing overload resolution to determine if its valid or not. Would an alternative be to just decode the raw strings here? Then wherever we actually use them in the binder, do the actual resolution etc? We can always get back to them from the containing symbol I think? (they always apply to other parameters in the same list as the one the attribute is on?) #Resolved
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.
Would an alternative be to just decode the raw strings here?
We could, yes, but then you don't get validation of the attribute at the attribute definition location. This seems suboptimal to me.
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.
Hmm, yeah thats a bummer. Just seems like we're doing a lot of work here that as a caller I woulnd't expect to happen. Do we have anywhere else where the attribute itself can error based on semantics of other code?
In reply to: 647835991 [](ancestors = 647835991)
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.
Hmm. Could we not just store the location along with the cracked data? So we can report on it if it fails later on?
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.
So we can report on it if it fails later on?
This doesn't help if you never use it in this project (as I would expect many to do, since these will mostly be used in libraries). Unless Aleksey has a different opinion or suggestion for how to approach this, I believe this is the right way to go.
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.
After discussion with Aleksey, we've decided that there's too much potential for issue with missing references, retargeting, and langversion changes to move forward with this for now. I'll remove the binder code.
Done with review pass (iteration 4) |
How changes in this function are related to the goal of the PR? In reply to: 857917744 In reply to: 857917744 In reply to: 857917744 In reply to: 857917744 Refers to: src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerFileReference.cs:296 in b8ea696. [](commit_id = b8ea696, deletion_comment = False) |
} | ||
else if (TryExtractStringArrayValueFromAttribute(targetAttribute.Handle, out var paramNames)) | ||
{ | ||
Debug.Assert(targetAttribute.SignatureIndex == 1); |
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.
else if (TryExtractStringArrayValueFromAttribute(targetAttribute.Handle, out var paramNames)) | ||
{ | ||
Debug.Assert(targetAttribute.SignatureIndex == 1); | ||
return paramNames.NullToEmpty(); |
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.
Is an attribute with an empty array a valid form? If so, I think we should not represent invalid/broken forms with an empty array. This is applicable to other places in this function. Basically I think we should be able to judge from the result of GetInterpolatedStringHandlerArgumentAttributeValues if the attribute was missing or malformed. #Closed
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.
Basically I think we should be able to judge from the result of GetInterpolatedStringHandlerArgumentAttributeValues if the attribute was missing or malformed.
I originally had it as default
if missing/malformed, but thinking about it more I realized I didn't have a reason to need to understand the difference. Unless we want to actually error at the use site when we encounter a malformed attribute from metadata (which I didn't see the need to do), it feels like having the distinction really doesn't buy us much of anything.
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.
Unless we want to actually error at the use site when we encounter a malformed attribute from metadata (which I didn't see the need to do), it feels like having the distinction really doesn't buy us much of anything.
My initial thinking is that we should fail to create a handler at the call site if there is a maformed InterpolatedStringHandlerArgumentAttribute attribute on the corresponding parameter.
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.
I'll record whether the data has errors in a separate bit in the packed flags.
<value>Parameter to interpolated string handler conversion occurs after handler parameter</value> | ||
</data> | ||
<data name="ERR_CannotUseSelfAsInterpolatedStringHandlerArgument" xml:space="preserve"> | ||
<value>Interpolated string handler arguments cannot refer to themselves.</value> |
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.
Is this error about referring to the parameter in the attribute applied to the same parameter? Perhaps it would be clearer to say something like: "cannot be used to create a handler for this parameter"? #Closed
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.
I tried to make it a bit more clear. Let me know if you think it still needs work.
@@ -2020,6 +2020,13 @@ | |||
|
|||
<Node Name="BoundInterpolatedStringHandlerPlaceholder" Base="BoundValuePlaceholderBase"/> | |||
|
|||
<!-- Type expression placeholder for the arguments to the constructor call for an interpolated string handler |
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.
They resulted from me nullable enabling the decoding functions (which I did because I had missed a case in the attribute decoding and was suprised I didn't get a warning about it). In reply to: 857917744 Refers to: src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerFileReference.cs:296 in b8ea696. [](commit_id = b8ea696, deletion_comment = False) |
Since the change goes beyond just adding/adjusting annotations. I think the change should be taken out of this PR and out of this feature branch. If we need to suppress nulable warnings in this function to do this, that is what we should do, I think. In reply to: 857971951 Refers to: src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerFileReference.cs:296 in b8ea696. [](commit_id = b8ea696, deletion_comment = False) |
MethodSymbol { Parameters: var p } => p, | ||
PropertySymbol { Parameters: var p } => p, | ||
_ => throw ExceptionUtilities.UnexpectedValue(ContainingSymbol) | ||
}; |
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 looks like we already have internal static ImmutableArray<ParameterSymbol> GetParameters(this Symbol member)
helper that can be used here. #Closed
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.
BTW, are we going to crash on an event accessor parameter here?
case { Length: 0 } when ContainingSymbol.IsStatic: | ||
// Invalid data, bail | ||
builder.Free(); | ||
return ImmutableArray<int>.Empty; |
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.
switch (name) | ||
{ | ||
case null: | ||
case { Length: 0 } when ContainingSymbol.IsStatic: |
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.
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.
Perhaps the Length check confused me as I thought we are dealing with an array. Consider comparing with an empty string instead, for clarity. Below as well.
@@ -164,6 +164,8 @@ internal override ImmutableHashSet<string> NotNullIfParameterNotNull | |||
get { return ImmutableHashSet<string>.Empty; } | |||
} | |||
|
|||
internal override ImmutableArray<int> InterpolatedStringHandlerArgumentIndexes => _originalParam.InterpolatedStringHandlerArgumentIndexes; |
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.
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.
Cloned parameter symbols share attribute data, so they shouldn't change.
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.
Cloned parameter symbols share attribute data, so they shouldn't change.
Perhaps attribute applications shouldn't change, but the meaning of attributes can. It is quite possible that for other well-known attributes it isn't an issue because they are self-contained. However for this attribute, surrounding parameters and parameter ordinals are significant, etc. So, I would say we should reinterpret the attribute application rather than take it as is. But the question is whether there is a scenario where this particular symbol is required to expose this information at all. If not, then why return anything but a value indicating - fail creating a handler at the call site. Anything other than an "error" value would require thorough testing for this code path.
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.
I'll make these throw for now and mark with a prototype, and we'll see if the indexes are needed during lowering in the next PR. I have a hunch they might be.
@@ -169,6 +169,8 @@ public override string GetDocumentationCommentXml(CultureInfo preferredCulture = | |||
return _underlyingParameter.GetDocumentationCommentXml(preferredCulture, expandIncludes, cancellationToken); | |||
} | |||
|
|||
internal override ImmutableArray<int> InterpolatedStringHandlerArgumentIndexes => _underlyingParameter.InterpolatedStringHandlerArgumentIndexes; |
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.
internal override ImmutableArray InterpolatedStringHandlerArgumentIndexes => _underlyingParameter.InterpolatedStringHandlerArgumentIndexes;
I would prefer overriding this property only in leaf classes. Otherwise, it is hard to predict whether this implementation is appropriate for all derived types now and in the future. #Closed
@@ -746,6 +751,10 @@ internal override void DecodeWellKnownAttribute(ref DecodeWellKnownAttributeArgu | |||
arguments.GetOrCreateData<ParameterWellKnownAttributeData>().HasEnumeratorCancellationAttribute = true; | |||
ValidateCancellationTokenAttribute(arguments.AttributeSyntaxOpt, (BindingDiagnosticBag)arguments.Diagnostics); | |||
} | |||
else if (attribute.IsTargetAttribute(this, AttributeDescription.InterpolatedStringHandlerArgumentAttribute)) |
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.
{ | ||
if (parameter.Name.Equals(name, StringComparison.Ordinal)) | ||
{ | ||
if ((object)parameter == this) |
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.
_ => throw ExceptionUtilities.UnexpectedValue(ContainingType) | ||
}; | ||
|
||
foreach (var parameter in parameters) |
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.
foreach (var parameter in parameters) | ||
{ | ||
var argumentType = parameter?.Type ?? ContainingType; | ||
analyzedArguments.Arguments.Add(new BoundInterpolatedStringArgumentPlaceholder(arguments.AttributeSyntaxOpt!, argumentType, hasErrors: argumentType.IsErrorType())); |
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.
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.
We have tests for this side in this PR, but we're also ignoring any diagnostics that come up from this. Regardless, since I'm removing this code it'll be tested in more depth in the next PR.
else | ||
{ | ||
// Could not find a constructor on '{0}' that matches the parameters specified in the InterpolatedStringHandlerArgumentAttribute. | ||
diagnostics.Add(ErrorCode.ERR_CouldNotFindApplicableInterpolatedStringHandlerConstructor, errorLocation, handlerType); |
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.
diagnostics.Add(ErrorCode.ERR_CouldNotFindApplicableInterpolatedStringHandlerConstructor, errorLocation, handlerType);
I think we should worry whether there are things that are specific to this particular location that could cause a failure that might not occur at a different location (missing references, language version, etc.)
#Closed
errorName: handlerType.Name, | ||
errorLocation, | ||
suppressResultDiagnostics: true, | ||
BindingDiagnosticBag.Discarded, |
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.
When I see us binding with BindingDiagnosticBag.Discarded
and then reporting a diagnostics based on the result, that is a red flag for me. Used asembly references are not tracked and it is quite possible that things break after they are removed because compiler says they are not used. #Closed
var vbCode = @" | ||
Imports System.Runtime.CompilerServices | ||
Public Class C | ||
Public Sub M(i As Integer, <InterpolatedStringHandlerArgument({ Nothing })> c As CustomHandler) |
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.
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.
Yes, this is intentionally an array with 1 element, that element being Nothing
.
} | ||
|
||
[Fact] | ||
public void InterpolatedStringHandlerArgumentAttributeError_ReferenceSelf() |
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.
var vbCode = @" | ||
Imports System.Runtime.CompilerServices | ||
Public Class C | ||
Public Shared Sub M(<InterpolatedStringHandlerArgument({ """" })> c As CustomHandler) |
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.
} | ||
|
||
[Fact] | ||
public void InterpolatedStringHandlerArgumentAttributeWarn_ParameterAfterHandler() |
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.
|
||
public partial struct CustomHandler | ||
{ | ||
private CustomHandler(int literalLength, int formattedCount, int i) : this() {} |
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.
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.
I'll test location more fully in the next PR. We're removing the error for this scenario.
var handler = GetCustomHandlerType("CustomHandler", "partial struct", useBoolReturns: true); | ||
|
||
var comp = CreateCompilation(new[] { code, InterpolatedStringHandlerArgumentAttribute, handler }, parseOptions: TestOptions.RegularPreview); | ||
CompileAndVerify(comp, sourceSymbolValidator: verifier, symbolValidator: verifier); |
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.
Done with review pass (commit 5). |
* Remove attribute constructor resolution during source binding * More tests * Small refactorings.
@AlekseyTs @chsienki addressed your feedback. In reply to: 858138258 |
@@ -621,6 +621,10 @@ public override ImmutableArray<CustomModifier> RefCustomModifiers | |||
} | |||
} | |||
|
|||
internal override ImmutableArray<int> InterpolatedStringHandlerArgumentIndexes => _underlyingParameter.InterpolatedStringHandlerArgumentIndexes; |
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.
_underlyingParameter.InterpolatedStringHandlerArgumentIndexes
Should these be adjusted? I think reduced extension methods remove the first parameter, it becomes the receiver. If we never bind against a reduced method symbol (it used to be that C# only used them for SemanticModel, this is likely still the case), consider simply throwing from both new APIs. #Closed
} | ||
|
||
private struct PackedFlags | ||
{ | ||
// Layout: | ||
// |...|fffffffff|n|rr|cccccccc|vvvvvvvv| | ||
// |..|fffffffff|n|rr|ccccccccc|vvvvvvvvv| |
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.
We are running out of bits here really fast. I believe CallerArgumentEx[pression is going to add at least couple of bits here. Automatic merge isn't goint to work as well.
As an alternative, can we use default
value in _lazyInterpolatedStringHandlerAttributeIndexes
to indicate an error and use another sentinel value to represent an unitilized state, like a shared array with a single int.MinValue in it? #Closed
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.
Rather than creating a new structure, I think the appropriate thing to do will be to move _bits
to a long at that point. It'll take a bit of effort when merging, but I'd much rather do that than try and create a separate side-structure for just this bit.
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.
Talked with Aleksey offline, I misread his comment. I'll do this.
var vbCode = @" | ||
Imports System.Runtime.CompilerServices | ||
Public Class C | ||
Public Sub M(i As Integer, <InterpolatedStringHandlerArgument({ Nothing })> c As CustomHandler) |
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.
} | ||
} | ||
|
||
private bool _hasInterpolatedStringHandlerArgumentAttributeError = false; |
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.
@333fred It looks like there are some related test failures. |
Done with review pass (commit 7) In reply to: 859088157 |
@AlekseyTs addressed feedback. In reply to: 859088157 |
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.
LGTM (commit 9), assuming CI is passing
@chsienki can you please take another look? |
Validate and decode InterpolatedStringHandlerArgumentAttribute from source and from metadata. This doesn't implement call-site validation, just errors on attribute usage and metadata decoding.
Test plan: #51499