Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make partial interface members virtual #77379

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

jjonescz
Copy link
Member

@jjonescz jjonescz commented Feb 28, 2025

Fixes #77346.

The classic (not extended) partial methods are always private (hence also not virtual), and that is also true in interfaces. However, the extended partial methods feature allows partial methods to be public and virtual, but in interfaces, they are not implicitly public nor virtual unlike their non-partial counterparts which I think was just an oversight (all speclet wording I could find in the mentioned features supports that). Since partial properties and events are based on extended partial methods, they have the same bug. This PR fixes all that.

@dotnet-issue-labeler dotnet-issue-labeler bot added Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead labels Feb 28, 2025
@jjonescz jjonescz marked this pull request as ready for review February 28, 2025 18:18
@jjonescz jjonescz requested a review from a team as a code owner February 28, 2025 18:18
@RikkiGibson RikkiGibson self-assigned this Mar 4, 2025
Copy link
Member

@RikkiGibson RikkiGibson left a comment

Choose a reason for hiding this comment

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

Impl and testing seems good, we should just seek confirmation that we want to do this as a bug-fix level change, and figure out if we need to update the breaking change doc.

@jjonescz jjonescz changed the base branch from features/PartialEventsCtors to main March 5, 2025 14:56
@AlekseyTs
Copy link
Contributor

AlekseyTs commented Mar 5, 2025

we should just seek confirmation that we want to do this as a bug-fix level change

It is unfortunate that we have this behavior with already shipped features. Changing virtual-ness of interface members feels like a big change to me. #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Mar 5, 2025

Done with review pass (commit 3) #Resolved

Copy link
Member

@RikkiGibson RikkiGibson left a comment

Choose a reason for hiding this comment

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

LGTM modulo additional suggested tests and confirmation on moving forward with the break.

@jjonescz
Copy link
Member Author

jjonescz commented Apr 1, 2025

@RikkiGibson @AlekseyTs for another look, thanks
@jaredpar said he is okay with moving forward with this break but that I should also send an e-mail to LDT which I did and also got no pushback #Closed

@jaredpar
Copy link
Member

jaredpar commented Apr 1, 2025

Good to go from my perspective. #Closed

@jaredpar jaredpar added the breaking-change https://github.com/dotnet/sdk/blob/main/documentation/project-docs/breaking-change-guidelines.md label Apr 1, 2025
@AlekseyTs
Copy link
Contributor

AlekseyTs commented Apr 1, 2025

@jjonescz It looks like there are conflicts that should be resolved #Closed

@@ -794,7 +794,7 @@ partial C() { }
}
""";
CreateCompilation(source).VerifyDiagnostics(
// (3,13): error CS9276: Partial member 'C.C()' must have a definition part.
// (3,13): error CS9401: Partial member 'C.C()' must have a definition part.
// partial C() { }
Diagnostic(ErrorCode.ERR_PartialMemberMissingDefinition, "C").WithArguments("C.C()").WithLocation(3, 13),
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 2, 2025

Choose a reason for hiding this comment

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

nostic(ErrorCode.ERR_PartialMemberMissingDefinition, "C").WithArguments("C.C(

I think this error is unexpected. sealed on interface members means non-virtual.
I guess the behavior is consistent with non-partial method. #Resolved

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's cover other accessibility modifiers (it looks like we cover public and private)

Copy link
Member Author

@jjonescz jjonescz Apr 3, 2025

Choose a reason for hiding this comment

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

This comment is placed on unchanged code but judging by the quoted text // (3,48): error CS0238: 'I.E' cannot be sealed because it is not an override, I assume this comment was intended for InInterface_Sealed_Private.

Copy link
Contributor

@AlekseyTs AlekseyTs Apr 3, 2025

Choose a reason for hiding this comment

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

That is correct (the location), this is a brand new test, so I am not sure why you consider it an unchanged code. Even though I "dismissed" my comment in the first message of this thread, the second message is still relevant.

Copy link
Member Author

Choose a reason for hiding this comment

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

this is a brand new test, so I am not sure why you consider it an unchanged code.

Sorry, I meant the location that this comment appears on GitHub and my CodeFlow is on unchanged test. I guess CodeFlow doesn't play nicely with merge commits.

using System;

M(new C1());
M(new C2());
Copy link
Contributor

Choose a reason for hiding this comment

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

"public"

Let's cover other accessibility modifiers


static void validateEvent(EventSymbol e)
{
Assert.False(e.IsAbstract);
Copy link
Contributor

Choose a reason for hiding this comment

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

"public"

Let's cover other accessibility modifiers

// partial event System.Action E { add { } remove { } }
Diagnostic(ErrorCode.ERR_PartialMemberDuplicateImplementation, "E").WithArguments("C.E").WithLocation(9, 33),
// (9,33): error CS0102: The type 'C' already contains a definition for 'E'
// partial event System.Action E { add { } remove { } }
Diagnostic(ErrorCode.ERR_DuplicateNameInClass, "E").WithArguments("C", "E").WithLocation(9, 33),
// (11,13): error CS9278: Partial member 'C.C()' may not have multiple implementing declarations.
// (11,13): error CS9403: Partial member 'C.C()' may not have multiple implementing declarations.
// partial C() { }
Diagnostic(ErrorCode.ERR_PartialMemberDuplicateImplementation, "C").WithArguments("C.C()").WithLocation(11, 13),
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 2, 2025

Choose a reason for hiding this comment

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

rCode.

It would be good to test situations when only one partial has sealed or virtual modifier. For other members too. #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure what would you like to see tested in such scenarios. We already have test for the error that will occur "Both partial member declarations must have identical combinations of 'virtual', 'override', 'sealed', and 'new' modifiers." (That's tested for class, can extend that to interface if you want)

var source = $$"""
partial interface I
{
public static {{modifier}} partial int M();
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 2, 2025

Choose a reason for hiding this comment

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

public

Let's cover remaining accessibility modifiers, including a situation with none #Resolved

""";
var comp = CreateCompilation(source, targetFramework: TargetFramework.Net60)
.VerifyDiagnostics(
// (3,32): error CS0238: 'I.M()' cannot be sealed because it is not an override
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 2, 2025

Choose a reason for hiding this comment

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

// (3,32): error CS0238: 'I.M()' cannot be sealed because it is not an override

Same comment as for events #Resolved

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 guess same response as for events applies here - this is consistent with non-partial methods.

Copy link
Contributor

Choose a reason for hiding this comment

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

There was a second message on that thread, that is still relevant

{
// In C# 13 and earlier, interface partial members were not implicitly public and virtual and that is fixed only in C# 14 and later to avoid a breaking change.
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 2, 2025

Choose a reason for hiding this comment

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

// In C# 13 and earlier, interface partial members were not implicitly public and virtual and that is fixed only in C# 14 and later to avoid a breaking change.

I do not think we should change anything about implicit accessibility #Resolved

Copy link
Contributor

Choose a reason for hiding this comment

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

The breaking change email doesn't mention this aspect explicitly at all. It only talks about virtual-ness.

Copy link
Member Author

@jjonescz jjonescz Apr 3, 2025

Choose a reason for hiding this comment

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

The breaking change email doesn't mention this aspect explicitly at all. It only talks about virtual-ness.

Yes, I narrowed down the focus of the email because I think the virtualness is the most important part, it can lead to silent behavior breaks. The accessibility break leads to source breaks only, and it doesn't affect methods. The email has link to this doc which has all the details.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, but I do not find this response satisfactory. We can discuss offline in more details

Copy link
Member Author

Choose a reason for hiding this comment

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

Scheduled for LDM: dotnet/csharplang#9282

}

[Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/77346")]
public void InInterface_NonVirtual_CSharp13_Private(
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 2, 2025

Choose a reason for hiding this comment

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

InInterface_NonVirtual_CSharp13_Private

Is this a dupe of InInterface_Private? #Resolved

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, will remove, thanks.

}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/77346")]
public void InInterface_StaticVirtual_CSharp13()
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 2, 2025

Choose a reason for hiding this comment

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

InInterface_StaticVirtual_CSharp13

Is there a difference in behavior by comparison to InInterface_StaticVirtual #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

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

Looks like there isn't, I will merge the tests, thanks.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, I cannot merge them because the combination langVersion=CSharp13 + access="" has a different behavior. But it's correct that InInterface_StaticVirtual(langVersion=CSharp13, access="public") would have the same behavior as InInterface_StaticVirtual_CSharp13.

Copy link
Contributor

Choose a reason for hiding this comment

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

I find it hard to track differences in behavior across multiple "related" tests, hard to put back the entire matrix together and what/where the differences are in the matrix. It feels like it might be better to cover the entire matrix in one test, but adjust asserts to make expectations conditional based on the inputs. I think this way it will be easier to observe differences in behavior and when they happen. Consider restructuring tests this way.

Copy link
Contributor

Choose a reason for hiding this comment

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

If you decide to pursue this suggestion, we can work on it offline together

// partial event System.Action E;
Diagnostic(ErrorCode.ERR_PartialMemberDuplicateDefinition, "E").WithArguments("C.E").WithLocation(4, 33),
// (4,33): error CS0102: The type 'C' already contains a definition for 'E'
// partial event System.Action E;
Diagnostic(ErrorCode.ERR_DuplicateNameInClass, "E").WithArguments("C", "E").WithLocation(4, 33),
// (5,33): error CS9277: Partial member 'C.F' may not have multiple defining declarations.
// (5,33): error CS9402: Partial member 'C.F' may not have multiple defining declarations.
// partial event System.Action F;
Diagnostic(ErrorCode.ERR_PartialMemberDuplicateDefinition, "F").WithArguments("C.F").WithLocation(5, 33),
// (5,33): error CS0102: The type 'C' already contains a definition for 'F'
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 2, 2025

Choose a reason for hiding this comment

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

ror CS0102: The type

Is there a change in behavior around implicit accessibility in this scenario in this PR? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

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

Unfortunately, I cannot decipher where this comment was supposed to point to

Copy link
Contributor

Choose a reason for hiding this comment

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

Unfortunately, I cannot decipher where this comment was supposed to point to

For me, CodeFlow successfully places the comment on line 1013 in commit 6: Assert.Equal(Accessibility.Public, e.DeclaredAccessibility);

Copy link
Member Author

@jjonescz jjonescz Apr 4, 2025

Choose a reason for hiding this comment

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

Then yes, previously the event was implicitly private now it is implicitly public.

{
// In C# 13 and earlier, interface partial members were not implicitly public and virtual and that is fixed only in C# 14 and later to avoid a breaking change.
bool newPartialBehavior = (mods & DeclarationModifiers.Partial) == 0 || ((CSharpParseOptions)syntaxNode.SyntaxTree.Options).LanguageVersion > LanguageVersion.CSharp13;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 2, 2025

Choose a reason for hiding this comment

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

newPartialBehavior

Setting this to true regardless of language version feels unintuitive and confusing. #Resolved

Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps changing the name of the local might improve things

***Introduced in Visual Studio 2022 version 17.15***

We have fixed [an inconsistency](https://github.com/dotnet/roslyn/issues/77346)
where interface members would not be implicitly `virtual` and `public` if they were also marked `partial`.
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 2, 2025

Choose a reason for hiding this comment

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

I think this description is unclear because accessibility for methods isn't affected by the change #Resolved

partial interface I
{
public partial void M();
public partial void M() // implicitly virtual now
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 2, 2025

Choose a reason for hiding this comment

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

// implicitly virtual now

I think we should add examples targeting changes around accessibility, preferable when virtual-ness is not involved #Resolved

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 6)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compilers breaking-change https://github.com/dotnet/sdk/blob/main/documentation/project-docs/breaking-change-guidelines.md untriaged Issues and PRs which have not yet been triaged by a lead
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Partial default interface members are not virtual
4 participants