Skip to content

Commit 8b63d50

Browse files
authored
Parse partial events and constructors (#76860)
* Parse partial events and constructors * Update pre-existing tests * Explain why partial ctors are sometimes disallowed * Parse `partial` constructors unconditionally * Gate parsing on LangVersion * Extend tests and docs * Fix indentation * Rename combinatorial values * Test more lang versions * Parse partial events unconditionally
1 parent f0833b8 commit 8b63d50

File tree

9 files changed

+1771
-125
lines changed

9 files changed

+1771
-125
lines changed

docs/compilers/CSharp/Compiler Breaking Changes - DotNet 10.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,20 @@ unsafe record struct R(
291291
public bool Equals(R other) => true;
292292
}
293293
```
294+
295+
## `partial` cannot be a return type of methods
296+
297+
***Introduced in Visual Studio 2022 version 17.14***
298+
299+
The [partial events and constructors](https://github.com/dotnet/csharplang/issues/9058) language feature
300+
allows the `partial` modifier in more places and so it cannot be a return type unless escaped:
301+
302+
```cs
303+
class C
304+
{
305+
partial F() => new partial(); // previously worked
306+
@partial F() => new partial(); // workaround
307+
}
308+
309+
class partial { }
310+
```

src/Compilers/CSharp/Portable/Errors/MessageID.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ internal enum MessageID
296296
IDS_FeatureFirstClassSpan = MessageBase + 12849,
297297
IDS_FeatureUnboundGenericTypesInNameof = MessageBase + 12850,
298298
IDS_FeatureSimpleLambdaParameterModifiers = MessageBase + 12851,
299+
300+
IDS_FeaturePartialEventsAndConstructors = MessageBase + 12852,
299301
}
300302

301303
// Message IDs may refer to strings that need to be localized.
@@ -480,6 +482,7 @@ internal static LanguageVersion RequiredVersion(this MessageID feature)
480482
case MessageID.IDS_FeatureFirstClassSpan:
481483
case MessageID.IDS_FeatureUnboundGenericTypesInNameof:
482484
case MessageID.IDS_FeatureSimpleLambdaParameterModifiers:
485+
case MessageID.IDS_FeaturePartialEventsAndConstructors:
483486
return LanguageVersion.Preview;
484487

485488
// C# 13.0 features.

src/Compilers/CSharp/Portable/Parser/LanguageParser.cs

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1634,24 +1634,25 @@ private bool IsPartialType()
16341634

16351635
private bool IsPartialMember()
16361636
{
1637-
// note(cyrusn): this could have been written like so:
1638-
//
1639-
// return
1640-
// this.CurrentToken.ContextualKind == SyntaxKind.PartialKeyword &&
1641-
// this.PeekToken(1).Kind == SyntaxKind.VoidKeyword;
1642-
//
1643-
// However, we want to be lenient and allow the user to write
1644-
// 'partial' in most modifier lists. We will then provide them with
1645-
// a more specific message later in binding that they are doing
1646-
// something wrong.
1647-
//
1648-
// Some might argue that the simple check would suffice.
1649-
// However, we'd like to maintain behavior with
1650-
// previously shipped versions, and so we're keeping this code.
1637+
Debug.Assert(this.CurrentToken.ContextualKind == SyntaxKind.PartialKeyword);
1638+
1639+
// Check for:
1640+
// partial event
1641+
if (this.PeekToken(1).Kind == SyntaxKind.EventKeyword)
1642+
{
1643+
return true;
1644+
}
1645+
1646+
// Check for constructor:
1647+
// partial Identifier(
1648+
if (this.PeekToken(1).Kind == SyntaxKind.IdentifierToken &&
1649+
this.PeekToken(2).Kind == SyntaxKind.OpenParenToken)
1650+
{
1651+
return IsFeatureEnabled(MessageID.IDS_FeaturePartialEventsAndConstructors);
1652+
}
16511653

1652-
// Here we check for:
1654+
// Check for method/property:
16531655
// partial ReturnType MemberName
1654-
Debug.Assert(this.CurrentToken.ContextualKind == SyntaxKind.PartialKeyword);
16551656
using var _ = this.GetDisposableResetPoint(resetOnDispose: true);
16561657

16571658
this.EatToken(); // partial
@@ -5680,7 +5681,7 @@ private bool IsTrueIdentifier()
56805681
{
56815682
if (this.CurrentToken.Kind == SyntaxKind.IdentifierToken)
56825683
{
5683-
if (!IsCurrentTokenPartialKeywordOfPartialMethodOrType() &&
5684+
if (!IsCurrentTokenPartialKeywordOfPartialMemberOrType() &&
56845685
!IsCurrentTokenQueryKeywordInQuery() &&
56855686
!IsCurrentTokenWhereOfConstraintClause())
56865687
{
@@ -5727,7 +5728,7 @@ private SyntaxToken ParseIdentifierToken(ErrorCode code = ErrorCode.ERR_Identifi
57275728
// show the correct parameter help in this case. So, when we see "partial" we check if it's being used
57285729
// as an identifier or as a contextual keyword. If it's the latter then we bail out. See
57295730
// Bug: vswhidbey/542125
5730-
if (IsCurrentTokenPartialKeywordOfPartialMethodOrType() || IsCurrentTokenQueryKeywordInQuery())
5731+
if (IsCurrentTokenPartialKeywordOfPartialMemberOrType() || IsCurrentTokenQueryKeywordInQuery())
57315732
{
57325733
var result = CreateMissingIdentifierToken();
57335734
result = this.AddError(result, ErrorCode.ERR_InvalidExprTerm, this.CurrentToken.Text);
@@ -5754,7 +5755,7 @@ private bool IsCurrentTokenQueryKeywordInQuery()
57545755
return this.IsInQuery && this.IsCurrentTokenQueryContextualKeyword;
57555756
}
57565757

5757-
private bool IsCurrentTokenPartialKeywordOfPartialMethodOrType()
5758+
private bool IsCurrentTokenPartialKeywordOfPartialMemberOrType()
57585759
{
57595760
if (this.CurrentToken.ContextualKind == SyntaxKind.PartialKeyword)
57605761
{
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.CodeAnalysis.CSharp.Test.Utilities;
6+
using Xunit;
7+
8+
namespace Microsoft.CodeAnalysis.CSharp.UnitTests;
9+
10+
public sealed class PartialEventsAndConstructorsTests : CSharpTestBase
11+
{
12+
[Fact]
13+
public void ReturningPartialType_LocalFunction_InMethod()
14+
{
15+
var source = """
16+
class @partial
17+
{
18+
static void Main()
19+
{
20+
System.Console.Write(F().GetType().Name);
21+
partial F() => new();
22+
}
23+
}
24+
""";
25+
CompileAndVerify(source, parseOptions: TestOptions.Regular13, expectedOutput: "partial").VerifyDiagnostics();
26+
27+
var expectedDiagnostics = new[]
28+
{
29+
// (5,30): error CS0103: The name 'F' does not exist in the current context
30+
// System.Console.Write(F().GetType().Name);
31+
Diagnostic(ErrorCode.ERR_NameNotInContext, "F").WithArguments("F").WithLocation(5, 30),
32+
// (5,50): error CS1513: } expected
33+
// System.Console.Write(F().GetType().Name);
34+
Diagnostic(ErrorCode.ERR_RbraceExpected, "").WithLocation(5, 50),
35+
// (6,9): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method or property return type.
36+
// partial F() => new();
37+
Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(6, 9),
38+
// (6,17): error CS1520: Method must have a return type
39+
// partial F() => new();
40+
Diagnostic(ErrorCode.ERR_MemberNeedsType, "F").WithLocation(6, 17),
41+
// (6,24): error CS0201: Only assignment, call, increment, decrement, await, and new object expressions can be used as a statement
42+
// partial F() => new();
43+
Diagnostic(ErrorCode.ERR_IllegalStatement, "new()").WithLocation(6, 24),
44+
// (8,1): error CS1022: Type or namespace definition, or end-of-file expected
45+
// }
46+
Diagnostic(ErrorCode.ERR_EOFExpected, "}").WithLocation(8, 1)
47+
};
48+
49+
CreateCompilation(source, parseOptions: TestOptions.RegularNext).VerifyDiagnostics(expectedDiagnostics);
50+
CreateCompilation(source).VerifyDiagnostics(expectedDiagnostics);
51+
}
52+
53+
[Fact]
54+
public void ReturningPartialType_LocalFunction_TopLevel()
55+
{
56+
var source = """
57+
System.Console.Write(F().GetType().Name);
58+
partial F() => new();
59+
class @partial;
60+
""";
61+
CompileAndVerify(source, parseOptions: TestOptions.Regular13, expectedOutput: "partial").VerifyDiagnostics();
62+
63+
var expectedDiagnostics = new[]
64+
{
65+
// (1,22): error CS0103: The name 'F' does not exist in the current context
66+
// System.Console.Write(F().GetType().Name);
67+
Diagnostic(ErrorCode.ERR_NameNotInContext, "F").WithArguments("F").WithLocation(1, 22),
68+
// (2,9): error CS0116: A namespace cannot directly contain members such as fields, methods or statements
69+
// partial F() => new();
70+
Diagnostic(ErrorCode.ERR_NamespaceUnexpected, "F").WithLocation(2, 9),
71+
// (2,10): error CS0201: Only assignment, call, increment, decrement, await, and new object expressions can be used as a statement
72+
// partial F() => new();
73+
Diagnostic(ErrorCode.ERR_IllegalStatement, "() => new()").WithLocation(2, 10)
74+
};
75+
76+
CreateCompilation(source, parseOptions: TestOptions.RegularNext).VerifyDiagnostics(expectedDiagnostics);
77+
CreateCompilation(source).VerifyDiagnostics(expectedDiagnostics);
78+
}
79+
80+
[Fact]
81+
public void ReturningPartialType_Method()
82+
{
83+
var source = """
84+
class C
85+
{
86+
partial F() => new();
87+
static void Main()
88+
{
89+
System.Console.Write(new C().F().GetType().Name);
90+
}
91+
}
92+
class @partial;
93+
""";
94+
CompileAndVerify(source, parseOptions: TestOptions.Regular13, expectedOutput: "partial").VerifyDiagnostics();
95+
96+
var expectedDiagnostics = new[]
97+
{
98+
// (3,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method or property return type.
99+
// partial F() => new();
100+
Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(3, 5),
101+
// (3,13): error CS1520: Method must have a return type
102+
// partial F() => new();
103+
Diagnostic(ErrorCode.ERR_MemberNeedsType, "F").WithLocation(3, 13),
104+
// (3,20): error CS0201: Only assignment, call, increment, decrement, await, and new object expressions can be used as a statement
105+
// partial F() => new();
106+
Diagnostic(ErrorCode.ERR_IllegalStatement, "new()").WithLocation(3, 20),
107+
// (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?)
108+
// System.Console.Write(new C().F().GetType().Name);
109+
Diagnostic(ErrorCode.ERR_NoSuchMemberOrExtension, "F").WithArguments("C", "F").WithLocation(6, 38)
110+
};
111+
112+
CreateCompilation(source, parseOptions: TestOptions.RegularNext).VerifyDiagnostics(expectedDiagnostics);
113+
CreateCompilation(source).VerifyDiagnostics(expectedDiagnostics);
114+
}
115+
}

src/Compilers/CSharp/Test/Symbol/Symbols/StaticAbstractMembersInInterfacesTests.cs

Lines changed: 13 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -901,48 +901,30 @@ sealed static partial I3() {}
901901
targetFramework: _supportingFramework);
902902

903903
compilation1.VerifyDiagnostics(
904-
// (4,19): error CS0246: The type or namespace name 'partial' could not be found (are you missing a using directive or an assembly reference?)
904+
// (4,19): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method or property return type.
905905
// sealed static partial I2();
906-
Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "partial").WithArguments("partial").WithLocation(4, 19),
907-
// (4,27): error CS0501: 'I2.I2()' must declare a body because it is not marked abstract, extern, or partial
906+
Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(4, 19),
907+
// (4,27): error CS0106: The modifier 'sealed' is not valid for this item
908908
// sealed static partial I2();
909-
Diagnostic(ErrorCode.ERR_ConcreteMissingBody, "I2").WithArguments("I2.I2()").WithLocation(4, 27),
910-
// (4,27): error CS0542: 'I2': member names cannot be the same as their enclosing type
911-
// sealed static partial I2();
912-
Diagnostic(ErrorCode.ERR_MemberNameSameAsType, "I2").WithArguments("I2").WithLocation(4, 27),
913-
// (9,12): error CS0246: The type or namespace name 'partial' could not be found (are you missing a using directive or an assembly reference?)
914-
// static partial I2() {}
915-
Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "partial").WithArguments("partial").WithLocation(9, 12),
916-
// (9,20): error CS0542: 'I2': member names cannot be the same as their enclosing type
909+
Diagnostic(ErrorCode.ERR_BadMemberFlag, "I2").WithArguments("sealed").WithLocation(4, 27),
910+
// (9,12): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method or property return type.
917911
// static partial I2() {}
918-
Diagnostic(ErrorCode.ERR_MemberNameSameAsType, "I2").WithArguments("I2").WithLocation(9, 20),
912+
Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(9, 12),
919913
// (9,20): error CS0111: Type 'I2' already defines a member called 'I2' with the same parameter types
920914
// static partial I2() {}
921915
Diagnostic(ErrorCode.ERR_MemberAlreadyExists, "I2").WithArguments("I2", "I2").WithLocation(9, 20),
922-
// (9,20): error CS0161: 'I2.I2()': not all code paths return a value
923-
// static partial I2() {}
924-
Diagnostic(ErrorCode.ERR_ReturnExpected, "I2").WithArguments("I2.I2()").WithLocation(9, 20),
925-
// (14,12): error CS0246: The type or namespace name 'partial' could not be found (are you missing a using directive or an assembly reference?)
916+
// (14,12): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method or property return type.
926917
// static partial I3();
927-
Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "partial").WithArguments("partial").WithLocation(14, 12),
928-
// (14,20): error CS0501: 'I3.I3()' must declare a body because it is not marked abstract, extern, or partial
929-
// static partial I3();
930-
Diagnostic(ErrorCode.ERR_ConcreteMissingBody, "I3").WithArguments("I3.I3()").WithLocation(14, 20),
931-
// (14,20): error CS0542: 'I3': member names cannot be the same as their enclosing type
932-
// static partial I3();
933-
Diagnostic(ErrorCode.ERR_MemberNameSameAsType, "I3").WithArguments("I3").WithLocation(14, 20),
934-
// (19,19): error CS0246: The type or namespace name 'partial' could not be found (are you missing a using directive or an assembly reference?)
918+
Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(14, 12),
919+
// (19,19): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method or property return type.
935920
// sealed static partial I3() {}
936-
Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "partial").WithArguments("partial").WithLocation(19, 19),
937-
// (19,27): error CS0542: 'I3': member names cannot be the same as their enclosing type
921+
Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(19, 19),
922+
// (19,27): error CS0106: The modifier 'sealed' is not valid for this item
938923
// sealed static partial I3() {}
939-
Diagnostic(ErrorCode.ERR_MemberNameSameAsType, "I3").WithArguments("I3").WithLocation(19, 27),
924+
Diagnostic(ErrorCode.ERR_BadMemberFlag, "I3").WithArguments("sealed").WithLocation(19, 27),
940925
// (19,27): error CS0111: Type 'I3' already defines a member called 'I3' with the same parameter types
941926
// sealed static partial I3() {}
942-
Diagnostic(ErrorCode.ERR_MemberAlreadyExists, "I3").WithArguments("I3", "I3").WithLocation(19, 27),
943-
// (19,27): error CS0161: 'I3.I3()': not all code paths return a value
944-
// sealed static partial I3() {}
945-
Diagnostic(ErrorCode.ERR_ReturnExpected, "I3").WithArguments("I3.I3()").WithLocation(19, 27)
927+
Diagnostic(ErrorCode.ERR_MemberAlreadyExists, "I3").WithArguments("I3", "I3").WithLocation(19, 27)
946928
);
947929
}
948930

0 commit comments

Comments
 (0)