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

Parse partial events and constructors #76860

17 changes: 17 additions & 0 deletions docs/compilers/CSharp/Compiler Breaking Changes - DotNet 10.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,3 +291,20 @@ unsafe record struct R(
public bool Equals(R other) => true;
}
```

## `partial` cannot be a return type of methods

***Introduced in Visual Studio 2022 version 17.14***

The [partial events and constructors](https://github.com/dotnet/csharplang/issues/9058) language feature
allows the `partial` modifier in more places and so it cannot be a return type unless escaped:

```cs
class C
{
partial F() => new partial(); // previously worked
@partial F() => new partial(); // workaround
}

class partial { }
```
3 changes: 3 additions & 0 deletions src/Compilers/CSharp/Portable/Errors/MessageID.cs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@ internal enum MessageID
IDS_FeatureFirstClassSpan = MessageBase + 12849,
IDS_FeatureUnboundGenericTypesInNameof = MessageBase + 12850,
IDS_FeatureSimpleLambdaParameterModifiers = MessageBase + 12851,

IDS_FeaturePartialEventsAndConstructors = MessageBase + 12852,
}

// Message IDs may refer to strings that need to be localized.
Expand Down Expand Up @@ -480,6 +482,7 @@ internal static LanguageVersion RequiredVersion(this MessageID feature)
case MessageID.IDS_FeatureFirstClassSpan:
case MessageID.IDS_FeatureUnboundGenericTypesInNameof:
case MessageID.IDS_FeatureSimpleLambdaParameterModifiers:
case MessageID.IDS_FeaturePartialEventsAndConstructors:
return LanguageVersion.Preview;

// C# 13.0 features.
Expand Down
39 changes: 20 additions & 19 deletions src/Compilers/CSharp/Portable/Parser/LanguageParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1634,24 +1634,25 @@ private bool IsPartialType()

private bool IsPartialMember()
{
// note(cyrusn): this could have been written like so:
//
// return
// this.CurrentToken.ContextualKind == SyntaxKind.PartialKeyword &&
// this.PeekToken(1).Kind == SyntaxKind.VoidKeyword;
//
// However, we want to be lenient and allow the user to write
// 'partial' in most modifier lists. We will then provide them with
// a more specific message later in binding that they are doing
// something wrong.
//
// Some might argue that the simple check would suffice.
// However, we'd like to maintain behavior with
// previously shipped versions, and so we're keeping this code.
Debug.Assert(this.CurrentToken.ContextualKind == SyntaxKind.PartialKeyword);
jjonescz marked this conversation as resolved.
Show resolved Hide resolved

// Here we check for:
// Check for:
// partial event
if (this.PeekToken(1).Kind == SyntaxKind.EventKeyword)
{
return IsFeatureEnabled(MessageID.IDS_FeaturePartialEventsAndConstructors);
Copy link
Contributor

Choose a reason for hiding this comment

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

not to belabor it, but, it wasn't obvious to me why event parsing should be conditional on LangVersion. Maybe it's simpler to just be consistent? Perhaps there is some existing form we are trying to avoid breaking? Feel free to add as an open question for a future PR, if any further investigation/work is found to be needed here.

Copy link
Member Author

@jjonescz jjonescz Jan 29, 2025

Choose a reason for hiding this comment

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

Good point, I did this for consistency but now I realize in parsing we want to avoid being conditional like this whenever possible. I don't think this can break anyone since event is a keyword anywhere (it's not contextual).

}

// Check for constructor:
// partial Identifier(
if (this.PeekToken(1).Kind == SyntaxKind.IdentifierToken &&
this.PeekToken(2).Kind == SyntaxKind.OpenParenToken)
{
return IsFeatureEnabled(MessageID.IDS_FeaturePartialEventsAndConstructors);
}
jjonescz marked this conversation as resolved.
Show resolved Hide resolved

// Check for method/property:
// partial ReturnType MemberName
Debug.Assert(this.CurrentToken.ContextualKind == SyntaxKind.PartialKeyword);
using var _ = this.GetDisposableResetPoint(resetOnDispose: true);

this.EatToken(); // partial
Expand Down Expand Up @@ -5680,7 +5681,7 @@ private bool IsTrueIdentifier()
{
if (this.CurrentToken.Kind == SyntaxKind.IdentifierToken)
{
if (!IsCurrentTokenPartialKeywordOfPartialMethodOrType() &&
if (!IsCurrentTokenPartialKeywordOfPartialMemberOrType() &&
!IsCurrentTokenQueryKeywordInQuery() &&
!IsCurrentTokenWhereOfConstraintClause())
{
Expand Down Expand Up @@ -5727,7 +5728,7 @@ private SyntaxToken ParseIdentifierToken(ErrorCode code = ErrorCode.ERR_Identifi
// show the correct parameter help in this case. So, when we see "partial" we check if it's being used
// as an identifier or as a contextual keyword. If it's the latter then we bail out. See
// Bug: vswhidbey/542125
if (IsCurrentTokenPartialKeywordOfPartialMethodOrType() || IsCurrentTokenQueryKeywordInQuery())
if (IsCurrentTokenPartialKeywordOfPartialMemberOrType() || IsCurrentTokenQueryKeywordInQuery())
{
var result = CreateMissingIdentifierToken();
result = this.AddError(result, ErrorCode.ERR_InvalidExprTerm, this.CurrentToken.Text);
Expand All @@ -5754,7 +5755,7 @@ private bool IsCurrentTokenQueryKeywordInQuery()
return this.IsInQuery && this.IsCurrentTokenQueryContextualKeyword;
}

private bool IsCurrentTokenPartialKeywordOfPartialMethodOrType()
private bool IsCurrentTokenPartialKeywordOfPartialMemberOrType()
{
if (this.CurrentToken.ContextualKind == SyntaxKind.PartialKeyword)
{
Expand Down
115 changes: 115 additions & 0 deletions src/Compilers/CSharp/Test/Emit3/PartialEventsAndConstructorsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// 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.

using Microsoft.CodeAnalysis.CSharp.Test.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.CSharp.UnitTests;

public sealed class PartialEventsAndConstructorsTests : CSharpTestBase
{
[Fact]
public void ReturningPartialType_LocalFunction_InMethod()
{
var source = """
class @partial
{
static void Main()
{
System.Console.Write(F().GetType().Name);
partial F() => new();
}
}
""";
CompileAndVerify(source, parseOptions: TestOptions.Regular13, expectedOutput: "partial").VerifyDiagnostics();
jjonescz marked this conversation as resolved.
Show resolved Hide resolved

var expectedDiagnostics = new[]
{
// (5,30): error CS0103: The name 'F' does not exist in the current context
// System.Console.Write(F().GetType().Name);
Diagnostic(ErrorCode.ERR_NameNotInContext, "F").WithArguments("F").WithLocation(5, 30),
// (5,50): error CS1513: } expected
// System.Console.Write(F().GetType().Name);
Diagnostic(ErrorCode.ERR_RbraceExpected, "").WithLocation(5, 50),
// (6,9): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method or property return type.
Copy link
Member

Choose a reason for hiding this comment

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

or a method or property return type

Should this error message be updated to include event or constructor?

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, it will be in the next PR (I already have that locally so I would skip adding a prototype comment for that if that's okay)

// partial F() => new();
Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(6, 9),
// (6,17): error CS1520: Method must have a return type
// partial F() => new();
Diagnostic(ErrorCode.ERR_MemberNeedsType, "F").WithLocation(6, 17),
// (6,24): error CS0201: Only assignment, call, increment, decrement, await, and new object expressions can be used as a statement
// partial F() => new();
Diagnostic(ErrorCode.ERR_IllegalStatement, "new()").WithLocation(6, 24),
// (8,1): error CS1022: Type or namespace definition, or end-of-file expected
// }
Diagnostic(ErrorCode.ERR_EOFExpected, "}").WithLocation(8, 1)
};

CreateCompilation(source, parseOptions: TestOptions.RegularNext).VerifyDiagnostics(expectedDiagnostics);
CreateCompilation(source).VerifyDiagnostics(expectedDiagnostics);
}

[Fact]
public void ReturningPartialType_LocalFunction_TopLevel()
{
var source = """
System.Console.Write(F().GetType().Name);
partial F() => new();
class @partial;
""";
CompileAndVerify(source, parseOptions: TestOptions.Regular13, expectedOutput: "partial").VerifyDiagnostics();

var expectedDiagnostics = new[]
{
// (1,22): error CS0103: The name 'F' does not exist in the current context
// System.Console.Write(F().GetType().Name);
Diagnostic(ErrorCode.ERR_NameNotInContext, "F").WithArguments("F").WithLocation(1, 22),
// (2,9): error CS0116: A namespace cannot directly contain members such as fields, methods or statements
// partial F() => new();
Diagnostic(ErrorCode.ERR_NamespaceUnexpected, "F").WithLocation(2, 9),
// (2,10): error CS0201: Only assignment, call, increment, decrement, await, and new object expressions can be used as a statement
// partial F() => new();
Diagnostic(ErrorCode.ERR_IllegalStatement, "() => new()").WithLocation(2, 10)
};

CreateCompilation(source, parseOptions: TestOptions.RegularNext).VerifyDiagnostics(expectedDiagnostics);
CreateCompilation(source).VerifyDiagnostics(expectedDiagnostics);
}

[Fact]
public void ReturningPartialType_Method()
{
var source = """
class C
{
partial F() => new();
static void Main()
{
System.Console.Write(new C().F().GetType().Name);
}
}
class @partial;
""";
CompileAndVerify(source, parseOptions: TestOptions.Regular13, expectedOutput: "partial").VerifyDiagnostics();

var expectedDiagnostics = new[]
{
// (3,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method or property return type.
// partial F() => new();
Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(3, 5),
// (3,13): error CS1520: Method must have a return type
// partial F() => new();
Diagnostic(ErrorCode.ERR_MemberNeedsType, "F").WithLocation(3, 13),
// (3,20): error CS0201: Only assignment, call, increment, decrement, await, and new object expressions can be used as a statement
// partial F() => new();
Diagnostic(ErrorCode.ERR_IllegalStatement, "new()").WithLocation(3, 20),
// (6,38): error CS1061: 'C' does not contain a definition for 'F' and no accessible extension method 'F' accepting a first argument of type 'C' could be found (are you missing a using directive or an assembly reference?)
// System.Console.Write(new C().F().GetType().Name);
Diagnostic(ErrorCode.ERR_NoSuchMemberOrExtension, "F").WithArguments("C", "F").WithLocation(6, 38)
};

CreateCompilation(source, parseOptions: TestOptions.RegularNext).VerifyDiagnostics(expectedDiagnostics);
CreateCompilation(source).VerifyDiagnostics(expectedDiagnostics);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -901,48 +901,30 @@ sealed static partial I3() {}
targetFramework: _supportingFramework);

compilation1.VerifyDiagnostics(
// (4,19): error CS0246: The type or namespace name 'partial' could not be found (are you missing a using directive or an assembly reference?)
// (4,19): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method or property return type.
// sealed static partial I2();
Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "partial").WithArguments("partial").WithLocation(4, 19),
// (4,27): error CS0501: 'I2.I2()' must declare a body because it is not marked abstract, extern, or partial
Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(4, 19),
// (4,27): error CS0106: The modifier 'sealed' is not valid for this item
// sealed static partial I2();
Diagnostic(ErrorCode.ERR_ConcreteMissingBody, "I2").WithArguments("I2.I2()").WithLocation(4, 27),
// (4,27): error CS0542: 'I2': member names cannot be the same as their enclosing type
// sealed static partial I2();
Diagnostic(ErrorCode.ERR_MemberNameSameAsType, "I2").WithArguments("I2").WithLocation(4, 27),
// (9,12): error CS0246: The type or namespace name 'partial' could not be found (are you missing a using directive or an assembly reference?)
// static partial I2() {}
Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "partial").WithArguments("partial").WithLocation(9, 12),
// (9,20): error CS0542: 'I2': member names cannot be the same as their enclosing type
Diagnostic(ErrorCode.ERR_BadMemberFlag, "I2").WithArguments("sealed").WithLocation(4, 27),
// (9,12): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method or property return type.
// static partial I2() {}
Diagnostic(ErrorCode.ERR_MemberNameSameAsType, "I2").WithArguments("I2").WithLocation(9, 20),
Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(9, 12),
// (9,20): error CS0111: Type 'I2' already defines a member called 'I2' with the same parameter types
// static partial I2() {}
Diagnostic(ErrorCode.ERR_MemberAlreadyExists, "I2").WithArguments("I2", "I2").WithLocation(9, 20),
// (9,20): error CS0161: 'I2.I2()': not all code paths return a value
// static partial I2() {}
Diagnostic(ErrorCode.ERR_ReturnExpected, "I2").WithArguments("I2.I2()").WithLocation(9, 20),
// (14,12): error CS0246: The type or namespace name 'partial' could not be found (are you missing a using directive or an assembly reference?)
// (14,12): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method or property return type.
// static partial I3();
Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "partial").WithArguments("partial").WithLocation(14, 12),
// (14,20): error CS0501: 'I3.I3()' must declare a body because it is not marked abstract, extern, or partial
// static partial I3();
Diagnostic(ErrorCode.ERR_ConcreteMissingBody, "I3").WithArguments("I3.I3()").WithLocation(14, 20),
// (14,20): error CS0542: 'I3': member names cannot be the same as their enclosing type
// static partial I3();
Diagnostic(ErrorCode.ERR_MemberNameSameAsType, "I3").WithArguments("I3").WithLocation(14, 20),
// (19,19): error CS0246: The type or namespace name 'partial' could not be found (are you missing a using directive or an assembly reference?)
Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(14, 12),
// (19,19): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method or property return type.
// sealed static partial I3() {}
Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "partial").WithArguments("partial").WithLocation(19, 19),
// (19,27): error CS0542: 'I3': member names cannot be the same as their enclosing type
Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(19, 19),
// (19,27): error CS0106: The modifier 'sealed' is not valid for this item
// sealed static partial I3() {}
Diagnostic(ErrorCode.ERR_MemberNameSameAsType, "I3").WithArguments("I3").WithLocation(19, 27),
Diagnostic(ErrorCode.ERR_BadMemberFlag, "I3").WithArguments("sealed").WithLocation(19, 27),
// (19,27): error CS0111: Type 'I3' already defines a member called 'I3' with the same parameter types
// sealed static partial I3() {}
Diagnostic(ErrorCode.ERR_MemberAlreadyExists, "I3").WithArguments("I3", "I3").WithLocation(19, 27),
// (19,27): error CS0161: 'I3.I3()': not all code paths return a value
// sealed static partial I3() {}
Diagnostic(ErrorCode.ERR_ReturnExpected, "I3").WithArguments("I3.I3()").WithLocation(19, 27)
Diagnostic(ErrorCode.ERR_MemberAlreadyExists, "I3").WithArguments("I3", "I3").WithLocation(19, 27)
);
}

Expand Down
Loading