-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Support interpolated string handlers in extension blocks #78425
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
base: main
Are you sure you want to change the base?
Conversation
@@ -7,8 +7,9 @@ namespace Microsoft.CodeAnalysis.CSharp | |||
internal partial class BoundInterpolatedStringArgumentPlaceholder | |||
{ | |||
public const int InstanceParameter = -1; | |||
public const int TrailingConstructorValidityParameter = -2; | |||
public const int UnspecifiedParameter = -3; | |||
public const int ExtensionReceiver = -2; |
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.
Looks like we adjusted all places that used InstanceParameter to also consider ExtensionReceiver except one which is responsible for reporting ERR_InterpolatedStringsReferencingInstanceCannotBeInObjectInitializers - doesn't that need to be adjusted as well?
// If this is the extension method receiver (ie, parameter 0), then any non-empty list of indexes must | ||
// be an error, and we should have already returned an empty list. | ||
Debug.Assert(_underlyingParameter.ContainingSymbol is not NamedTypeSymbol); | ||
Debug.Assert(originalIndexes.All(static index => index != BoundInterpolatedStringArgumentPlaceholder.InstanceParameter)); |
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.
This assert seems unnecessary since we throw for the exact same condition below
BoundInterpolatedStringArgumentPlaceholder.InstanceParameter => throw ExceptionUtilities.Unreachable(), | ||
BoundInterpolatedStringArgumentPlaceholder.ExtensionReceiver => 0, | ||
BoundInterpolatedStringArgumentPlaceholder.TrailingConstructorValidityParameter => BoundInterpolatedStringArgumentPlaceholder.TrailingConstructorValidityParameter, | ||
BoundInterpolatedStringArgumentPlaceholder.UnspecifiedParameter => BoundInterpolatedStringArgumentPlaceholder.UnspecifiedParameter, |
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.
Should we instead have case var i when i < 0 => i
to handle other possible values in the future? Otherwise consider asserting that index >= 0
in the case where we return index + 1
.
@@ -19099,6 +19099,810 @@ public void AppendFormatted<T>(T hole, int alignment = 0, string format = null) | |||
Diagnostic(ErrorCode.ERR_InterpolatedStringHandlerMethodReturnMalformed, "{f2()}").WithArguments("?.()").WithLocation(1, 21)); | |||
} | |||
|
|||
[Theory, WorkItem("https://github.com/dotnet/roslyn/issues/78137")] |
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.
Does this PR also by any chance fix #78435?
} | ||
"""; | ||
|
||
var comp = CreateCompilation(src, targetFramework: TargetFramework.Net90); |
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 verifying diagnostics. In the test below as well.
"""; | ||
|
||
var expectedOutput = ExecutionConditionUtil.IsCoreClr ? "12" : null; | ||
CompileAndVerify([exeSource, src], targetFramework: TargetFramework.Net90, expectedOutput: expectedOutput, verify: Verification.FailsPEVerify); |
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 adding more VerifyDiagnostics()
calls throughout the new tests.
Are we testing extension indexers? They are in a semi-supported state. If the scenario is currently blocked due to an error, let's still add a test so that we do not forget about adjusting this code path if/when the error is no longer reported. Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs:5977 in 4d67a96. [](commit_id = 4d67a96, deletion_comment = False) |
@@ -712,6 +712,7 @@ private void GetInterpolatedStringPlaceholders( | |||
switch (argIndex) | |||
{ | |||
case BoundInterpolatedStringArgumentPlaceholder.InstanceParameter: | |||
case BoundInterpolatedStringArgumentPlaceholder.ExtensionReceiver: |
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.
It looks like ref safety analysis also does the same trick as nullable analysis by adjusting the arguments
to include receiver at position 0. See usages of ReplaceWithExtensionImplementationIfNeeded
helper. It also looks like GetInterpolatedStringPlaceholders
is getting called before that transformation is applied. I think that it would be less confusing and more robust to always operate on data in the transformed form. Therefore, the transformation should be applied earlier (before GetInterpolatedStringPlaceholders
calls) and changes here should probably be similar to the changes made in NullableWalker,
In terms of implementation, we might want to group all transformed information into a single struct type, always pass it around as a single instance and make the transformation a part of construction of the instance. This way, the transformation will be done in one place and no one will be able to go around it.
We should make sure we have functional test coverage for this code as well.
@@ -9571,6 +9571,9 @@ void visitInterpolatedStringHandlerConstructor() | |||
return; | |||
} | |||
|
|||
// In new extension form, the nullable rewriter processes the arguments as if receiver is the first item in the argument list, like the old extension form. This means that all of | |||
// our placeholders will be off-by-one, with the extension receiver in the first position. | |||
var newExtensionFormOffset = handlerData.ArgumentPlaceholders.Any(p => p.ArgumentIndex == BoundInterpolatedStringArgumentPlaceholder.ExtensionReceiver) ? 1 : 0; |
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.
@@ -22,8 +24,31 @@ public RewrittenParameterSymbol(ParameterSymbol originalParameter) : | |||
|
|||
internal sealed override int CallerArgumentExpressionParameterIndex => _underlyingParameter.CallerArgumentExpressionParameterIndex; | |||
|
|||
internal sealed override ImmutableArray<int> InterpolatedStringHandlerArgumentIndexes => throw ExceptionUtilities.Unreachable(); // Tracked by https://github.com/dotnet/roslyn/issues/76130 : Follow up | |||
internal sealed override ImmutableArray<int> 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.
var originalIndexes = this._underlyingParameter.InterpolatedStringHandlerArgumentIndexes; | ||
if (originalIndexes.IsDefaultOrEmpty) | ||
{ | ||
return originalIndexes; |
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 (ContainingSymbol.ContainingSymbol is TypeSymbol { IsExtension: true, ExtensionParameter.Name: var extensionParameterName } | ||
&& string.Equals(extensionParameterName, name, StringComparison.Ordinal)) | ||
{ | ||
builder.Add(BoundInterpolatedStringArgumentPlaceholder.ExtensionReceiver); |
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.
src/Compilers/CSharp/Portable/Symbols/Source/SourceComplexParameterSymbol.cs
Show resolved
Hide resolved
src/Compilers/CSharp/Portable/Symbols/Source/SourceComplexParameterSymbol.cs
Show resolved
Hide resolved
@@ -19099,6 +19099,810 @@ public void AppendFormatted<T>(T hole, int alignment = 0, string format = null) | |||
Diagnostic(ErrorCode.ERR_InterpolatedStringHandlerMethodReturnMalformed, "{f2()}").WithArguments("?.()").WithLocation(1, 21)); | |||
} | |||
|
|||
[Theory, WorkItem("https://github.com/dotnet/roslyn/issues/78137")] | |||
[CombinatorialData] | |||
public void InterpolationHandler_ReceiverParameter_Identity(bool useMetadataRef) |
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 think we can benefit from asserting IL for unique enough scenarios. In particular I am interested in understanding and have a baseline for receiver capturing. For example, when we have a by value struct receiver, it is going to be captured by reference or by value? Are ByRef locals going to be involved? If so, could this block some async
scenarios, etc.
|
||
[Theory, WorkItem("https://github.com/dotnet/roslyn/issues/78137")] | ||
[CombinatorialData] | ||
public void InterpolationHandler_ReceiverParameter_WithConversion(bool useMetadataRef) |
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 add a comment; the receiver is a string
, so there's an implicit BoundConversion
as the receiver to the extension method itself.
|
||
[Theory, WorkItem("https://github.com/dotnet/roslyn/issues/78137")] | ||
[CombinatorialData] | ||
public void InterpolationHandler_ReceiverParameter_ByRef(bool useMetadataRef) |
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 are lacking tests for "by value" struct scenarios. In addition to a trivial case, it would be interesting to test a scenario when a handler constructor modifies the receiver by direct access (not through parameter), for example by changing value of the field used as the receiver. Are we going to observe the change in the receiver value inside the extension method?
Done with the review pass (commit 2) |
…dexers/static methods
…ated-string * upstream/main: (330 commits) Add support for 'fixed' Add test Add assert Modify ABWQ.AddWork to take in a ROS instead of an IEnumerable (dotnet#78691) [main] Source code updates from dotnet/dotnet (dotnet#78692) Add a test to verify that behavior is expected and bug no longer occurs [main] Source code updates from dotnet/dotnet (dotnet#78663) Add comment Revert "fix symbol publishing? (dotnet#78671)" (dotnet#78686) Revert "Update Microsoft.Build.Tasks.CodeAnalysis.Sdk.csproj (dotnet#78681)" (dotnet#78687) Use VerifyDiagnostics format to report CompileAndVerify diagnostics (dotnet#78666) Fix nullable crash for field keyword in partial property (dotnet#78672) Update Microsoft.Build.Tasks.CodeAnalysis.Sdk.csproj (dotnet#78681) Applied code review suggestions. Add test assertions Use ThrowIfFalse when resolving paths Cleanup fix symbol publishing? (dotnet#78671) Use SBRP version of M.CA for analyzers in the context of source build (dotnet#78668) Hot Reload: Handle document deletes (dotnet#78516) ...
…ceiver parameter is used.
{ | ||
var originalIndexes = _underlyingParameter.InterpolatedStringHandlerArgumentIndexes; | ||
Debug.Assert(originalIndexes.IsEmpty); | ||
return originalIndexes; |
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 bool HasInterpolatedStringHandlerArgumentError => _underlyingParameter.HasInterpolatedStringHandlerArgumentError; |
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 don't think we should return false always. If the parameter has handler arguments, then it should have an error on it, regardless of whether you're looking at it as an receiver parameter or not. InterpolationHandler_ParameterErrors_MappedCorrectly_02
asserts that this returns true
, and I think it should stay that way.
@@ -3,6 +3,8 @@ | |||
// See the LICENSE file in the project root for more information. | |||
|
|||
using System.Collections.Immutable; | |||
using System.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.
Are the added usings still necessary?
Is this function going to do the right thing for a receiver parameter declared in source? Refers to: src/Compilers/CSharp/Portable/Symbols/Source/SourceComplexParameterSymbol.cs:1237 in b20fcbd. [](commit_id = b20fcbd, deletion_comment = False) |
@@ -28,5 +28,9 @@ internal override ParameterSymbol WithCustomModifiersAndParams(TypeSymbol newTyp | |||
_originalParam.WithCustomModifiersAndParamsCore(newType, newCustomModifiers, newRefCustomModifiers, newIsParams), | |||
this.ContainingSymbol); | |||
} | |||
|
|||
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.
I don't think so currently: if it was going to, then we'd have already known about it because SourceClonedParameterSymbol
throws an unreachable exception for this codepath. This is only hittable in extension scenarios. I can add an assert to that effect if you'd like?
@@ -2339,7 +2441,7 @@ class Nested { } | |||
|
|||
var tree = comp.SyntaxTrees[0]; | |||
var model = comp.GetSemanticModel(tree); | |||
var extension = tree.GetRoot().DescendantNodes().OfType<ExtensionBlockDeclarationSyntax>().Single(); |
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 int this[int j] { get => i + j; set { } } | ||
} | ||
} | ||
"""; |
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 6) |
} | ||
"""; | ||
|
||
var comp = CreateCompilation(src, targetFramework: TargetFramework.Net90); |
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.
Closes #78137.
Relates to test plan #76130