-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Extension indexers: allow and resolve extension indexers #81607
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
Conversation
37bad6f to
344fa46
Compare
344fa46 to
f504f6e
Compare
|
It looks like the speclet says nothing about consumption scenarios #Closed |
Yes, the speclet was not yet updated.
|
| if (member is PropertySymbol) | ||
| { | ||
| diagnostics.Add(new CSDiagnostic( | ||
| new CSDiagnosticInfo(ErrorCode.ERR_ExtensionDisallowsIndexerMember, new CSharpRequiredLanguageVersion(MessageID.IDS_FeatureExtensionIndexers.RequiredVersion())), |
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 (!IsAllowedExtensionMember(member, languageVersion)) | ||
| { | ||
| diagnostics.Add(ErrorCode.ERR_ExtensionDisallowsMember, member.GetFirstLocation()); | ||
| if (member is PropertySymbol) |
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.
| <data name="IDS_FeatureExtensionIndexers" xml:space="preserve"> | ||
| <value>extension indexers</value> | ||
| </data> | ||
| <data name="ERR_ExtensionDisallowsIndexerMember" xml:space="preserve"> |
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 static void ReportDiagnosticsIfDisallowedExtensionIndexer(BindingDiagnosticBag diagnostics, PropertySymbol property, SyntaxNode syntax) | ||
| { | ||
| if (property.IsExtensionBlockMember()) |
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 setMethod = replace(SetMethod); | ||
| Symbol symbol = ReferenceEquals(Symbol, Method) && method is not null ? method : Symbol; | ||
|
|
||
| Debug.Assert(SetMethod?.IsExtensionBlockMember() != 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.
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 analyze the invocation for property access and indexer access in VisitPropertyAccess, VisitIndexerAccess and VisitAssignmentOperator.
In VisitPropertyAccess there is no argument mixing to analyze.
In VisitAssignmentOperator we only analyze the setter (so Method is set, but SetMethod isn't).
This PR added argument mixing handling to VisitIndexerAccess, which is the first time we'd hit this logic with extension member get and set both used.
I have to say I found this Method/SetMethod design confusing. So I made a separate commit that simplifies this design by removing SetMethod. For a given scenario, we can always analyze arg mixing for one specific method, we don't need two.
After that change, we analyze the getter invocation in VisitIndexerAccess (when a getter is involved) and we analyze the setter invocation in VisitAssignmentOperator (which we were already doing previously, but only for extension members).
The only downside is that for certain operations (increment operator) we now report two sets of diagnostics: one for the getter and one for the setter, so there's a bit of redundancy. I think that's okay, but I could leave a PROTOTYPE to further refine if you want.
This also fixed spurious diagnostics (see removed diagnostics in RefEscapingTests.cs
| lookupResult, WellKnownMemberNames.Indexer, arity: 0, LookupOptions.AllMethodsOnArityZero, | ||
| originalBinder: binder, useSiteInfo: ref useSiteInfo); | ||
|
|
||
| if (!lookupResult.IsMultiViable || lookupResult.Symbols.All(s => s.Kind != SymbolKind.Property)) |
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.
|
|
||
| MemberResolutionResult<PropertySymbol> resolutionResult = result.ValidResult; | ||
| Debug.Assert(resolutionResult.Result.ConversionForArg(0).Exists); | ||
| resolutionResult = resolutionResult.WithResult(resolutionResult.Result.WithoutReceiverArgument()); |
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. We're getting argumentNames and argumentRefKinds from analyzedArguments (not from actualArguments or some other source).
| ArrayBuilder<PropertySymbol>? properties = null; | ||
| foreach (var member in lookupResult.Symbols) | ||
| { | ||
| if (member is PropertySymbol property) |
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.
|
|
||
| overloadResolutionResult.Free(); | ||
| return propertyAccess; | ||
| bool isNewExtensionMember = property.IsExtensionBlockMember(); |
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.
|
|
||
| // Make sure that the result of overload resolution is valid. | ||
| var gotError = MemberGroupFinalValidationAccessibilityChecks(receiver, property, syntax, diagnostics, invokedAsExtensionMethod: false); | ||
| private BoundExpression BindIndexerOrIndexedPropertyAccessContinued(SyntaxNode syntax, BoundExpression receiver, AnalyzedArguments analyzedArguments, BindingDiagnosticBag diagnostics, ImmutableArray<string> argumentNames, ImmutableArray<RefKind> argumentRefKinds, MemberResolutionResult<PropertySymbol> resolutionResult) |
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.
| Binder binder, | ||
| ExtensionScope scope, | ||
| BindingDiagnosticBag diagnostics, | ||
| out BoundExpression? extensionIndexerAccess) |
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 TryBindExtensionIndexer(SyntaxNode syntax, BoundExpression left, AnalyzedArguments analyzedArguments, BindingDiagnosticBag diagnostics, [NotNullWhen(true)] out BoundExpression? extensionIndexerAccess) |
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.
| singleLookupResults.Free(); | ||
| } | ||
|
|
||
| private void LookupAllNewExtensionMembersInSingleBinder(LookupResult result, string? 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.
| int arity, LookupOptions options, Binder originalBinder, ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo) | ||
| { | ||
| var singleLookupResults = ArrayBuilder<SingleLookupResult>.GetInstance(); | ||
| EnumerateAllNewExtensionMembersInSingleBinder(singleLookupResults, name, arity, options, originalBinder, 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.
| CompoundUseSiteInfo<AssemblySymbol> useSiteInfo = binder.GetNewCompoundUseSiteInfo(diagnostics); | ||
|
|
||
| scope.Binder.LookupAllNewExtensionMembersInSingleBinder( | ||
| lookupResult, WellKnownMemberNames.Indexer, arity: 0, LookupOptions.AllMethodsOnArityZero, |
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 could get a method back. The logic below filters them out.
But your question made me realize we were misusing the AllMethodsOnArityZero flag, so removed it here.
For methods and named types, explicit type arguments can be used, so we need the option of looking up with or without explicit arity. But for properties/indexers, we don't need such an option. Adjusted extensionMemberMatches in NamedTypeSymbol.GetExtensionMembers to always find properties/indexers from generic extension blocks given arity zero.
| return; | ||
| case BoundIndexerAccess { Indexer.SetMethod: { } indexerSet } indexer when indexerSet.IsExtensionBlockMember(): | ||
| methodInvocationInfo = MethodInvocationInfo.FromIndexerAccess(indexer); | ||
| Debug.Assert(ReferenceEquals(methodInvocationInfo.MethodInfo.Method, indexerSet)); |
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 get an identical but not reference-equal method. SubstitutedPropertySymbol.GetMethod and .SetMethod return new symbols every time they are called.
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 assert the equality fact then?
| // Tracked by https://github.com/dotnet/roslyn/issues/71056 | ||
| AddPlaceholderReplacement(argumentPlaceholder, integerArgument); | ||
| // https://github.com/dotnet/roslyn/issues/78829 - Do we need to do something special for recievers of extension indexers here? | ||
| Debug.Assert(!indexerAccess.Indexer.IsExtensionBlockMember()); |
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 think the comment is useful until the design decision is pending. Consider transforming it into a PROTOTYPE comment, if you simply want to remove a link to the issue.
| Debug.Assert(member.Kind != SymbolKind.NamedType); | ||
| if (member.Kind == SymbolKind.Method | ||
| && (options & LookupOptions.AllMethodsOnArityZero) == 0 | ||
| && arity != member.GetMemberArityIncludingExtension()) |
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 is verification skipped? #Resolved Refers to: src/Compilers/CSharp/Test/CSharp15/ExtensionIndexersTests.cs:2598 in 8dad62b. [](commit_id = 8dad62b, deletion_comment = False) |
Why is verification skipped? #Resolved Refers to: src/Compilers/CSharp/Test/CSharp15/ExtensionIndexersTests.cs:2654 in 8dad62b. [](commit_id = 8dad62b, deletion_comment = False) |
Why is verification skipped? #Resolved Refers to: src/Compilers/CSharp/Test/CSharp15/ExtensionIndexersTests.cs:2763 in 8dad62b. [](commit_id = 8dad62b, deletion_comment = False) |
|
|
||
| comp = CreateCompilation(src, targetFramework: TargetFramework.Net70); | ||
| comp.VerifyEmitDiagnostics(); | ||
| CompileAndVerify(comp, expectedOutput: ExpectedOutput("^1"), verify: Verification.Skipped).VerifyDiagnostics(); |
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 skipped verification in tests that use .NET Core TFM and therefore cannot verify on Desktop.
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 we use FailsPEVerify instead?
|
|
||
| var comp = CreateCompilation(source, targetFramework: TargetFramework.Net100); | ||
| comp.VerifyEmitDiagnostics( | ||
| // (5,21): error CS8352: Cannot use variable 'span' in this context because it may expose referenced variables outside of their declaration scope |
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 do. I think that was already tested elsewhere. It would be something like _ = s.P; with an already created instance of S (which determines its escape scope).
The object creation scenario however is different. The created instance has its escape scoped computed from the inputs, which includes initializers.
|
|
||
| var comp = CreateCompilation(source, targetFramework: TargetFramework.Net100); | ||
| comp.VerifyEmitDiagnostics( | ||
| // (5,25): error CS8352: Cannot use variable 'span' in this context because it may expose referenced variables outside of their declaration scope |
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.
Answered elsewhere
|
|
||
| var comp = CreateCompilation(source, targetFramework: TargetFramework.Net100); | ||
| comp.VerifyEmitDiagnostics( | ||
| // (5,21): error CS8352: Cannot use variable 'span' in this context because it may expose referenced variables outside of their declaration scope |
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.
Answered elsewhere
| "; | ||
| var comp = CreateCompilationWithMscorlibAndSpan(csharp, parseOptions: TestOptions.Regular.WithLanguageVersion(languageVersion), options: TestOptions.UnsafeDebugDll); | ||
| comp.VerifyDiagnostics( | ||
| // (11,15): warning CS9080: Use of variable 'x' in this context may expose referenced variables outside of their declaration scope |
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, the struct is readonly so all its members are readonly. A readonly accessor does not risk capturing and exposing the referenced variable.
| "; | ||
| var comp = CreateCompilationWithMscorlibAndSpan(csharp, parseOptions: TestOptions.Regular.WithLanguageVersion(languageVersion), options: TestOptions.UnsafeDebugDll); | ||
| comp.VerifyDiagnostics( | ||
| // (11,15): warning CS9080: Use of variable 'x' in this context may expose referenced variables outside of their declaration scope |
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, the property is readonly so the accessors are readonly
| "; | ||
| var comp = CreateCompilationWithMscorlibAndSpan(csharp, parseOptions: TestOptions.Regular.WithLanguageVersion(languageVersion), options: TestOptions.UnsafeDebugDll); | ||
| comp.VerifyDiagnostics( | ||
| // (11,15): warning CS9080: Use of variable 'x' in this context may expose referenced variables outside of their declaration scope |
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, the setter is readonly
|
Done with review pass (commit 7) |
|
It looks like some of the ref safety analysis changes are desirable regardless of extension indexers work. I think it would be good to open issues for corresponding scenarios and link them to this PR. Also, we should consider if it is worth making those changes in |
Answered elsewhere In reply to: 3684111329 Refers to: src/Compilers/CSharp/Test/CSharp15/ExtensionIndexersTests.cs:2598 in 8dad62b. [](commit_id = 8dad62b, deletion_comment = False) |
Same In reply to: 3684119503 Refers to: src/Compilers/CSharp/Test/CSharp15/ExtensionIndexersTests.cs:2654 in 8dad62b. [](commit_id = 8dad62b, deletion_comment = False) |
Same In reply to: 3684123771 Refers to: src/Compilers/CSharp/Test/CSharp15/ExtensionIndexersTests.cs:2763 in 8dad62b. [](commit_id = 8dad62b, deletion_comment = False) |
|
Opened #81796 In reply to: 3684255145 |
AlekseyTs
left a comment
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 8), with suggestion to use Verification.FailsPEVerify instead of Verification.Skipped
|
@dotnet/roslyn-compiler for second review. Someone comfortable with ref analysis would be great. Thanks |
| var accessorKind = property.RefKind == RefKind.None ? AccessorKind.Set : AccessorKind.Get; | ||
| var methodInfo = MethodInfo.Create(property, accessorKind); | ||
| bool isSet = property.RefKind == RefKind.None; | ||
| var accessorKind = isSet ? AccessorKind.Set : AccessorKind.Get; |
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 variable looks unused. #Resolved
| => new MethodInvocationInfo | ||
| public static MethodInvocationInfo FromIndexerReadAccess(BoundIndexerAccess indexerAccess, BoundExpression? substitutedReceiver = null) | ||
| { | ||
| Debug.Assert(indexerAccess.AccessorKind != AccessorKind.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.
What if we get AccessorKind.Both here? Then this doesn't seem to be "indexer read access." #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.
"Both" does include a read, so the name of the method seems okay. But I could rename to FromIndexerGetter to avoid any ambiguity...
| OverloadResolutionResult<PropertySymbol> overloadResolutionResult = OverloadResolutionResult<PropertySymbol>.GetInstance(); | ||
| // We don't consider when we're in default parameter values or attribute arguments so that we avoid cycles. This is an error scenario, | ||
| // so we don't care if we accidentally miss a parameter being applicable. | ||
| bool allowRefOmittedArguments = !InParameterDefaultValue && !InAttributeArgument && receiver.IsExpressionOfComImportType(); |
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 the comment above this variable be removed too? #Resolved
| Diagnostic(ErrorCode.ERR_CallArgMixing, "local[stackSpan]").WithArguments("S3.this[System.Span<int>]", "span").WithLocation(48, 9), | ||
| // (48,15): error CS8352: Cannot use variable 'stackSpan' in this context because it may expose referenced variables outside of their declaration scope | ||
| // local[stackSpan]++; // 4 | ||
| Diagnostic(ErrorCode.ERR_EscapeVariable, "stackSpan").WithArguments("stackSpan").WithLocation(48, 15), |
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 do we produce these duplicate diagnostics now? #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.
Both the getter and the setter produce a diagnostic
The main changes in this PR:
TryBindExtensionIndexerIDS_FeatureExtensionIndexersand check LangVer (declarations and usages, seeCheckExtensionMembersandReportDiagnosticsIfDisallowedExtensionIndexer)DefaultMemberAttributeon grouping type, which contains the extension indexer(s)RefSafetyAnalysis.MethodInfoand fix analysis of assignmentsIdentified some follow-ups around nullability, CREF binding,
GetMemberGroup, "countable" properties, interpolation handlers, improving diagnostic quality, an existing crash with a method namedthis[]in metadata.The usages of extension indexers: element access, null-conditional indexing and assignment, list and spread pattern, and object initializer.
Corresponding spec is
incominghere. Some notable choices (will confirm with LDM):baseFixes #81796 which was opened for tracking purpose
Relates to test plan #81505