-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Collection expressions: require Add method callable with a single argument for class and struct types that do not use [CollectionBuilder] #72654
Conversation
e2ac0e8
to
051d5ec
Compare
…ent for class and struct types that do not use [CollectionBuilder]
// Any trailing parameters must be optional or params. | ||
for (int i = requiredLength; i < parameters.Length; i++) | ||
{ | ||
if (parameters[i] is { IsOptional: false, IsParams: 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.
The IsParams
check is inconsistent with how overload resolution does that.
- Only value on corresponding least overridden method matters
- Only value on the last parameter matters.
- The definition type of the least overridden method parameter matters, see
OverloadResolution.IsValidParams
. In fact I suggest to use that helper here #Closed
return true; | ||
} | ||
|
||
var implicitReceiver = new BoundObjectOrCollectionValuePlaceholder(syntax, isNewInstance: true, targetType) { WasCompilerGenerated = true }; |
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 implicitReceiver = new BoundObjectOrCollectionValuePlaceholder(syntax, isNewInstance: true, targetType) { WasCompilerGenerated = true };
I assume the logic starting from here to the end of the method corresponds to what is happening in MethodGroupResolution Binder.BindExtensionMethod
. Consider extracting this code into a local function, adding a comment referring to the "prototype" API (Binder.BindExtensionMethod
), adding a comment to Binder.BindExtensionMethod
stating that relevant changes there should be mirrored in the local function here. This will make it easier to make sure that necessary adjustments will be made to properly handle extension types. #Closed
var useSiteInfo = CompoundUseSiteInfo<AssemblySymbol>.Discarded; | ||
|
||
var lookupResult = LookupResult.GetInstance(); | ||
LookupInstanceMember(lookupResult, targetType, leftIsBaseReference: false, rightName: methodName, rightArity: 0, invoked: true, ref useSiteInfo); |
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 trying to duplicate the lookup code, I think we should use Binder.BindInstanceMemberAccess
and proceed only if we get a BoundMethodGroup
back. I am thinking of a call similar to the one in HasCollectionExpressionApplicableAddMethod.makeInvocationExpression
. Afterwards, I think we should pay attention to the BoundMethodGroup.SearchExtensionMethods
value before looking for extensions. #Closed
} | ||
} | ||
|
||
// Value parameter must be by value, in, or ref readonly. |
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.
return false; | ||
} | ||
|
||
// If the method is generic, can the type arguments be inferred from the one or two arguments? |
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 typeParameters = method.TypeParameters; | ||
if (typeParameters.Length > 0) | ||
{ | ||
var usedParameterTypes = parameters.Slice(0, requiredLength).SelectAsArray(p => p.Type); |
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.
Changed to AsSpan(0, requiredLength)
.
var typeParameters = method.TypeParameters; | ||
if (typeParameters.Length > 0) | ||
{ | ||
var usedParameterTypes = parameters.Slice(0, requiredLength).SelectAsArray(p => p.Type); |
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.
Do we have a test demonstrating that inaccessible instance method doesn't prevent us from finding an extension method? #Closed Refers to: src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs:34333 in 442498d. [](commit_id = 442498d, deletion_comment = False) |
Are there ref restrictions for Refers to: src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs:34608 in 442498d. [](commit_id = 442498d, deletion_comment = False) |
It is probably worth adding a similar test but with type for an additional params collection parameter missing instead. It would be good to observe what effect this is going to have on conversion classification. #Closed Refers to: src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs:36190 in 442498d. [](commit_id = 442498d, deletion_comment = False) |
|
||
return false; | ||
|
||
static bool isApplicableAddMethod(MethodSymbol method, bool expectingExtensionMethod) |
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 there are legitimate test failures in the IOperation leg. #Resolved |
Done with review pass (commit 1) |
@@ -1285,6 +1285,85 @@ internal bool HasCollectionExpressionApplicableAddMethod(SyntaxNode syntax, Type | |||
} | |||
} | |||
|
|||
internal bool HasCollectionExpressionAddMethod(SyntaxNode syntax, TypeSymbol targetType) |
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 am not sure how easy it will be to come up with a scenario making importance of this obvious, but I think this helper should keep track of all dependencies (ref assemblies) that were involved on the path to success and make the caller aware of that set. I do not think the set must be explicitly narrowed, i.e. no reason to trim dependencies from "failed" attempts. Our primary concern is to not drop dependencies for "success". #Closed
switch (element) | ||
{ | ||
case BoundCall call: | ||
return call.Arguments[call.InvokedAsExtensionMethod ? 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.
return call.Arguments[call.InvokedAsExtensionMethod ? 1 : 0];
This logic looks suspicious. It is not obvious what are we trying to accomplish and why. It looks like BoundCollectionExpression
holds onto the original UnconvertedCollectionExpression
node. Could we. perhaps, use information from that node instead? Not sure what do we want to do with expressions that do not have a natural type though. #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.
It feels like it would be better to place this special logic into CreateBoundCollectionExpressionSpreadElement
, where it would make sense as an error recovery logic. It doesn't feel like it belongs in this general helper because we don't really know where the element
node is coming from and whether it is appropriate to dig into it this way.
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 case is to unwrap the Add()
argument when bindCollectionInitializerElementAddMethod
fails in overload resolution with one or more inapplicable or ambiguous Add
methods. We hit this case for spreads and for regular elements.
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 hit this case for spreads and for regular elements.
Thanks for the clarification. Consider adding a comment what the code is trying to accomplish and why it makes sense to do what it is doing.
Done with review pass (commit 2) |
Added In reply to: 2015424773 Refers to: src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs:34333 in 442498d. [](commit_id = 442498d, deletion_comment = 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.
LGTM (commit 13), assuming CI is passing
@dotnet/roslyn-compiler for a second review, thanks. |
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 17)
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.
Generally this looks good, and I don't think any of my test suggestions should block merging, so I'm signing off. But let's get those tests in in a separate PR.
@@ -4521,12 +4509,18 @@ static void Main() | |||
// (7,13): error CS1061: 'string' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'string' could be found (are you missing a using directive or an assembly reference?) | |||
// s = [default]; | |||
Diagnostic(ErrorCode.ERR_NoSuchMemberOrExtension, "[default]").WithArguments("string", "Add").WithLocation(7, 13), | |||
// (7,13): error CS9215: Collection expression type 'string' must have an instance or extension method 'Add' that can be called with a single argument. |
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.
Can we do something to suppress one of the duplicate errors here?
[Theory] | ||
[InlineData("")] | ||
[InlineData("in")] | ||
[InlineData("ref readonly")] |
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.
Why does this work for ref readonly
? I would, at the very least, expect a warning? #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.
Why does this work for
ref readonly
?
Why do you think it shouldn't?
I would, at the very least, expect a warning?
A warning where? Saying what?
Are you referring to: "warning CS9192: Argument 1 should be passed with 'ref' or 'in' keyword"
Note, we intentionally do not report it for MyCollection<C> z = new () {x};
today because there is no place to add any of the suggested keywords to make the warning to go away.
[Theory] | ||
[InlineData("")] | ||
[InlineData("in")] | ||
[InlineData("ref readonly")] |
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.
Why does this work for ref readonly
? I would, at the very least, expect a warning? #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.
Same response, I guess.
[Theory] | ||
[InlineData("")] | ||
[InlineData("in")] | ||
[InlineData("ref readonly")] |
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.
Why does this work for ref readonly
? I would, at the very least, expect a warning? #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.
Same response, I guess.
[Theory] | ||
[InlineData("")] | ||
[InlineData("in")] | ||
[InlineData("ref readonly")] |
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.
Why does this work for ref readonly
? I would, at the very least, expect a warning? #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.
Same response, I guess.
.method public instance void Add(!T x, !T y) | ||
{ | ||
.param [2] | ||
.custom instance void [mscorlib]System.ParamArrayAttribute::.ctor() = ( 01 00 00 00 ) |
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 have a version with ParamCollectionAttribute
? #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.
Should we have a version with
ParamCollectionAttribute
?
AddMethod_ParamCollectionAttribute_03
int x = 1; | ||
int[] y = [2, 3]; | ||
MyCollection<int> z = [x, ..y]; | ||
MyCollection<int> w = new() { x }; |
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.
What if we do MyCollection<int[]>
?
} | ||
|
||
[Fact] | ||
public void TargetTypedElement_PublicAPI_List() | ||
public void AddMethod_Extension_12_WrongThisType() |
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.
Let's have a version of this one with a variant interface: IE, the Add
method is defined as an extension on IEnumerable<object>
, when I have a MyCollection<string>
and it implements IEnumerable<string>
. #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.
Added AddMethod_Extension_13_WrongThisType
.
comp.VerifyEmitDiagnostics( | ||
// (11,36): error CS1103: The first parameter of an extension method cannot be of type 'dynamic' | ||
// public static void Add<T>(this dynamic d, T t) { d.__AddInternal(t); } | ||
Diagnostic(ErrorCode.ERR_BadTypeforThis, "dynamic").WithArguments("dynamic").WithLocation(11, 36)); |
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.
And what happens when we see this in IL? Will we call it from Main
in source?
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); | ||
private List<MyCollection?> GetList() => _list ??= new(); | ||
unsafe public void Add(void* p) => throw null; |
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.
Let's have a variant where this is the closest extension method, but there's a better (safe) extension method further away. #Resolved
{ | ||
return Conversion.NoConversion; | ||
} | ||
|
||
// PROTOTYPE: Probably should at least accumulate dependencies and propagate them in case of success. | ||
} | ||
|
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.
// PROTOTYPE: Probably should at least accumulate dependencies and propagate them in case of success.
Logged #72770.
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 18)
Looks like there's legitimate test failure in UsedAssemblies leg for CollectionExpressionTests.AddMethod_RefOmittedArguments (assertion failed at Microsoft.CodeAnalysis.CSharp.LocalRewriter.MakeCollectionInitializer(BoundExpression rewrittenReceiver, BoundCollectionElementInitializer initializer) in /_/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_ObjectOrCollectionInitializerExpression.cs:line 145) |
How the CI passed then? |
The test was marked as skipped in the last commit (1316830) |
The assert is not specific to the UsedAssemblies leg, I think. |
Spec changes: dotnet/csharplang#8022
Fixes #72098