Skip to content

Conversation

@jcouv
Copy link
Member

@jcouv jcouv commented Oct 28, 2025

Addresses parts of #78830

  • Report what operator methods conflict

Relates to test plan #76130

@jcouv jcouv self-assigned this Oct 28, 2025
@jcouv jcouv added Area-Compilers Feature - Extension Everything The extension everything feature labels Oct 28, 2025
@jcouv jcouv force-pushed the extensions-diagnostics2 branch 2 times, most recently from ebe80f9 to 928f3b3 Compare October 28, 2025 07:50
@jcouv jcouv force-pushed the extensions-diagnostics2 branch from 928f3b3 to 7f4d084 Compare October 28, 2025 16:59
@jcouv jcouv marked this pull request as ready for review October 28, 2025 18:22
@jcouv jcouv requested a review from a team as a code owner October 28, 2025 18:22
<value>The extension resolution is ambiguous between the following members: '{0}' and '{1}'</value>
</data>
<data name="ERR_SingleInapplicableBinaryOperator" xml:space="preserve">
<value>Operator cannot be applied on operands of type '{0}' and '{1}'. The closest inapplicable candidate is '{2}'</value>
Copy link
Contributor

@AlekseyTs AlekseyTs Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on

"to"? #Closed

<value>Operator cannot be applied on operands of type '{0}' and '{1}'. The closest inapplicable candidate is '{2}'</value>
</data>
<data name="ERR_SingleInapplicableUnaryOperator" xml:space="preserve">
<value>Operator cannot be applied on operand of type '{0}'. The closest inapplicable candidate is '{1}'</value>
Copy link
Contributor

@AlekseyTs AlekseyTs Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on

"to"? #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Oct 29, 2025

            ref OperatorResolutionForReporting operatorResolutionForReporting)

Consider declaring this parameter before all out parameters #Closed


Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs:587 in 7f4d084. [](commit_id = 7f4d084, deletion_comment = False)

/// </summary>
private struct OperatorResolutionForReporting
{
private object? _nonExtensionResult;
Copy link
Contributor

@AlekseyTs AlekseyTs Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_nonExtensionResult

It would be good to document what types can be stored in these fields #Closed

/// <summary>
/// Follows a very simplified version of OverloadResolutionResult.ReportDiagnostics which can be expanded in the future if needed.
/// </summary>
internal bool TryReportDiagnostics(SyntaxNode node, BindingDiagnosticBag diagnostics, Binder binder, object leftDisplay, object? rightDisplay)
Copy link
Contributor

@AlekseyTs AlekseyTs Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

, BindingDiagnosticBag diagnostics

I think we usually place diagnostics parameter at the end. #Closed

{
if (res.Signature.Method is null)
{
// Skip build-in operators
Copy link
Contributor

@AlekseyTs AlekseyTs Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build-in

"built-in"? #Closed

{
if (res.Signature.Method is null)
{
// Skip build-in operators
Copy link
Contributor

@AlekseyTs AlekseyTs Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build-in

"built-in"? #Closed

return false;
}

static void populateResults(ArrayBuilder<(MethodSymbol?, OperatorAnalysisResultKind)> results, object? extensionResult)
Copy link
Contributor

@AlekseyTs AlekseyTs Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extensionResult

Is this necessarily an extension result? #Closed

{
return kind switch
{
MemberResolutionKind.ApplicableInExpandedForm => OperatorAnalysisResultKind.Applicable,
Copy link
Contributor

@AlekseyTs AlekseyTs Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MemberResolutionKind.ApplicableInExpandedForm => OperatorAnalysisResultKind.Applicable,

Is this code path reachable? #Closed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this code path reachable?

I guess this shouldn't be a concern for this component.

};
}

static void assertNone(ArrayBuilder<(MethodSymbol? member, OperatorAnalysisResultKind resultKind)> results, OperatorAnalysisResultKind kind)
Copy link
Contributor

@AlekseyTs AlekseyTs Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assertNone

Consider adding a conditional DEBUG attribute #Closed


if (results.Any(m => m.resultKind == OperatorAnalysisResultKind.Applicable))
{
return false;
Copy link
Contributor

@AlekseyTs AlekseyTs Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return false;

Is this code path reachable? #Closed

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, for instance ReportDiagnostics_Binary_03 where we get here with only built-in operators being applicable. tryGetTwoBest discards built-in operators since we don't have a member to report them.

}

assertNone(results, OperatorAnalysisResultKind.Applicable);
assertNone(results, OperatorAnalysisResultKind.Worse);
Copy link
Contributor

@AlekseyTs AlekseyTs Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assertNone(results, OperatorAnalysisResultKind.Worse);

It is not obvious to me why we can make this assumption #Closed

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went back and forth on this. I'll put fallback handling here instead, even though I don't think it's currently reachable due to how we produce operator results. It'll be more robust

@jcouv jcouv requested review from AlekseyTs and jjonescz October 29, 2025 20:15
@AlekseyTs
Copy link
Contributor

AlekseyTs commented Oct 29, 2025

            if (!operatorResolutionForReporting.SaveResult(overloadResolutionResult, isExtension))

It feels counterintuitive to do that when inPlaceResult is set. When it is set, we either succeeded or reported errors and will not attempt to report more diagnostics. #Closed


Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs:556 in 6a9164b. [](commit_id = 6a9164b, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Oct 29, 2025

                    if (operatorResolutionForReporting.SaveResult(result, isExtension: true))

It feels counterintuitive to do that when result of BinaryOperatorAnalyzeOverloadResolutionResult HasValue #Closed


Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs:632 in 6a9164b. [](commit_id = 6a9164b, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Oct 29, 2025

                    operatorResolutionForReporting.Free();

This looks suspicious and unintuitive. It doesn't look like we tried to report anything from the object. Also it looks like the caller will try to Free it as well. Perhaps it is safe to do, but still looks very strange. #Closed


Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs:1241 in 6a9164b. [](commit_id = 6a9164b, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Oct 29, 2025

                if (operatorResolutionForReporting.SaveResult(result, isExtension: true))

Similar comment for a situation when result of BinaryOperatorAnalyzeOverloadResolutionResult HasValue #Closed


Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs:2157 in 6a9164b. [](commit_id = 6a9164b, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Oct 29, 2025

        if (!operatorResolutionForReporting.SaveResult(result, isExtension: false))

Similar comment for a situation when result of BinaryOperatorAnalyzeOverloadResolutionResult HasValue #Closed


Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs:2199 in 6a9164b. [](commit_id = 6a9164b, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 2)

@jcouv
Copy link
Member Author

jcouv commented Oct 30, 2025

            if (!operatorResolutionForReporting.SaveResult(overloadResolutionResult, isExtension))

We collect information for reporting systematically. The information we collect may or may not end up being needed to report more diagnostics.


In reply to: 3464049874


Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs:556 in 6a9164b. [](commit_id = 6a9164b, deletion_comment = False)

@jcouv
Copy link
Member Author

jcouv commented Oct 30, 2025

                    operatorResolutionForReporting.Free();

Thanks, that was wrong


In reply to: 3464343239


Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs:1241 in 6a9164b. [](commit_id = 6a9164b, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 3)

@jcouv
Copy link
Member Author

jcouv commented Oct 30, 2025

                if (operatorResolutionForReporting.SaveResult(result, isExtension: true))

Same response provided elsewhere applies to remaining comments similar to this one


In reply to: 3464425102


Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs:2157 in 6a9164b. [](commit_id = 6a9164b, deletion_comment = False)

@jcouv jcouv requested a review from AlekseyTs October 30, 2025 21:52
@AlekseyTs
Copy link
Contributor

AlekseyTs commented Oct 31, 2025

        BoundExpression bindIncrementOperator(ExpressionSyntax node, ExpressionSyntax operandSyntax, SyntaxToken operatorToken, BindingDiagnosticBag diagnostics, ref OperatorResolutionForReporting operatorResolutionForReporting)

We usually keep this parameter last #Resolved


Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs:3279 in 99a248c. [](commit_id = 99a248c, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Oct 31, 2025

        ref OperatorResolutionForReporting operatorResolutionForReporting)

Consider placing this parameter before out parameters #Resolved


Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs:2440 in 99a248c. [](commit_id = 99a248c, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Oct 31, 2025

        ref OperatorResolutionForReporting operatorResolutionForReporting)

Consider placing this parameter before out parameters #Resolved


Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs:2333 in 99a248c. [](commit_id = 99a248c, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Oct 31, 2025

        ref OperatorResolutionForReporting operatorResolutionForReporting)

Consider placing this parameter before out parameters #Resolved


Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs:2291 in 99a248c. [](commit_id = 99a248c, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Oct 31, 2025

        ref OperatorResolutionForReporting operatorResolutionForReporting)

Consider placing this parameter before out parameters #Resolved


Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs:2189 in 99a248c. [](commit_id = 99a248c, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Oct 31, 2025

        ref OperatorResolutionForReporting operatorResolutionForReporting)

Consider placing this parameter before out parameters #Resolved


Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs:2129 in 99a248c. [](commit_id = 99a248c, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Oct 31, 2025

        ref OperatorResolutionForReporting operatorResolutionForReporting)

Consider placing this parameter before out parameters #Resolved


Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs:2088 in 99a248c. [](commit_id = 99a248c, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Oct 31, 2025

        ref OperatorResolutionForReporting operatorResolutionForReporting)

Consider placing this parameter before out parameters #Resolved


Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs:2070 in 99a248c. [](commit_id = 99a248c, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Oct 31, 2025

        BoundExpression bindConditionalLogicalOperator(BinaryExpressionSyntax node, BoundExpression left, BoundExpression right, BindingDiagnosticBag diagnostics, ref OperatorResolutionForReporting operatorResolutionForReporting)

Diagnostics usually the last parameter, that is when there are no out parameters #Resolved


Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs:1439 in 99a248c. [](commit_id = 99a248c, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Oct 31, 2025

        out BinaryOperatorSignature resultSignature, out BinaryOperatorAnalysisResult best, ref OperatorResolutionForReporting operatorResolutionForReporting)

Consider placing this parameter before out parameters #Resolved


Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs:1140 in 99a248c. [](commit_id = 99a248c, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Oct 31, 2025

        BoundExpression bindCompoundAssignment(AssignmentExpressionSyntax node, BindingDiagnosticBag diagnostics, ref OperatorResolutionForReporting operatorResolutionForReporting)

Diagnostics usually the last parameter #Resolved


Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs:31 in 99a248c. [](commit_id = 99a248c, deletion_comment = False)

Copy link
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (commit 5), assuming CI is passing

@jcouv
Copy link
Member Author

jcouv commented Nov 2, 2025

@jjonescz @dotnet/roslyn-compiler for second review. Thanks

private object? _extensionResult;

[Conditional("DEBUG")]
private void AssertInvariant()
Copy link
Member

@jjonescz jjonescz Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private void AssertInvariant()
private readonly void AssertInvariant()
``` #Resolved

/// <summary>
/// Follows a very simplified version of OverloadResolutionResult.ReportDiagnostics which can be expanded in the future if needed.
/// </summary>
internal bool TryReportDiagnostics(SyntaxNode node, Binder binder, object leftDisplay, object? rightDisplay, BindingDiagnosticBag diagnostics)
Copy link
Member

@jjonescz jjonescz Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
internal bool TryReportDiagnostics(SyntaxNode node, Binder binder, object leftDisplay, object? rightDisplay, BindingDiagnosticBag diagnostics)
internal readonly bool TryReportDiagnostics(SyntaxNode node, Binder binder, object leftDisplay, object? rightDisplay, BindingDiagnosticBag diagnostics)
``` #Resolved

Debug.Assert(results.All(r => r.resultKind == OperatorAnalysisResultKind.Inapplicable));

// There is much room to improve diagnostics on inapplicable candidates, but for now we just report the candidate if there is a single one.
if (results.Count == 1 && results[0].member is { } inapplicableMember)
Copy link
Member

@jjonescz jjonescz Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (results.Count == 1 && results[0].member is { } inapplicableMember)
if (results is [{ member: { } inapplicableMember }])
``` #Resolved

var comp = CreateCompilation(src, targetFramework: TargetFramework.Net90);
comp.VerifyEmitDiagnostics(
// (32,17): error CS0035: Operator '-' is ambiguous on an operand of type 'I2'
// (32,17): error CS9339: Operator resolution is ambiguous between the following members:'I1.operator -(I1)' and 'I3.operator -(I3)'
Copy link
Member

@jjonescz jjonescz Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// (32,17): error CS9339: Operator resolution is ambiguous between the following members:'I1.operator -(I1)' and 'I3.operator -(I3)'
// (32,17): error CS9339: Operator resolution is ambiguous between the following members: 'I1.operator -(I1)' and 'I3.operator -(I3)'
``` #Resolved

""";

var comp = CreateCompilation(src, options: TestOptions.DebugExe);
#if DEBUG
Copy link
Member

@jjonescz jjonescz Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are the diagnostics subtly different between Debug and Release? #ByDesign

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have GetMembers() and GetMembersUnordered().
We use GetMembersUnordered() in NamedTypeSymbol.GetExtensionMembers() as we do in NamedTypeSymbol.DoGetExtensionMethods().

I explored making the diagnostics deterministic too (in #80552 ) but we ended up deciding against it.

For some reason, GetMembersUnordered() only swaps the first and last elements, instead of a more aggressive de-ordering, so our usage of GetMembersUnordered() isn't apparent is as many tests as you'd think.

#if DEBUG
comp.VerifyEmitDiagnostics(
// (35,13): error CS0035: Operator '-' is ambiguous on an operand of type 'C1'
// (35,13): error CS9339: Operator resolution is ambiguous between the following members: 'Extensions2.extension(C1).operator -(C1)' and 'Extensions1.extension(C1).operator -(C1)'
Copy link
Member

@jjonescz jjonescz Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// (35,13): error CS9339: Operator resolution is ambiguous between the following members: 'Extensions2.extension(C1).operator -(C1)' and 'Extensions1.extension(C1).operator -(C1)'
// (35,13): error CS9342: Operator resolution is ambiguous between the following members: 'Extensions2.extension(C1).operator -(C1)' and 'Extensions1.extension(C1).operator -(C1)'
``` #Resolved

[Fact]
public void ReportDiagnostics_CompoundAssignment_01()
{
// inner scope has inapplicable operator, outter scope too
Copy link
Member

@jjonescz jjonescz Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// inner scope has inapplicable operator, outter scope too
// inner scope has inapplicable operator, outer scope too
``` #Resolved

@jcouv jcouv enabled auto-merge (squash) November 4, 2025 09:04
@jcouv jcouv merged commit ed83c92 into dotnet:main Nov 5, 2025
24 of 25 checks passed
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Nov 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-Compilers Feature - Extension Everything The extension everything feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants