Skip to content

Improve arity error message to prefer generic type when both generic and non-generic exist#80707

Merged
CyrusNajmabadi merged 20 commits intomainfrom
copilot/fix-inaccurate-error-message
Nov 1, 2025
Merged

Improve arity error message to prefer generic type when both generic and non-generic exist#80707
CyrusNajmabadi merged 20 commits intomainfrom
copilot/fix-inaccurate-error-message

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Oct 14, 2025

Summary

This PR fixes an inaccurate error message that occurs when a user supplies type arguments to a type name that has both generic and non-generic versions.

Problem

When attempting to use a type with the wrong number of type arguments, if both a generic and non-generic version exist with the same name, the compiler incorrectly reports an error about the non-generic version instead of the generic one.

For example, consider this code:

class MyExpression { }
class MyExpression<T> { }

class Test
{
    void M()
    {
        MyExpression<int, string> x;  // Using 2 type arguments
    }
}

Before this fix, the compiler reported:

error CS0308: The non-generic type 'MyExpression' cannot be used with type arguments

This is confusing because:

  • A generic version MyExpression<T> does exist
  • The user is clearly trying to use it with type arguments
  • The error message mentions the wrong type

After this fix, the compiler reports:

error CS0305: Using the generic type 'MyExpression<T>' requires 1 type arguments

This is much clearer because:

  • It mentions the correct generic type
  • It tells the user exactly how many type arguments are needed (1)
  • The user can clearly see they provided 2 but only 1 is expected

Implementation

The fix modifies the lookup result merging logic in LookupResult.MergeEqual(SingleLookupResult) to add special handling for WrongArity results. When both a generic type (arity > 0) and a non-generic type (arity == 0) are found with wrong arity, the logic now prefers reporting the error about the generic type.

The implementation is minimal and surgical:

  • Added a preferGenericOverNonGeneric static local function to determine which symbol to prefer
  • Modified the MergeEqual method to use this logic when both results have LookupResultKind.WrongArity
  • The logic prefers ANY generic type over non-generic types; it does not select the "closest match" by arity count

Testing

  • Added 7 comprehensive test cases in BindingTests.cs covering:

    • Generic and non-generic types with wrong arity
    • Generic types only with wrong arity
    • Non-generic types only (preserves existing behavior)
    • Correct arity (no error)
    • Multiple generic types with different arities
    • Generic type used with no type arguments when both generic and non-generic exist (binds to non-generic successfully)
    • Generic type only used with no type arguments (reports arity error)
  • Updated 1 existing test in BindingTests.cs that was validating the old behavior

  • All existing arity-related tests pass (1,293 tests)

  • No regressions found in semantic error tests

Fixes #24406

Original prompt

This section details on the original issue you should resolve

<issue_title>Inaccurate error message when type has incorrect number of type arguments</issue_title>
<issue_description>Version Used: master (22 Jan 2018; 03839fa)

Steps to Reproduce:

Try to compile the following code:

class MyExpression {}
class MyExpression<T> {}

class C
{
    MyExpression<int, int> e;
}

Expected Behavior: An error message that explains that MyExpression expects 0 or 1 type arguments, but was used with 2.

Actual Behavior:

CS0308: The non-generic type 'MyExpression' cannot be used with type arguments

This error message is not accurate: MyExpression can be used with type arguments (specifically, with one), it just can't be used with two.

The actual code where I encountered this was using System.Linq.Expressions.Expression<T>: the source code contained Expression<Data, bool> instead of Expression<Func<Data, bool>>. If the error message was clearer, I think I would have realized what the issue was sooner.</issue_description>

<agent_instructions>Copilot, see if you can improve things here. If we don't find a match, and the user has supplied type arguments, and we do have a generic and non-generic options, mention the generic type instead and say that the wrong number of type arguments were provided.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

Fixes #24406


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI changed the title [WIP] Fix inaccurate error message for type arguments Improve arity error message to prefer generic type when both generic and non-generic exist Oct 14, 2025
Copilot AI requested a review from CyrusNajmabadi October 14, 2025 15:35
Comment thread src/Compilers/CSharp/Portable/Binder/LookupResult.cs Outdated
}
}

private static bool PreferGenericOverNonGeneric(Symbol currentSymbol, Symbol newSymbol)
Copy link
Copy Markdown
Contributor

@CyrusNajmabadi CyrusNajmabadi Oct 14, 2025

Choose a reason for hiding this comment

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

@copilot make this a static local function inside MergeEqual #Closed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in commit 7b5a8c6.

[Fact]
public void TestGenericAndNonGenericType()
{
var text = @"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@copilot make all these test strings into raw string literals.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in commit a2c4d9f.

{
public class ImprovedArityErrorTests : CSharpTestBase
{
[Fact]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

add an appropriate WorkItem attribute to all these tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in commit a2c4d9f.

@CyrusNajmabadi CyrusNajmabadi marked this pull request as ready for review October 14, 2025 18:02
@CyrusNajmabadi CyrusNajmabadi requested a review from a team as a code owner October 14, 2025 18:02
@CyrusNajmabadi
Copy link
Copy Markdown
Contributor

@dotnet/roslyn-ide ptal.

Comment thread src/Compilers/CSharp/Portable/Binder/LookupResult.cs Outdated
Copy link
Copy Markdown
Member

@jcouv jcouv left a 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 14)

Copilot AI requested a review from jcouv October 24, 2025 16:25
Copy link
Copy Markdown
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

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

LGTM Thanks (commit 15)

@CyrusNajmabadi
Copy link
Copy Markdown
Contributor

@dotnet/roslyn-compiler For a second pair of eyes.

}

static bool preferGenericOverNonGeneric(Symbol firstSymbol, Symbol secondSymbol)
=> firstSymbol is NamedTypeSymbol { Arity: 0 } && secondSymbol is NamedTypeSymbol { Arity: > 0 };
Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson Oct 31, 2025

Choose a reason for hiding this comment

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

I think we should just use Symbol.GetArity() instead of pattern matching, and also test the analogous method case:

class Program
{
    static void Main()
    {
        // error CS0308: The non-generic method 'Program.M(object)' cannot be used with type arguments
        // (It seems reasonable to prefer `M<T1, T2>` in this case for the same reason as a type.)
        M<int>(default!); 
    }

    void M(object obj) { }
    T2 M<T1, T2>(T1 t) => throw null!;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

{
// When both results are WrongArity, prefer reporting errors about generic types
// over non-generic types to provide more helpful error messages
if (preferGenericOverNonGeneric(this.SingleSymbolOrDefault, result.Symbol))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Consider a name like isNonGenericVersusGeneric instead, so that the terms in the function name are in the same order as the tests we do on the parameters.

Currently the name reads as if we are trying to decide whether we prefer or not an incoming generic symbol over a non-generic symbol. But that's not really what it's doing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

_symbolList.Add(result.Symbol);
}
}
// else: existing result is preferred (generic over non-generic), keep it
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this method would be easier to understand if we just returned in the branches where we are "done" merging the results, and there wouldn't be a need to duplicate the logic to _symbolList.Add(result.Symbol);.

For example, in a branch where this result is better, just return. In a branch where this result is worse, call this.SetFrom(result) then return.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inaccurate error message when type has incorrect number of type arguments

5 participants