File: Symbols\UserDefinedOperatorErrorTests.cs
Web Access
Project: src\src\Compilers\CSharp\Test\Symbol\Microsoft.CodeAnalysis.CSharp.Symbol.UnitTests.csproj (Microsoft.CodeAnalysis.CSharp.Symbol.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#nullable disable
 
using Xunit;
 
namespace Microsoft.CodeAnalysis.CSharp.UnitTests
{
    public partial class CompilationErrorTests : CompilingTestBase
    {
        [Fact]
        public void UserDefinedOperatorCollisionErrors()
        {
            // DELIBERATE SPEC VIOLATION: 
            //
            // Though the spec does not say so, the native and Roslyn compilers both enforce
            // signature uniqueness rules as though the user-defined operators were declared
            // in source as their underlying "op_whatever" methods. With one exception:
            // user-defined conversion operators are allowed to overload on return type.
            //
            // See the tests below for the details.
 
            var text =
@"
class C
{
    // These two collide by signature
    public static C operator + (C c1, C c2) { return c1; }
    public static C op_Addition(C c1, C c2) { return c1; } 
 
    // But this one does not.
    public static int op_Addition(int c1, int c2) { return c2; }
    
    // These two collide by name; the error is reported on the latter.
    public static C operator - (C c1, C c2) { return c1; }
    public int op_Subtraction;
 
    // These two collide by name; the error is reported on the latter.
    public int op_Division;
    public static C operator / (C c1, C c2) { return c1; }
 
    // These two collide by name; the error is reported on the operator
    // regardless of which one comes first.
    public static C operator * (C c1, C c2) { return c1; }
    private class op_Multiply {} 
 
    // These two collide because they have different return types but
    // identical parameters types. The behavior is that the error
    // given says that they collide because of the name op_Modulus,
    // rather than there being a custom error message as there is for
    // the following scenario.
    public static C operator % (C c1, C c2) { return c1; }
    public static int operator % (C c1, C c2) { return 0; }
 
    // These collide because operators do not consider whether they
    // are implicit or explicit, only what the target and source types are.
    public static implicit operator string(C c) { return null; }
    public static explicit operator string(C c) { return null; }
}
class D
{
    // These do *not* collide even though they have identical names
    // and differ in return type.
    public static implicit operator string(D d) { return null; }
    public static implicit operator int(D d) { return 0; }
}
class E
{
    // However, these *do* collide because one of them is a method
    // and the other is treated as a method for the purpose of finding
    // a collision. The fact that the return types differ is relevant.
    public static implicit operator string(E e) { return null; }
    public static int op_Implicit(E e) { return 0; }
    // But this one does not collide, even though *as a conversion* 
    // the two conversions would collide:
    public static string op_Explicit(E e) { return null; }
}
class F
{
    // These do collide, same as above. Note that the error is
    // reported on the latter.
    public static int op_Implicit(F f) { return 0; }
    public static implicit operator string(F f) { return null; }
}
class G
{
    // User-defined conversions collide with other members.
    public static implicit operator string(G g) { return null; }
    public int op_Implicit { get; set; }
}
class H
{
    // User-defined conversions collide with nested types
    public static implicit operator string(H h) { return null; }
    private class op_Implicit {}
}
";
            var comp = CreateCompilation(text);
            comp.VerifyDiagnostics(
                // (6,21): error CS0111: Type 'C' already defines a member called 'op_Addition' with the same parameter types
                //     public static C op_Addition(C c1, C c2) { return c1; } 
                Diagnostic(ErrorCode.ERR_MemberAlreadyExists, "op_Addition").WithArguments("op_Addition", "C"),
                // (13,16): error CS0102: The type 'C' already contains a definition for 'op_Subtraction'
                //     public int op_Subtraction;
                Diagnostic(ErrorCode.ERR_DuplicateNameInClass, "op_Subtraction").WithArguments("C", "op_Subtraction"),
                // (17,30): error CS0102: The type 'C' already contains a definition for 'op_Division'
                //     public static C operator / (C c1, C c2) { return c1; }
                Diagnostic(ErrorCode.ERR_DuplicateNameInClass, "/").WithArguments("C", "op_Division"),
                // (21,30): error CS0102: The type 'C' already contains a definition for 'op_Multiply'
                //     public static C operator * (C c1, C c2) { return c1; }
                Diagnostic(ErrorCode.ERR_DuplicateNameInClass, "*").WithArguments("C", "op_Multiply"),
                // (30,32): error CS0111: Type 'C' already defines a member called 'op_Modulus' with the same parameter types
                //     public static int operator % (C c1, C c2) { return 0; }
                Diagnostic(ErrorCode.ERR_MemberAlreadyExists, "%").WithArguments("op_Modulus", "C"),
                // (35,37): error CS0557: Duplicate user-defined conversion in type 'C'
                //     public static explicit operator string(C c) { return null; }
                Diagnostic(ErrorCode.ERR_DuplicateConversionInClass, "string").WithArguments("C"),
                // (50,23): error CS0111: Type 'E' already defines a member called 'op_Implicit' with the same parameter types
                //     public static int op_Implicit(E e) { return 0; }
                Diagnostic(ErrorCode.ERR_MemberAlreadyExists, "op_Implicit").WithArguments("op_Implicit", "E"),
                // (60,37): error CS0111: Type 'F' already defines a member called 'op_Implicit' with the same parameter types
                //     public static implicit operator string(F f) { return null; }
                Diagnostic(ErrorCode.ERR_MemberAlreadyExists, "string").WithArguments("op_Implicit", "F"),
                // (66,16): error CS0102: The type 'G' already contains a definition for 'op_Implicit'
                //     public int op_Implicit { get; set; }
                Diagnostic(ErrorCode.ERR_DuplicateNameInClass, "op_Implicit").WithArguments("G", "op_Implicit"),
                // (71,37): error CS0102: The type 'H' already contains a definition for 'op_Implicit'
                //     public static implicit operator string(H h) { return null; }
                Diagnostic(ErrorCode.ERR_DuplicateNameInClass, "string").WithArguments("H", "op_Implicit"),
                // (13,16): warning CS0649: Field 'C.op_Subtraction' is never assigned to, and will always have its default value 0
                //     public int op_Subtraction;
                Diagnostic(ErrorCode.WRN_UnassignedInternalField, "op_Subtraction").WithArguments("C.op_Subtraction", "0"),
                // (16,16): warning CS0649: Field 'C.op_Division' is never assigned to, and will always have its default value 0
                //     public int op_Division;
                Diagnostic(ErrorCode.WRN_UnassignedInternalField, "op_Division").WithArguments("C.op_Division", "0")
                );
        }
 
        [Fact]
        public void UserDefinedOperatorBodyErrors()
        {
            // User-defined operators have the same behavior as other methods;
            // for example, they must return a value compatible with their declared
            // return type and have an unreachable end point.
 
            var text =
@"
class C
{
    public class D {}
    public static D operator + (C c1, C c2) { return c1; }
    public static explicit operator int (C c) { }
}
";
            var comp = CreateCompilation(text);
            comp.VerifyDiagnostics(
// (5,54): error CS0029: Cannot implicitly convert type 'C' to 'C.D'
//     public static D operator + (C c1, C c2) { return c1; }
Diagnostic(ErrorCode.ERR_NoImplicitConv, "c1").WithArguments("C", "C.D"),
 
// (6,37): error CS0161: 'C.explicit operator int(C)': not all code paths return a value
//     public static explicit operator int (C c) { }
Diagnostic(ErrorCode.ERR_ReturnExpected, "int").WithArguments("C.explicit operator int(C)")
                );
        }
 
        [Fact]
        public void UserDefinedOperatorModifierErrors()
        {
            var text =
@"
partial class C
{
    partial public static int operator + (C c1, C c2) { return 0; }
    abstract public int operator - (C c1, C c2) { return 0; }
    sealed public int operator << (C c1, int c2) { return 0; }
    new public static int operator >> (C c1, int c2) { return 0; }
    readonly public static int operator * (C c1, C c2) { return 0; }
    volatile public static int operator % (C c1, C c2) { return 0; }
    virtual public int operator - (C c1) { return 0; }
    override public int operator ~ (C c1) { return 0; }
    public public public static int operator & (C c1, C c2) { return 0; }
    extern static public int operator ^ (C c1, C c2) { return 1; }
    static public int operator + (C c1);
    new public static int operator >>> (C c1, int c2) { return 0; }
}
";
 
            // UNDONE: May be unsafe
 
            // UNDONE: Native compiler squiggles "operator +". Roslyn squiggles "operator". But perhaps
            // UNDONE: the better thing to actually squiggle is the offending token.
 
            var comp = CreateCompilation(text);
            comp.VerifyDiagnostics(
                // (4,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method return type.
                //     partial public static int operator + (C c1, C c2) { return 0; }
                Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(4, 5),
                // (4,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method return type.
                //     partial public static int operator + (C c1, C c2) { return 0; }
                Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(4, 5),
                // (5,34): error CS0106: The modifier 'abstract' is not valid for this item
                //     abstract public int operator - (C c1, C c2) { return 0; }
                Diagnostic(ErrorCode.ERR_BadMemberFlag, "-").WithArguments("abstract").WithLocation(5, 34),
                // (5,34): error CS0558: User-defined operator 'C.operator -(C, C)' must be declared static and public
                //     abstract public int operator - (C c1, C c2) { return 0; }
                Diagnostic(ErrorCode.ERR_OperatorsMustBeStatic, "-").WithArguments("C.operator -(C, C)").WithLocation(5, 34),
                // (6,32): error CS0106: The modifier 'sealed' is not valid for this item
                //     sealed public int operator << (C c1, int c2) { return 0; }
                Diagnostic(ErrorCode.ERR_BadMemberFlag, "<<").WithArguments("sealed").WithLocation(6, 32),
                // (6,32): error CS0558: User-defined operator 'C.operator <<(C, int)' must be declared static and public
                //     sealed public int operator << (C c1, int c2) { return 0; }
                Diagnostic(ErrorCode.ERR_OperatorsMustBeStatic, "<<").WithArguments("C.operator <<(C, int)").WithLocation(6, 32),
                // (7,36): error CS0106: The modifier 'new' is not valid for this item
                //     new public static int operator >> (C c1, int c2) { return 0; }
                Diagnostic(ErrorCode.ERR_BadMemberFlag, ">>").WithArguments("new").WithLocation(7, 36),
                // (8,41): error CS0106: The modifier 'readonly' is not valid for this item
                //     readonly public static int operator * (C c1, C c2) { return 0; }
                Diagnostic(ErrorCode.ERR_BadMemberFlag, "*").WithArguments("readonly").WithLocation(8, 41),
                // (9,41): error CS0106: The modifier 'volatile' is not valid for this item
                //     volatile public static int operator % (C c1, C c2) { return 0; }
                Diagnostic(ErrorCode.ERR_BadMemberFlag, "%").WithArguments("volatile").WithLocation(9, 41),
                // (10,33): error CS0106: The modifier 'virtual' is not valid for this item
                //     virtual public int operator - (C c1) { return 0; }
                Diagnostic(ErrorCode.ERR_BadMemberFlag, "-").WithArguments("virtual").WithLocation(10, 33),
                // (10,33): error CS0558: User-defined operator 'C.operator -(C)' must be declared static and public
                //     virtual public int operator - (C c1) { return 0; }
                Diagnostic(ErrorCode.ERR_OperatorsMustBeStatic, "-").WithArguments("C.operator -(C)").WithLocation(10, 33),
                // (11,34): error CS0106: The modifier 'override' is not valid for this item
                //     override public int operator ~ (C c1) { return 0; }
                Diagnostic(ErrorCode.ERR_BadMemberFlag, "~").WithArguments("override").WithLocation(11, 34),
                // (11,34): error CS0558: User-defined operator 'C.operator ~(C)' must be declared static and public
                //     override public int operator ~ (C c1) { return 0; }
                Diagnostic(ErrorCode.ERR_OperatorsMustBeStatic, "~").WithArguments("C.operator ~(C)").WithLocation(11, 34),
                // (12,12): error CS1004: Duplicate 'public' modifier
                //     public public public static int operator & (C c1, C c2) { return 0; }
                Diagnostic(ErrorCode.ERR_DuplicateModifier, "public").WithArguments("public").WithLocation(12, 12),
                // (13,39): error CS0179: 'C.operator ^(C, C)' cannot be extern and declare a body
                //     extern static public int operator ^ (C c1, C c2) { return 1; }
                Diagnostic(ErrorCode.ERR_ExternHasBody, "^").WithArguments("C.operator ^(C, C)").WithLocation(13, 39),
                // (14,32): error CS0501: 'C.operator +(C)' must declare a body because it is not marked abstract, extern, or partial
                //     static public int operator + (C c1);
                Diagnostic(ErrorCode.ERR_ConcreteMissingBody, "+").WithArguments("C.operator +(C)").WithLocation(14, 32),
                // (15,36): error CS0106: The modifier 'new' is not valid for this item
                //     new public static int operator >>> (C c1, int c2) { return 0; }
                Diagnostic(ErrorCode.ERR_BadMemberFlag, ">>>").WithArguments("new").WithLocation(15, 36)
                );
        }
 
        [Fact]
        public void UserDefinedOperatorAccessibilityErrors()
        {
            var text =
@"
public class C
{
    private class D {}
 
    public static D operator + (C c1, C c2) { return null; }
    public static int operator - (C c, D d) { return 0; }
    public static explicit operator C(D d) { return null; }
}
";
            // UNDONE: Roslyn squiggles just the "operator"; Native compiler squiggles the "operator +".
            // UNDONE: Consider matching the native compiler behavior, or, even better, squiggle the
            // UNDONE: offending type.
 
            var comp = CreateCompilation(text);
            comp.VerifyDiagnostics(
// (6,30): error CS0056: Inconsistent accessibility: return type 'C.D' is less accessible than operator 'C.operator +(C, C)'
//     public static D operator + (C c1, C c2) { return null; }
Diagnostic(ErrorCode.ERR_BadVisOpReturn, "+").WithArguments("C.operator +(C, C)", "C.D"),
 
// (7,32): error CS0057: Inconsistent accessibility: parameter type 'C.D' is less accessible than operator 'C.operator -(C, C.D)'
//     public static int operator - (C c, D d) { return 0; }
Diagnostic(ErrorCode.ERR_BadVisOpParam, "-").WithArguments("C.operator -(C, C.D)", "C.D"),
 
// (8,37): error CS0057: Inconsistent accessibility: parameter type 'C.D' is less accessible than operator 'C.explicit operator C(C.D)'
//     public static explicit operator C(D d) { return null; }
Diagnostic(ErrorCode.ERR_BadVisOpParam, "C").WithArguments("C.explicit operator C(C.D)", "C.D")
                );
        }
 
        [Fact]
        public void UserDefinedOperatorConstraintViolationErrors()
        {
            var text =
@"
public class C
{
    public class D<T> where T : class {}
    public static D<int> operator + (C c1, C c2) { return null; }
    public static int operator - (C c, D<double> d) { return 0; }
    public static explicit operator C(D<decimal> d) { return null; }
}
";
            // UNDONE: The squiggles are not in the ideal places here. The native compiler squiggles the entire
            // UNDONE: name of the operator in all cases, which is bad. Roslyn squiggles the "operator"
            // UNDONE: token if the return type is bad and the parameter name if the parameter type is 
            // UNDONE: bad. This seems no better; surely the right thing to squiggle is the offending type.
 
            var comp = CreateCompilation(text);
            comp.VerifyDiagnostics(
// (5,35): error CS0452: The type 'int' must be a reference type in order to use it as parameter 'T' in the generic type or method 'C.D<T>'
//     public static D<int> operator + (C c1, C c2) { return null; }
Diagnostic(ErrorCode.ERR_RefConstraintNotSatisfied, "+").WithArguments("C.D<T>", "T", "int"),
 
// (6,50): error CS0452: The type 'double' must be a reference type in order to use it as parameter 'T' in the generic type or method 'C.D<T>'
//     public static int operator - (C c, D<double> d) { return 0; }
Diagnostic(ErrorCode.ERR_RefConstraintNotSatisfied, "d").WithArguments("C.D<T>", "T", "double"),
 
// (7,50): error CS0452: The type 'decimal' must be a reference type in order to use it as parameter 'T' in the generic type or method 'C.D<T>'
//     public static explicit operator C(D<decimal> d) { return null; }
Diagnostic(ErrorCode.ERR_RefConstraintNotSatisfied, "d").WithArguments("C.D<T>", "T", "decimal")
                );
        }
    }
}