diff --git a/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 10.md b/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 10.md index 6e4f936da04d5..6eb3cc0e7b0f3 100644 --- a/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 10.md +++ b/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 10.md @@ -364,3 +364,41 @@ class extension { } // type may not be named "extension" class C { } // type parameter may not be named "extension" ``` +## Partial properties and events are now implicitly virtual and public + +***Introduced in Visual Studio 2022 version 17.15*** + +We have fixed [an inconsistency](https://github.com/dotnet/roslyn/issues/77346) +where partial interface properties and events would not be implicitly `virtual` and `public` unlike their non-partial equivalents. +This inconsistency is however [preserved](./Deviations%20from%20Standard.md#interface-partial-methods) for partial interface methods to avoid a larger breaking change. +Note that Visual Basic and other languages not supporting default interface members will start requiring to implement implicitly virtual `partial` interface members. + +To keep the previous behavior, explicitly mark `partial` interface members as `private` (if they don't have any accessibility modifiers) +and `sealed` (if they don't have the `private` modifier which implies `sealed`, and they don't already have modifier `virtual` or `sealed`). + +```cs +System.Console.Write(((I)new C()).P); // wrote 1 previously, writes 2 now + +partial interface I +{ + public partial int P { get; } + public partial int P => 1; // implicitly virtual now +} + +class C : I +{ + public int P => 2; // implements I.P +} +``` + +```cs +System.Console.Write(((I)new C()).P); // inaccessible previously, writes 1 now + +partial interface I +{ + partial int P { get; } // implicitly public now + partial int P => 1; +} + +class C : I; +``` diff --git a/docs/compilers/CSharp/Deviations from Standard.md b/docs/compilers/CSharp/Deviations from Standard.md index 66cee1e3a9ca5..d9a647efd1145 100644 --- a/docs/compilers/CSharp/Deviations from Standard.md +++ b/docs/compilers/CSharp/Deviations from Standard.md @@ -77,3 +77,10 @@ The compiler is free to make assumptions about the shape and behavior of well-kn It may not check for unexpected constraints, `Obsolete` attribute, or `UnmanagedCallersOnly` attribute. It may perform some optimizations based on expectations that the types/members are well-behaved. Note: the compiler should remain resilient to missing well-known types/members. + +# Interface partial methods + +Interface partial methods are implicitly non-virtual, +unlike non-partial interface methods and other interface partial member kinds, +see [a related breaking change](./Compiler%20Breaking%20Changes%20-%20DotNet%2010.md#partial-properties-and-events-are-now-implicitly-virtual-and-public) +and [LDM 2025-04-07](https://github.com/dotnet/csharplang/blob/main/meetings/2025/LDM-2025-04-07.md#breaking-change-discussion-making-partial-members-in-interfaces-virtual-andor-public). diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs b/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs index 175403675a328..aa33e331bd268 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs @@ -245,8 +245,24 @@ internal static void CheckFeatureAvailabilityForPartialEventsAndConstructors(Loc } #nullable disable - internal static DeclarationModifiers AdjustModifiersForAnInterfaceMember(DeclarationModifiers mods, bool hasBody, bool isExplicitInterfaceImplementation) + internal static DeclarationModifiers AdjustModifiersForAnInterfaceMember(DeclarationModifiers mods, bool hasBody, bool isExplicitInterfaceImplementation, bool forMethod) { + // Interface partial non-method members are implicitly public and virtual just like their non-partial counterparts. + // Interface partial methods are implicitly private and not virtual (this is a spec violation but being preserved to avoid breaks). + bool notPartialOrNewPartialBehavior = (mods & DeclarationModifiers.Partial) == 0 || !forMethod; + + if ((mods & DeclarationModifiers.AccessibilityMask) == 0) + { + if (!isExplicitInterfaceImplementation && notPartialOrNewPartialBehavior) + { + mods |= DeclarationModifiers.Public; + } + else + { + mods |= DeclarationModifiers.Private; + } + } + if (isExplicitInterfaceImplementation) { if ((mods & DeclarationModifiers.Abstract) != 0) @@ -258,11 +274,11 @@ internal static DeclarationModifiers AdjustModifiersForAnInterfaceMember(Declara { mods &= ~DeclarationModifiers.Sealed; } - else if ((mods & (DeclarationModifiers.Private | DeclarationModifiers.Partial | DeclarationModifiers.Virtual | DeclarationModifiers.Abstract)) == 0) + else if ((mods & (DeclarationModifiers.Private | DeclarationModifiers.Virtual | DeclarationModifiers.Abstract)) == 0 && notPartialOrNewPartialBehavior) { Debug.Assert(!isExplicitInterfaceImplementation); - if (hasBody || (mods & (DeclarationModifiers.Extern | DeclarationModifiers.Sealed)) != 0) + if (hasBody || (mods & (DeclarationModifiers.Extern | DeclarationModifiers.Partial | DeclarationModifiers.Sealed)) != 0) { if ((mods & DeclarationModifiers.Sealed) == 0) { @@ -279,18 +295,6 @@ internal static DeclarationModifiers AdjustModifiersForAnInterfaceMember(Declara } } - if ((mods & DeclarationModifiers.AccessibilityMask) == 0) - { - if ((mods & DeclarationModifiers.Partial) == 0 && !isExplicitInterfaceImplementation) - { - mods |= DeclarationModifiers.Public; - } - else - { - mods |= DeclarationModifiers.Private; - } - } - return mods; } diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceEventSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceEventSymbol.cs index 7487788040b59..dfcef501fc5d6 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceEventSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceEventSymbol.cs @@ -569,7 +569,7 @@ private DeclarationModifiers MakeModifiers(SyntaxTokenList modifiers, bool expli // Proper errors must have been reported by now. if (isInterface) { - mods = ModifierUtils.AdjustModifiersForAnInterfaceMember(mods, !isFieldLike, explicitInterfaceImplementation); + mods = ModifierUtils.AdjustModifiersForAnInterfaceMember(mods, !isFieldLike, explicitInterfaceImplementation, forMethod: false); } return mods; diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceOrdinaryMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceOrdinaryMethodSymbol.cs index 4a3d1891ccbce..a73ebfe661819 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceOrdinaryMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceOrdinaryMethodSymbol.cs @@ -797,7 +797,8 @@ private static DeclarationModifiers AddImpliedModifiers(DeclarationModifiers mod if (containingTypeIsInterface) { mods = ModifierUtils.AdjustModifiersForAnInterfaceMember(mods, hasBody, - methodKind == MethodKind.ExplicitInterfaceImplementation); + methodKind == MethodKind.ExplicitInterfaceImplementation, + forMethod: true); } else if (methodKind == MethodKind.ExplicitInterfaceImplementation) { diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs index 2c0bf177b81cc..b35dea6073e47 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs @@ -482,7 +482,7 @@ private static (DeclarationModifiers modifiers, bool hasExplicitAccessMod) MakeM // Proper errors must have been reported by now. if (isInterface) { - mods = ModifierUtils.AdjustModifiersForAnInterfaceMember(mods, accessorsHaveImplementation, isExplicitInterfaceImplementation); + mods = ModifierUtils.AdjustModifiersForAnInterfaceMember(mods, accessorsHaveImplementation, isExplicitInterfaceImplementation, forMethod: false); } if (isIndexer) diff --git a/src/Compilers/CSharp/Test/Emit3/PartialEventsAndConstructorsTests.cs b/src/Compilers/CSharp/Test/Emit3/PartialEventsAndConstructorsTests.cs index cbe87ebb75e9e..032e460fd667d 100644 --- a/src/Compilers/CSharp/Test/Emit3/PartialEventsAndConstructorsTests.cs +++ b/src/Compilers/CSharp/Test/Emit3/PartialEventsAndConstructorsTests.cs @@ -946,6 +946,315 @@ partial interface I Diagnostic(ErrorCode.ERR_PartialMemberMissingImplementation, "E").WithArguments("I.E").WithLocation(3, 33)); } + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/77346")] + public void InInterface_Virtual( + [CombinatorialValues("", "public", "private", "protected", "internal", "protected internal", "private protected")] string access, + [CombinatorialValues("", "virtual", "sealed")] string virt) + { + var source1 = $$""" + using System; + + partial interface I + { + {{access}} {{virt}} partial event Action E; + {{access}} {{virt}} partial event Action E + { + add { Console.Write(1); } + remove { Console.Write(2); } + } + } + """; + + var source2 = """ + using System; + + partial interface I + { + static void Main() + { + M(new C1()); + M(new C2()); + } + + static void M(I x) + { + x.E += null; + x.E -= null; + } + } + + class C1 : I; + + class C2 : I + { + event Action I.E + { + add { Console.Write(3); } + remove { Console.Write(4); } + } + } + """; + + var expectedAccessibility = access switch + { + "" or "public" => Accessibility.Public, + "private" => Accessibility.Private, + "protected" => Accessibility.Protected, + "internal" => Accessibility.Internal, + "protected internal" => Accessibility.ProtectedOrInternal, + "private protected" => Accessibility.ProtectedAndFriend, + _ => throw ExceptionUtilities.UnexpectedValue(access), + }; + + bool expectedVirtual = access == "private" + ? virt == "virtual" + : virt != "sealed"; + + bool expectedSealed = access == "private" && virt == "sealed"; + + bool executable = virt != "sealed" && access is not "private"; + + DiagnosticDescription[] expectedDiagnostics = []; + + if (access == "private") + { + if (virt == "sealed") + { + expectedDiagnostics = + [ + // (5,41): error CS0238: 'I.E' cannot be sealed because it is not an override + // private sealed partial event Action E; + Diagnostic(ErrorCode.ERR_SealedNonOverride, "E").WithArguments("I.E").WithLocation(5, 41) + ]; + } + else if (virt == "virtual") + { + expectedDiagnostics = + [ + // (5,42): error CS0621: 'I.E': virtual or abstract members cannot be private + // private virtual partial event Action E; + Diagnostic(ErrorCode.ERR_VirtualPrivate, "E").WithArguments("I.E").WithLocation(5, 42) + ]; + } + } + + var comp = CreateCompilation(executable ? [source1, source2] : source1, + options: TestOptions.DebugDll + .WithOutputKind(executable ? OutputKind.ConsoleApplication : OutputKind.DynamicallyLinkedLibrary) + .WithMetadataImportOptions(MetadataImportOptions.All), + targetFramework: TargetFramework.Net60).VerifyDiagnostics(expectedDiagnostics); + + if (expectedDiagnostics.Length == 0) + { + CompileAndVerify(comp, + sourceSymbolValidator: validate, + symbolValidator: validate, + expectedOutput: executable && ExecutionConditionUtil.IsMonoOrCoreClr ? "1234" : null, + verify: Verification.FailsPEVerify).VerifyDiagnostics(); + } + else + { + validate(comp.SourceModule); + } + + void validate(ModuleSymbol module) + { + var e = module.GlobalNamespace.GetMember("I.E"); + validateEvent(e); + + if (module is SourceModuleSymbol) + { + validateEvent((EventSymbol)e.GetPartialImplementationPart()!); + } + } + + void validateEvent(EventSymbol e) + { + Assert.False(e.IsAbstract); + Assert.Equal(expectedVirtual, e.IsVirtual); + Assert.Equal(expectedSealed, e.IsSealed); + Assert.False(e.IsStatic); + Assert.False(e.IsExtern); + Assert.False(e.IsOverride); + Assert.Equal(expectedAccessibility, e.DeclaredAccessibility); + Assert.True(e.ContainingModule is not SourceModuleSymbol || e.IsPartialMember()); + validateAccessor(e.AddMethod!); + validateAccessor(e.RemoveMethod!); + } + + void validateAccessor(MethodSymbol m) + { + Assert.False(m.IsAbstract); + Assert.Equal(expectedVirtual, m.IsVirtual); + Assert.Equal(expectedVirtual, m.IsMetadataVirtual()); + Assert.Equal(expectedVirtual, m.IsMetadataNewSlot()); + Assert.Equal(expectedSealed, m.IsSealed); + Assert.False(m.IsStatic); + Assert.False(m.IsExtern); + Assert.False(m.IsOverride); + Assert.Equal(expectedAccessibility, m.DeclaredAccessibility); + Assert.True(m.ContainingModule is not SourceModuleSymbol || m.IsPartialMember()); + } + } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/77346")] + public void InInterface_StaticVirtual( + [CombinatorialValues("", "public", "private", "protected", "internal", "protected internal", "private protected")] string access, + [CombinatorialValues("", "virtual", "sealed")] string virt) + { + var source1 = $$""" + using System; + + partial interface I + { + {{access}} static {{virt}} partial event Action E; + {{access}} static {{virt}} partial event Action E + { + add { Console.Write(1); } + remove { Console.Write(2); } + } + } + """; + + var source2 = """ + using System; + + partial interface I + { + static void Main() + { + M(); + M(); + M(); + } + + static void M() where T : I + { + T.E += null; + T.E -= null; + } + } + + class C1 : I + { + static event Action E + { + add { Console.Write(3); } + remove { Console.Write(4); } + } + } + + class C2 : I + { + public static event Action E + { + add { Console.Write(5); } + remove { Console.Write(6); } + } + } + + class C3 : I + { + static event Action I.E + { + add { Console.Write(7); } + remove { Console.Write(8); } + } + } + """; + + var expectedAccessibility = access switch + { + "" or "public" => Accessibility.Public, + "private" => Accessibility.Private, + "protected" => Accessibility.Protected, + "internal" => Accessibility.Internal, + "protected internal" => Accessibility.ProtectedOrInternal, + "private protected" => Accessibility.ProtectedAndFriend, + _ => throw ExceptionUtilities.UnexpectedValue(access), + }; + + bool expectedVirtual = virt == "virtual"; + + bool executable = virt == "virtual" && access is not "private"; + + DiagnosticDescription[] expectedDiagnostics = []; + + if (access == "private" && virt == "virtual") + { + expectedDiagnostics = + [ + // (5,49): error CS0621: 'I.E': virtual or abstract members cannot be private + // private static virtual partial event Action E; + Diagnostic(ErrorCode.ERR_VirtualPrivate, "E").WithArguments("I.E").WithLocation(5, 49) + ]; + } + + var comp = CreateCompilation(executable ? [source1, source2] : source1, + options: TestOptions.DebugDll + .WithOutputKind(executable ? OutputKind.ConsoleApplication : OutputKind.DynamicallyLinkedLibrary) + .WithMetadataImportOptions(MetadataImportOptions.All), + targetFramework: TargetFramework.Net60).VerifyDiagnostics(expectedDiagnostics); + + if (expectedDiagnostics.Length == 0) + { + CompileAndVerify(comp, + sourceSymbolValidator: validate, + symbolValidator: validate, + expectedOutput: executable && ExecutionConditionUtil.IsMonoOrCoreClr ? "125678" : null, + verify: virt != "virtual" ? Verification.FailsPEVerify : Verification.Fails with + { + ILVerifyMessage = """ + [M]: Missing callvirt following constrained prefix. { Offset = 0x8 } + [M]: Missing callvirt following constrained prefix. { Offset = 0x15 } + """, + }).VerifyDiagnostics(); + } + else + { + validate(comp.SourceModule); + } + + void validate(ModuleSymbol module) + { + var e = module.GlobalNamespace.GetMember("I.E"); + validateEvent(e); + + if (module is SourceModuleSymbol) + { + validateEvent((EventSymbol)e.GetPartialImplementationPart()!); + } + } + + void validateEvent(EventSymbol e) + { + Assert.False(e.IsAbstract); + Assert.Equal(expectedVirtual, e.IsVirtual); + Assert.False(e.IsSealed); + Assert.True(e.IsStatic); + Assert.False(e.IsExtern); + Assert.False(e.IsOverride); + Assert.Equal(expectedAccessibility, e.DeclaredAccessibility); + Assert.True(e.ContainingModule is not SourceModuleSymbol || e.IsPartialMember()); + validateAccessor(e.AddMethod!); + validateAccessor(e.RemoveMethod!); + } + + void validateAccessor(MethodSymbol m) + { + Assert.False(m.IsAbstract); + Assert.Equal(expectedVirtual, m.IsVirtual); + Assert.Equal(expectedVirtual, m.IsMetadataVirtual()); + Assert.False(m.IsMetadataNewSlot()); + Assert.False(m.IsSealed); + Assert.True(m.IsStatic); + Assert.False(m.IsExtern); + Assert.False(m.IsOverride); + Assert.Equal(expectedAccessibility, m.DeclaredAccessibility); + Assert.True(m.ContainingModule is not SourceModuleSymbol || m.IsPartialMember()); + } + } + [Fact] public void Abstract() { diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/ExtendedPartialMethodsTests.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/ExtendedPartialMethodsTests.cs index a92361f6fa327..7c6adb42c5980 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/ExtendedPartialMethodsTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/ExtendedPartialMethodsTests.cs @@ -1926,6 +1926,346 @@ partial class C : I Diagnostic(ErrorCode.ERR_PartialMethodWithNonVoidReturnMustHaveAccessMods, "M").WithArguments("C.I.M()").WithLocation(10, 19)); } + [Fact] + public void InInterface() + { + var source = """ + partial interface I + { + public partial int M(); + public partial int M() => 0; + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (4,24): error CS8701: Target runtime doesn't support default interface implementation. + // public partial int M() => 0; + Diagnostic(ErrorCode.ERR_RuntimeDoesNotSupportDefaultInterfaceImplementation, "M").WithLocation(4, 24)); + + CreateCompilation(source, targetFramework: TargetFramework.Net60).VerifyDiagnostics(); + + CreateCompilation(source, targetFramework: TargetFramework.Net60, parseOptions: TestOptions.Regular7).VerifyDiagnostics( + // (3,24): error CS8703: The modifier 'public' is not valid for this item in C# 7.0. Please use language version '8.0' or greater. + // public partial int M(); + Diagnostic(ErrorCode.ERR_InvalidModifierForLanguageVersion, "M").WithArguments("public", "7.0", "8.0").WithLocation(3, 24), + // (3,24): error CS8703: The modifier 'partial' is not valid for this item in C# 7.0. Please use language version '8.0' or greater. + // public partial int M(); + Diagnostic(ErrorCode.ERR_InvalidModifierForLanguageVersion, "M").WithArguments("partial", "7.0", "8.0").WithLocation(3, 24), + // (3,24): error CS8107: Feature 'extended partial methods' is not available in C# 7.0. Please use language version 9.0 or greater. + // public partial int M(); + Diagnostic(ErrorCode.ERR_FeatureNotAvailableInVersion7, "M").WithArguments("extended partial methods", "9.0").WithLocation(3, 24), + // (4,24): error CS8107: Feature 'default interface implementation' is not available in C# 7.0. Please use language version 8.0 or greater. + // public partial int M() => 0; + Diagnostic(ErrorCode.ERR_FeatureNotAvailableInVersion7, "M").WithArguments("default interface implementation", "8.0").WithLocation(4, 24), + // (4,24): error CS8107: Feature 'extended partial methods' is not available in C# 7.0. Please use language version 9.0 or greater. + // public partial int M() => 0; + Diagnostic(ErrorCode.ERR_FeatureNotAvailableInVersion7, "M").WithArguments("extended partial methods", "9.0").WithLocation(4, 24)); + } + + [Fact] + public void InInterface_DefinitionOnly() + { + var source = """ + partial interface I + { + public partial int M(); + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (3,24): error CS8795: Partial method 'I.M()' must have an implementation part because it has accessibility modifiers. + // public partial int M(); + Diagnostic(ErrorCode.ERR_PartialMethodWithAccessibilityModsMustHaveImplementation, "M").WithArguments("I.M()").WithLocation(3, 24)); + } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/77346")] + public void InInterface_Virtual( + [CombinatorialValues("", "public", "private", "protected", "internal", "protected internal", "private protected")] string access, + [CombinatorialValues("", "virtual", "sealed")] string virt, + [CombinatorialValues(LanguageVersion.CSharp13, LanguageVersion.Preview, LanguageVersionFacts.CSharpNext)] LanguageVersion langVersion) + { + var source1 = $$""" + using System; + + partial interface I + { + {{access}} {{virt}} + partial void M(); + {{access}} {{virt}} + partial void M() { Console.Write(1); } + } + """; + + var source2 = """ + using System; + + partial interface I + { + static void Main() + { + Helper(new C1()); + Helper(new C2()); + } + + static void Helper(I x) + { + x.M(); + } + } + + class C1 : I; + + class C2 : I + { + void I.M() { Console.Write(2); } + } + """; + + bool expectedPrivate = access is "" or "private"; + + var expectedAccessibility = access switch + { + _ when expectedPrivate => Accessibility.Private, + "public" => Accessibility.Public, + "protected" => Accessibility.Protected, + "internal" => Accessibility.Internal, + "protected internal" => Accessibility.ProtectedOrInternal, + "private protected" => Accessibility.ProtectedAndFriend, + _ => throw ExceptionUtilities.UnexpectedValue(access), + }; + + bool expectedVirtual = virt == "virtual"; + + bool expectedSealed = virt == "sealed"; + + bool executable = !expectedPrivate && virt == "virtual"; + + DiagnosticDescription[] expectedDiagnostics = []; + + if (access == "" && virt != "") + { + expectedDiagnostics = + [ + // (6,18): error CS8798: Partial method 'I.M()' must have accessibility modifiers because it has a 'virtual', 'override', 'sealed', 'new', or 'extern' modifier. + // partial void M(); + Diagnostic(ErrorCode.ERR_PartialMethodWithExtendedModMustHaveAccessMods, "M").WithArguments("I.M()").WithLocation(6, 18), + // (8,18): error CS8798: Partial method 'I.M()' must have accessibility modifiers because it has a 'virtual', 'override', 'sealed', 'new', or 'extern' modifier. + // partial void M() { Console.Write(1); } + Diagnostic(ErrorCode.ERR_PartialMethodWithExtendedModMustHaveAccessMods, "M").WithArguments("I.M()").WithLocation(8, 18) + ]; + } + else if (virt == "sealed") + { + expectedDiagnostics = + [ + // (6,18): error CS0238: 'I.M()' cannot be sealed because it is not an override + // partial void M(); + Diagnostic(ErrorCode.ERR_SealedNonOverride, "M").WithArguments("I.M()").WithLocation(6, 18), + // (8,18): error CS0238: 'I.M()' cannot be sealed because it is not an override + // partial void M() { Console.Write(1); } + Diagnostic(ErrorCode.ERR_SealedNonOverride, "M").WithArguments("I.M()").WithLocation(8, 18) + ]; + } + else if (expectedPrivate && virt == "virtual") + { + expectedDiagnostics = + [ + // (6,18): error CS0621: 'I.M()': virtual or abstract members cannot be private + // partial void M(); + Diagnostic(ErrorCode.ERR_VirtualPrivate, "M").WithArguments("I.M()").WithLocation(6, 18), + // (8,18): error CS0621: 'I.M()': virtual or abstract members cannot be private + // partial void M() { Console.Write(1); } + Diagnostic(ErrorCode.ERR_VirtualPrivate, "M").WithArguments("I.M()").WithLocation(8, 18) + ]; + } + + var comp = CreateCompilation(executable ? [source1, source2] : source1, + options: TestOptions.DebugDll + .WithOutputKind(executable ? OutputKind.ConsoleApplication : OutputKind.DynamicallyLinkedLibrary) + .WithMetadataImportOptions(MetadataImportOptions.All), + parseOptions: TestOptions.Regular.WithLanguageVersion(langVersion), + targetFramework: TargetFramework.Net60).VerifyDiagnostics(expectedDiagnostics); + + if (expectedDiagnostics.Length == 0) + { + CompileAndVerify(comp, + sourceSymbolValidator: validate, + symbolValidator: validate, + expectedOutput: executable && ExecutionConditionUtil.IsMonoOrCoreClr ? "12" : null, + verify: Verification.FailsPEVerify).VerifyDiagnostics(); + } + else + { + validate(comp.SourceModule); + } + + void validate(ModuleSymbol module) + { + var m = module.GlobalNamespace.GetMember("I.M"); + validateMethod(m); + + if (module is SourceModuleSymbol) + { + validateMethod((MethodSymbol)m.GetPartialImplementationPart()!); + } + } + + void validateMethod(MethodSymbol m) + { + Assert.False(m.IsAbstract); + Assert.Equal(expectedVirtual, m.IsVirtual); + Assert.Equal(expectedVirtual, m.IsMetadataVirtual()); + Assert.Equal(expectedVirtual, m.IsMetadataNewSlot()); + Assert.Equal(expectedSealed, m.IsSealed); + Assert.False(m.IsStatic); + Assert.False(m.IsExtern); + Assert.False(m.IsOverride); + Assert.Equal(expectedAccessibility, m.DeclaredAccessibility); + Assert.True(m.ContainingModule is not SourceModuleSymbol || m.IsPartialMember()); + } + } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/77346")] + public void InInterface_StaticVirtual( + [CombinatorialValues("", "public", "private", "protected", "internal", "protected internal", "private protected")] string access, + [CombinatorialValues("", "virtual", "sealed")] string virt, + [CombinatorialValues(LanguageVersion.CSharp13, LanguageVersion.Preview, LanguageVersionFacts.CSharpNext)] LanguageVersion langVersion) + { + var source1 = $$""" + partial interface I + { + {{access}} static {{virt}} + partial void M(); + {{access}} static {{virt}} + partial void M() { System.Console.Write(1); } + } + """; + + var source2 = """ + partial interface I + { + static void Main() + { + Helper(); + Helper(); + Helper(); + } + + static void Helper() where T : I + { + T.M(); + } + } + + class C1 : I + { + static void M() { System.Console.Write(2); } + } + + class C2 : I + { + public static void M() { System.Console.Write(3); } + } + + class C3 : I + { + static void I.M() { System.Console.Write(4); } + } + """; + + bool expectedPrivate = access is "" or "private"; + + var expectedAccessibility = access switch + { + _ when expectedPrivate => Accessibility.Private, + "public" => Accessibility.Public, + "protected" => Accessibility.Protected, + "internal" => Accessibility.Internal, + "protected internal" => Accessibility.ProtectedOrInternal, + "private protected" => Accessibility.ProtectedAndFriend, + _ => throw ExceptionUtilities.UnexpectedValue(access), + }; + + bool expectedVirtual = virt == "virtual"; + + bool executable = virt == "virtual" && !expectedPrivate; + + DiagnosticDescription[] expectedDiagnostics = []; + + if (access == "" && virt == "virtual") + { + expectedDiagnostics = + [ + // (4,18): error CS8798: Partial method 'I.M()' must have accessibility modifiers because it has a 'virtual', 'override', 'sealed', 'new', or 'extern' modifier. + // partial void M(); + Diagnostic(ErrorCode.ERR_PartialMethodWithExtendedModMustHaveAccessMods, "M").WithArguments("I.M()").WithLocation(4, 18), + // (6,18): error CS8798: Partial method 'I.M()' must have accessibility modifiers because it has a 'virtual', 'override', 'sealed', 'new', or 'extern' modifier. + // partial void M() { System.Console.Write(1); } + Diagnostic(ErrorCode.ERR_PartialMethodWithExtendedModMustHaveAccessMods, "M").WithArguments("I.M()").WithLocation(6, 18) + ]; + } + else if (access == "private" && virt == "virtual") + { + expectedDiagnostics = + [ + // (4,18): error CS0621: 'I.M()': virtual or abstract members cannot be private + // partial void M(); + Diagnostic(ErrorCode.ERR_VirtualPrivate, "M").WithArguments("I.M()").WithLocation(4, 18), + // (6,18): error CS0621: 'I.M()': virtual or abstract members cannot be private + // partial void M() { System.Console.Write(1); } + Diagnostic(ErrorCode.ERR_VirtualPrivate, "M").WithArguments("I.M()").WithLocation(6, 18) + ]; + } + + var comp = CreateCompilation(executable ? [source1, source2] : source1, + parseOptions: TestOptions.Regular.WithLanguageVersion(langVersion), + options: TestOptions.DebugDll + .WithOutputKind(executable ? OutputKind.ConsoleApplication : OutputKind.DynamicallyLinkedLibrary) + .WithMetadataImportOptions(MetadataImportOptions.All), + targetFramework: TargetFramework.Net60).VerifyDiagnostics(expectedDiagnostics); + + if (expectedDiagnostics.Length == 0) + { + CompileAndVerify(comp, + sourceSymbolValidator: validate, + symbolValidator: validate, + expectedOutput: executable && ExecutionConditionUtil.IsMonoOrCoreClr ? "134" : null, + verify: virt != "virtual" ? Verification.FailsPEVerify : Verification.Fails with + { + ILVerifyMessage = """ + [Helper]: Missing callvirt following constrained prefix. { Offset = 0x7 } + """, + }).VerifyDiagnostics(); + } + else + { + validate(comp.SourceModule); + } + + void validate(ModuleSymbol module) + { + var m = module.GlobalNamespace.GetMember("I.M"); + validateMethod(m); + + if (module is SourceModuleSymbol) + { + validateMethod((MethodSymbol)m.GetPartialImplementationPart()!); + } + } + + void validateMethod(MethodSymbol m) + { + Assert.False(m.IsAbstract); + Assert.Equal(expectedVirtual, m.IsVirtual); + Assert.Equal(expectedVirtual, m.IsMetadataVirtual()); + Assert.False(m.IsMetadataNewSlot()); + Assert.False(m.IsSealed); + Assert.True(m.IsStatic); + Assert.False(m.IsExtern); + Assert.False(m.IsOverride); + Assert.Equal(expectedAccessibility, m.DeclaredAccessibility); + Assert.True(m.ContainingModule is not SourceModuleSymbol || m.IsPartialMember()); + } + } + [Fact] public void InsideStruct() { diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/PartialPropertiesTests.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/PartialPropertiesTests.cs index fe5ee45ce77a6..6fa3f1b3ffe69 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/PartialPropertiesTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/PartialPropertiesTests.cs @@ -2495,6 +2495,347 @@ class C Diagnostic(ErrorCode.ERR_PartialMemberOnlyInPartialClass, "P3").WithLocation(8, 17)); } + [Fact] + public void InInterface() + { + var source = """ + partial interface I + { + partial int P { get; set; } + partial int P { get => 0; set { } } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (4,21): error CS8701: Target runtime doesn't support default interface implementation. + // partial int P { get => 0; set { } } + Diagnostic(ErrorCode.ERR_RuntimeDoesNotSupportDefaultInterfaceImplementation, "get").WithLocation(4, 21), + // (4,31): error CS8701: Target runtime doesn't support default interface implementation. + // partial int P { get => 0; set { } } + Diagnostic(ErrorCode.ERR_RuntimeDoesNotSupportDefaultInterfaceImplementation, "set").WithLocation(4, 31)); + + CreateCompilation(source, targetFramework: TargetFramework.Net60).VerifyDiagnostics(); + + CreateCompilation(source, targetFramework: TargetFramework.Net60, parseOptions: TestOptions.Regular7).VerifyDiagnostics( + // (3,17): error CS8703: The modifier 'partial' is not valid for this item in C# 7.0. Please use language version '13.0' or greater. + // partial int P { get; set; } + Diagnostic(ErrorCode.ERR_InvalidModifierForLanguageVersion, "P").WithArguments("partial", "7.0", "13.0").WithLocation(3, 17), + // (4,17): error CS8703: The modifier 'partial' is not valid for this item in C# 7.0. Please use language version '13.0' or greater. + // partial int P { get => 0; set { } } + Diagnostic(ErrorCode.ERR_InvalidModifierForLanguageVersion, "P").WithArguments("partial", "7.0", "13.0").WithLocation(4, 17), + // (4,21): error CS8107: Feature 'default interface implementation' is not available in C# 7.0. Please use language version 8.0 or greater. + // partial int P { get => 0; set { } } + Diagnostic(ErrorCode.ERR_FeatureNotAvailableInVersion7, "get").WithArguments("default interface implementation", "8.0").WithLocation(4, 21), + // (4,31): error CS8107: Feature 'default interface implementation' is not available in C# 7.0. Please use language version 8.0 or greater. + // partial int P { get => 0; set { } } + Diagnostic(ErrorCode.ERR_FeatureNotAvailableInVersion7, "set").WithArguments("default interface implementation", "8.0").WithLocation(4, 31)); + } + + [Fact] + public void InInterface_DefinitionOnly() + { + var source = """ + partial interface I + { + partial int P { get; set; } + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (3,17): error CS9248: Partial property 'I.P' must have an implementation part. + // partial int P { get; set; } + Diagnostic(ErrorCode.ERR_PartialPropertyMissingImplementation, "P").WithArguments("I.P").WithLocation(3, 17)); + } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/77346")] + public void InInterface_Virtual( + [CombinatorialValues("", "public", "private", "protected", "internal", "protected internal", "private protected")] string access, + [CombinatorialValues("", "virtual", "sealed")] string virt, + [CombinatorialValues(LanguageVersion.CSharp13, LanguageVersion.Preview, LanguageVersionFacts.CSharpNext)] LanguageVersion langVersion) + { + var source1 = $$""" + using System; + + partial interface I + { + {{access}} {{virt}} + partial int P { get; set; } + {{access}} {{virt}} + partial int P + { + get { Console.Write(1); return 0; } + set { Console.Write(2); } + } + } + """; + + var source2 = """ + using System; + + partial interface I + { + static void Main() + { + M(new C1()); + M(new C2()); + } + + static void M(I x) + { + x.P++; + } + } + + class C1 : I; + + class C2 : I + { + int I.P + { + get { Console.Write(3); return 0; } + set { Console.Write(4); } + } + } + """; + + var expectedAccessibility = access switch + { + "" or "public" => Accessibility.Public, + "private" => Accessibility.Private, + "protected" => Accessibility.Protected, + "internal" => Accessibility.Internal, + "protected internal" => Accessibility.ProtectedOrInternal, + "private protected" => Accessibility.ProtectedAndFriend, + _ => throw ExceptionUtilities.UnexpectedValue(access), + }; + + bool expectedVirtual = access == "private" + ? virt == "virtual" + : virt != "sealed"; + + bool expectedSealed = access == "private" && virt == "sealed"; + + bool executable = access != "private" && virt != "sealed"; + + DiagnosticDescription[] expectedDiagnostics = []; + + if (virt == "sealed" && access == "private") + { + expectedDiagnostics = + [ + // (6,17): error CS0238: 'I.P' cannot be sealed because it is not an override + // partial int P { get; set; } + Diagnostic(ErrorCode.ERR_SealedNonOverride, "P").WithArguments("I.P").WithLocation(6, 17) + ]; + } + else if (access == "private" && expectedVirtual) + { + expectedDiagnostics = + [ + // (6,17): error CS0621: 'I.P': virtual or abstract members cannot be private + // partial int P { get; set; } + Diagnostic(ErrorCode.ERR_VirtualPrivate, "P").WithArguments("I.P").WithLocation(6, 17) + ]; + } + + var comp = CreateCompilation(executable ? [source1, source2] : source1, + options: TestOptions.DebugDll + .WithOutputKind(executable ? OutputKind.ConsoleApplication : OutputKind.DynamicallyLinkedLibrary) + .WithMetadataImportOptions(MetadataImportOptions.All), + parseOptions: TestOptions.Regular.WithLanguageVersion(langVersion), + targetFramework: TargetFramework.Net60).VerifyDiagnostics(expectedDiagnostics); + + if (expectedDiagnostics.Length == 0) + { + CompileAndVerify(comp, + sourceSymbolValidator: validate, + symbolValidator: validate, + expectedOutput: executable && ExecutionConditionUtil.IsMonoOrCoreClr ? "1234" : null, + verify: Verification.FailsPEVerify).VerifyDiagnostics(); + } + else + { + validate(comp.SourceModule); + } + + void validate(ModuleSymbol module) + { + var p = module.GlobalNamespace.GetMember("I.P"); + validateProperty(p); + + if (module is SourceModuleSymbol) + { + validateProperty((PropertySymbol)p.GetPartialImplementationPart()!); + } + } + + void validateProperty(PropertySymbol p) + { + Assert.False(p.IsAbstract); + Assert.Equal(expectedVirtual, p.IsVirtual); + Assert.Equal(expectedSealed, p.IsSealed); + Assert.False(p.IsStatic); + Assert.False(p.IsExtern); + Assert.False(p.IsOverride); + Assert.Equal(expectedAccessibility, p.DeclaredAccessibility); + Assert.True(p.ContainingModule is not SourceModuleSymbol || p.IsPartialMember()); + validateAccessor(p.GetMethod); + validateAccessor(p.SetMethod); + } + + void validateAccessor(MethodSymbol m) + { + Assert.False(m.IsAbstract); + Assert.Equal(expectedVirtual, m.IsVirtual); + Assert.Equal(expectedVirtual, m.IsMetadataVirtual()); + Assert.Equal(expectedVirtual, m.IsMetadataNewSlot()); + Assert.Equal(expectedSealed, m.IsSealed); + Assert.False(m.IsStatic); + Assert.False(m.IsExtern); + Assert.False(m.IsOverride); + Assert.Equal(expectedAccessibility, m.DeclaredAccessibility); + Assert.True(m.ContainingModule is not SourceModuleSymbol || m.IsPartialMember()); + } + } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/77346")] + public void InInterface_StaticVirtual( + [CombinatorialValues("", "public", "private", "protected", "internal", "protected internal", "private protected")] string access, + [CombinatorialValues("", "virtual", "sealed")] string virt, + [CombinatorialValues(LanguageVersion.CSharp13, LanguageVersion.Preview, LanguageVersionFacts.CSharpNext)] LanguageVersion langVersion) + { + var source1 = $$""" + partial interface I + { + {{access}} static {{virt}} + partial int P { get; set; } + {{access}} static {{virt}} + partial int P { get => 1; set { } } + } + """; + + var source2 = """ + partial interface I + { + static void Main() + { + M(); + M(); + M(); + } + + static void M() where T : I + { + System.Console.Write(T.P); + } + } + + class C1 : I + { + static int P { get => 2; set { } } + } + + class C2 : I + { + public static int P { get => 3; set { } } + } + + class C3 : I + { + static int I.P { get => 4; set { } } + } + """; + + var expectedAccessibility = access switch + { + "" or "public" => Accessibility.Public, + "private" => Accessibility.Private, + "protected" => Accessibility.Protected, + "internal" => Accessibility.Internal, + "protected internal" => Accessibility.ProtectedOrInternal, + "private protected" => Accessibility.ProtectedAndFriend, + _ => throw ExceptionUtilities.UnexpectedValue(access), + }; + + bool expectedVirtual = virt == "virtual"; + + bool executable = virt == "virtual" && access != "private"; + + DiagnosticDescription[] expectedDiagnostics = []; + + if (access == "private" && virt == "virtual") + { + expectedDiagnostics = + [ + // (4,17): error CS0621: 'I.P': virtual or abstract members cannot be private + // partial int P { get; set; } + Diagnostic(ErrorCode.ERR_VirtualPrivate, "P").WithArguments("I.P").WithLocation(4, 17) + ]; + } + + var comp = CreateCompilation(executable ? [source1, source2] : source1, + parseOptions: TestOptions.Regular.WithLanguageVersion(langVersion), + options: TestOptions.DebugDll + .WithOutputKind(executable ? OutputKind.ConsoleApplication : OutputKind.DynamicallyLinkedLibrary) + .WithMetadataImportOptions(MetadataImportOptions.All), + targetFramework: TargetFramework.Net60).VerifyDiagnostics(expectedDiagnostics); + + if (expectedDiagnostics.Length == 0) + { + CompileAndVerify(comp, + sourceSymbolValidator: validate, + symbolValidator: validate, + expectedOutput: executable && ExecutionConditionUtil.IsMonoOrCoreClr ? "134" : null, + verify: virt != "virtual" ? Verification.FailsPEVerify : Verification.Fails with + { + ILVerifyMessage = """ + [M]: Missing callvirt following constrained prefix. { Offset = 0x7 } + """, + }).VerifyDiagnostics(); + } + else + { + validate(comp.SourceModule); + } + + void validate(ModuleSymbol module) + { + var p = module.GlobalNamespace.GetMember("I.P"); + validateProperty(p); + + if (module is SourceModuleSymbol) + { + validateProperty((PropertySymbol)p.GetPartialImplementationPart()!); + } + } + + void validateProperty(PropertySymbol p) + { + Assert.False(p.IsAbstract); + Assert.Equal(expectedVirtual, p.IsVirtual); + Assert.False(p.IsSealed); + Assert.True(p.IsStatic); + Assert.False(p.IsExtern); + Assert.False(p.IsOverride); + Assert.Equal(expectedAccessibility, p.DeclaredAccessibility); + Assert.True(p.ContainingModule is not SourceModuleSymbol || p.IsPartialMember()); + validateAccessor(p.GetMethod); + validateAccessor(p.SetMethod); + } + + void validateAccessor(MethodSymbol m) + { + Assert.False(m.IsAbstract); + Assert.Equal(expectedVirtual, m.IsVirtual); + Assert.Equal(expectedVirtual, m.IsMetadataVirtual()); + Assert.False(m.IsMetadataNewSlot()); + Assert.False(m.IsSealed); + Assert.True(m.IsStatic); + Assert.False(m.IsExtern); + Assert.False(m.IsOverride); + Assert.Equal(expectedAccessibility, m.DeclaredAccessibility); + Assert.True(m.ContainingModule is not SourceModuleSymbol || m.IsPartialMember()); + } + } + [Fact] public void Semantics_Indexers_01() {