Extensions: address some diagnostic quality issues#80827
Conversation
c367836 to
b706ec5
Compare
b706ec5 to
06b2b5a
Compare
| allowTrailingSeparator: false, | ||
| requireOneElement: forExtension, // For extension declarations, we require at least one receiver parameter | ||
| allowSemicolonAsSeparator: false); | ||
| // Tracked by https://github.com/dotnet/roslyn/issues/78830 : diagnostic quality, consider suppressing parsing diagnostics on extension parameters beyond the first one |
There was a problem hiding this comment.
📝 I don't this will be that useful
| return new MethodGroupResolution(resultSymbol, LookupResultKind.Viable, diagnostics.ToReadOnly()); | ||
| } | ||
|
|
||
| static void initActualArguments(BoundExpression left, AnalyzedArguments? analyzedArguments, [NotNull] ref AnalyzedArguments? actualMethodArguments) |
There was a problem hiding this comment.
I realized that the "Actual" probably refers to the actualMethodArguments. Both feel confusing, but I guess this is a "pre-existing condition".
| if (methodResult.HasAnyApplicableMethod && propertyResult.HasAnyApplicableMember) | ||
| { | ||
| var firstMethod = methodResult.OverloadResolutionResult?.BestResult.Member ?? methodResult.MethodGroup.Methods[0]; | ||
| var firstProperty = propertyResult.BestResult.Member; |
There was a problem hiding this comment.
Good catch. This was broken. BestResult can only be accessed if the overload resolution has succeeded (ie. there's a single result that's still considered applicable, either in normal or expanded form, after overload resolution completes)
| } | ||
|
|
||
| ExtendedErrorTypeSymbol resultSymbol = new ExtendedErrorTypeSymbol(containingSymbol: null, symbols, LookupResultKind.OverloadResolutionFailure, errorInfo, arity); | ||
| return new MethodGroupResolution(resultSymbol, LookupResultKind.Viable, diagnostics.ToReadOnly()); |
There was a problem hiding this comment.
Made small adjustment to clarify.
It's meant to flow the kind from the LookupResult, but we can only get here when it's Viable (we have a check for IsMultiViable earlier in ResolveExtensions)
| if (Format.TypeQualificationStyle == SymbolDisplayTypeQualificationStyle.NameAndContainingTypes || | ||
| Format.TypeQualificationStyle == SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces) | ||
| Format.TypeQualificationStyle == SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces || | ||
| (Format.TypeQualificationStyle == SymbolDisplayTypeQualificationStyle.NameOnly && symbol.IsExtension)) |
There was a problem hiding this comment.
(Format.TypeQualificationStyle == SymbolDisplayTypeQualificationStyle.NameOnly && symbol.IsExtension)
I understand that this is probably gives us desired behavior for specific scenarios, but the condition itself is confusing. Especially for someone who doesn't know that we enumerated all possible SymbolDisplayTypeQualificationStyle values that are defined at the moment. I think the condition will have more sense and will be more robust to future changes to the SymbolDisplayTypeQualificationStyle enum if we simply use symbol.IsExtension as the condition. #Closed
| Diagnostic(ErrorCode.ERR_ExtensionResolutionFailed, "C.M").WithArguments("C", "M").WithLocation(1, 22)); | ||
| Diagnostic(ErrorCode.ERR_AmbigExtension, "C.M").WithArguments("E3.extension(C).M()", "E2.extension(C).M").WithLocation(1, 22)); | ||
|
|
||
| string expected(int index) |
| DiagnosticInfo errorInfo; | ||
| if (methodResult.HasAnyApplicableMethod && propertyResult.HasAnyApplicableMember) | ||
| { | ||
| var firstMethod = methodResult.OverloadResolutionResult?.BestResult.Member ?? methodResult.MethodGroup.Methods[0]; |
There was a problem hiding this comment.
This thread was marked as resolved, but it is still not obvious to me why we assume that methodResult.MethodGroup.Methods[0] is an applicable method.
Is there a test that takes this code path?
There was a problem hiding this comment.
All the methods in that method group are as applicable as it gets (they meet lookup criteria and are compatible/substitutable given the receiver type).
The scenario where we don't have an overload resolution result of methods is when there are no arguments. For example var x = object.M; or System.Action a = object.M;.
This reporting path is tested by ReportDiagnostics_23, ReportDiagnostics_24 and ReportDiagnostics_25.
In such scenarios, we start with a lookup result (which ensures we match name and arity requirements), then resolveMethods prunes methods that are not compatible with the receiver (using ReduceExtensionMember), instead of running normal overload resolution. We then form a MethodGroupResolution that is considered to have applicable methods (ie. its HasAnyApplicableMethod is true), since there's not possible overload resolution we could run.
// MethodGroupResolution criteria that lets us get here
public bool HasAnyApplicableMethod
{
get
{
return (this.MethodGroup != null) &&
(this.ResultKind == LookupResultKind.Viable) &&
((this.OverloadResolutionResult == null) || this.OverloadResolutionResult.HasAnyApplicableMember);
}
}
AlekseyTs
left a comment
There was a problem hiding this comment.
Done with review pass (commit 1)
|
@dotnet/roslyn-compiler for second review. Thanks |
| return result.BestResult.Member; | ||
| } | ||
|
|
||
| return result.ResultsBuilder.First(r => r.Result.Kind == MemberResolutionKind.Worse).Member; |
There was a problem hiding this comment.
return result.ResultsBuilder.First(r => r.Result.Kind == MemberResolutionKind.Worse).Member;
This looks fragile because the logic doesn't look equivalent to OverloadResolutionResult<TMember>.HasAnyApplicableMember. I suggest adding an OverloadResolutionResult<TMember>.FirstApplicableMember API next to OverloadResolutionResult<TMember>.HasAnyApplicableMember with shared logic for picking an applicable member, and using it here. That should help with keeping consistent behavior now and in the future. #Closed
There was a problem hiding this comment.
Perhaps that API doesn't have to return the first applicable candidate. It might return the "best" applicable candidate. The main point is that there should be an obvious guarantee that we get something when HasAnyApplicableMember is true.
There was a problem hiding this comment.
I'm okay to move the logic closer to HasAnyApplicableMember, but the distinction that is left here is Worse vs. Worst and it is intentional.
Because the result had HasAnyApplicableMember but not Succeeded, we know there's at least one Worse in the set. Worst is worse than Worse
| { | ||
| initActualArguments(left, analyzedArguments, ref actualMethodArguments); | ||
|
|
||
| propertyResult.ReportDiagnostics(binder, expression.Location, expression, diagnostics, memberName, left, left.Syntax, actualMethodArguments, symbols, |
There was a problem hiding this comment.
This is suspicious. It looks like we are using actualReceiverArguments in resolveProperties. Also, it looks like resolveProperties guarantees that actualReceiverArguments is initialized when propertyResult is not null. We can assert the fact after resolveProperties and on entry into this function. #Closed
There was a problem hiding this comment.
Thanks. I failed to noticed that we have separate actual arguments for methods vs. properties, and used the wrong one...
| } | ||
| else | ||
| { | ||
| initActualArguments(left, analyzedArguments, ref actualMethodArguments); |
| return result.ResultsBuilder.First(r => r.Result.Kind == MemberResolutionKind.Worse).Member; | ||
| } | ||
|
|
||
| static void initActualArguments(BoundExpression left, AnalyzedArguments? analyzedArguments, [NotNull] ref AnalyzedArguments? actualMethodArguments) |
|
Done with review pass (commit 3) |
| LookupResult lookupResult, | ||
| AnalyzedArguments? analyzedArguments, | ||
| ref AnalyzedArguments? actualMethodArguments, | ||
| ref AnalyzedArguments? actualReceiverArguments, |
| LookupResult lookupResult, | ||
| AnalyzedArguments? analyzedArguments, | ||
| ref AnalyzedArguments? actualMethodArguments, | ||
| ref AnalyzedArguments? actualReceiverArguments, |
| string memberName, | ||
| int arity, | ||
| LookupResult lookupResult, | ||
| AnalyzedArguments? analyzedArguments, |
| int arity, | ||
| LookupResult lookupResult, | ||
| AnalyzedArguments? analyzedArguments, | ||
| ref AnalyzedArguments? actualMethodArguments, |
| } | ||
|
|
||
| // We prefer Worse over Worst candidates | ||
| return ResultsBuilder.First(r => r.Result.Kind == MemberResolutionKind.Worse).Member; |
|
Done with review pass (commit 4) |
AlekseyTs
left a comment
There was a problem hiding this comment.
LGTM (commit 6), assuming CI is passing
* upstream/main: (332 commits) Cache lambdas in analyzer driver (dotnet#80759) Add information for NuGet package version 4.14 (dotnet#80870) Add missing search keywords to VB Advanced options page Fix IDE0031 false positive when preprocessor directives are used in if statements (dotnet#80878) Use core compiler on netfx hosts with toolset package (dotnet#80631) Make string concat assert more precise (dotnet#80619) Extensions: address some diagnostic quality issues (dotnet#80827) Add note on traversal order for bound nodes (dotnet#80872) Ensure that locals at the top level of a constructor have the same safe-context as parameters (dotnet#80807) Fix handling of SymbolDisplayCompilerInternalOptions.UseArityForGenericTypes option for non-native symbol implementations (dotnet#80826) Update src/Analyzers/CSharp/Tests/UseCollectionInitializer/UseCollectionInitializerTests.cs Add IsValidContainingStatement check to prevent collection initializers in using declarations Add back old DocumentSpan constructor (dotnet#80864) Add tests verifying pointer types in type parameters require unsafe context (dotnet#80776) Add regression test for Interlocked.Exchange with nullable types (dotnet#80796) Add regression test for ParseAttributeArgumentList with invalid input (fixes dotnet#8699) (dotnet#80705) Add regression test for compiler crash with syntax error in indexer declaration (dotnet#80772) Add runtime NullReferenceException validation to foreach null iteration tests (dotnet#80839) Update MicrosoftBuildTasksCoreVersionForMetrics to 17.11.48 (dotnet#80812) Mark CS4009 error as a "build only" error. (dotnet#80698) ...
Addresses parts of #78830
extension(int)instead ofE.extension(int))Relates to test plan #76130