From 88cdb8d17474b47c45304c0a5dbfc2e45c67fee2 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 24 Aug 2026 21:16:08 +0200 Subject: [PATCH 01/12] Avoid duplicate misplaced partial diagnostics Check partial ordering only for declarations that permit the modifier, leaving invalid declarations to the existing modifier check. --- .../Declarations/DeclarationTreeBuilder.cs | 7 +++ .../Portable/Symbols/Source/ModifierUtils.cs | 47 +++++++++++++------ .../DefaultInterfaceImplementationTests.cs | 6 --- .../CSharp/Test/Symbol/Symbols/EnumTests.cs | 3 -- .../Symbol/Symbols/Source/DelegateTests.cs | 3 -- .../StaticAbstractMembersInInterfacesTests.cs | 12 ----- .../Test/Symbol/Symbols/SymbolErrorTests.cs | 15 ------ .../Symbols/UserDefinedOperatorErrorTests.cs | 3 -- .../AccessorDeclarationParsingTests.cs | 3 -- 9 files changed, 40 insertions(+), 59 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs b/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs index d95718907da87..89d5822987077 100644 --- a/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs +++ b/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs @@ -785,6 +785,13 @@ ClassDeclarationSyntax or } var modifiers = node.Modifiers.ToDeclarationModifiers(isForTypeDeclaration: true, diagnostics: diagnostics); + // Extensions never allow 'partial', so there is no need to report an ordering error. + // The invalid modifier is reported later. + if (kind != DeclarationKind.Extension) + { + node.Modifiers.CheckPartialModifierOrder(diagnostics, isOrdinaryMethod: false); + } + var quickAttributes = GetQuickAttributes(node.AttributeLists); foreach (var modifier in node.Modifiers) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs b/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs index f4f78047c2246..ba88aea089f95 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs @@ -23,7 +23,15 @@ internal static DeclarationModifiers MakeAndCheckNonTypeMemberModifiers( out bool modifierErrors, out bool hasExplicitAccessModifier) { - var result = modifiers.ToDeclarationModifiers(isForTypeDeclaration: false, diagnostics.DiagnosticBag ?? new DiagnosticBag(), isOrdinaryMethod: isOrdinaryMethod); + var diagnosticBag = diagnostics.DiagnosticBag ?? new DiagnosticBag(); + var result = modifiers.ToDeclarationModifiers(isForTypeDeclaration: false, diagnosticBag); + // There is no need to report an ordering error when 'partial' is not allowed. + // The invalid modifier is reported below. + if ((allowedModifiers & DeclarationModifiers.Partial) != 0) + { + modifiers.CheckPartialModifierOrder(diagnosticBag, isOrdinaryMethod); + } + result = CheckModifiers(isForTypeDeclaration: false, isForInterfaceMember, result, allowedModifiers, errorLocation, diagnostics, modifiers, out modifierErrors); var readonlyToken = modifiers.FirstOrDefault(SyntaxKind.ReadOnlyKeyword); @@ -463,7 +471,9 @@ private static DeclarationModifiers GetDeclarationModifiersAndCheckForDuplicateM } public static DeclarationModifiers ToDeclarationModifiers( - this SyntaxTokenList modifiers, bool isForTypeDeclaration, DiagnosticBag diagnostics, bool isOrdinaryMethod = false) + this SyntaxTokenList modifiers, + bool isForTypeDeclaration, + DiagnosticBag diagnostics) { var result = GetDeclarationModifiersAndCheckForDuplicateModifiers(modifiers, diagnostics); if ((result & DeclarationModifiers.Partial) == DeclarationModifiers.Partial) @@ -473,18 +483,6 @@ public static DeclarationModifiers ToDeclarationModifiers( var messageId = isForTypeDeclaration ? MessageID.IDS_FeaturePartialTypes : MessageID.IDS_FeaturePartialMethod; messageId.CheckFeatureAvailability(diagnostics, modifier); - - // `partial` must always be the last modifier according to the language. However, there was a bug - // where we allowed `partial async` at the end of modifiers on methods. We keep this behavior for - // backcompat. - var isLast = i == modifiers.Count - 1; - var isPartialAsyncMethod = isOrdinaryMethod && i == modifiers.Count - 2 && modifiers[i + 1].ContextualKind() is SyntaxKind.AsyncKeyword; - if (!isLast && !isPartialAsyncMethod) - { - diagnostics.Add( - ErrorCode.ERR_PartialMisplaced, - modifier.GetLocation()); - } } switch (result & DeclarationModifiers.AccessibilityMask) @@ -505,6 +503,27 @@ public static DeclarationModifiers ToDeclarationModifiers( return result; } + internal static void CheckPartialModifierOrder(this SyntaxTokenList modifiers, DiagnosticBag diagnostics, bool isOrdinaryMethod) + { + var i = modifiers.IndexOf(SyntaxKind.PartialKeyword); + if (i < 0) + { + return; + } + + // `partial` must always be the last modifier according to the language. However, there was a bug + // where we allowed `partial async` at the end of modifiers on methods. We keep this behavior for + // backcompat. + var isLast = i == modifiers.Count - 1; + var isPartialAsyncMethod = isOrdinaryMethod && i == modifiers.Count - 2 && modifiers[i + 1].ContextualKind() is SyntaxKind.AsyncKeyword; + if (!isLast && !isPartialAsyncMethod) + { + diagnostics.Add( + ErrorCode.ERR_PartialMisplaced, + modifiers[i].GetLocation()); + } + } + private static void ReportDuplicateModifiers( SyntaxToken modifierToken, DeclarationModifiers modifierKind, diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/DefaultInterfaceImplementationTests.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/DefaultInterfaceImplementationTests.cs index 71264c9e6c860..c7e0eb147ad30 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/DefaultInterfaceImplementationTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/DefaultInterfaceImplementationTests.cs @@ -45105,18 +45105,12 @@ interface I19 // (66,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // partial static I14(); Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(66, 5), - // (66,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. - // partial static I14(); - Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(66, 5), // (70,12): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // static partial I15(); Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(70, 12), // (74,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // partial static I16() {} Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(74, 5), - // (74,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. - // partial static I16() {} - Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(74, 5), // (78,12): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // static partial I17() => throw null; Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(78, 12), diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/EnumTests.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/EnumTests.cs index e615c2da0e765..6579e26499cb1 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/EnumTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/EnumTests.cs @@ -404,9 +404,6 @@ static void Main() public void PartialPublicEnum() { CreateCompilation("partial public enum E { }").VerifyDiagnostics( - // (1,1): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method return type. - // partial public enum E { } - Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(1, 1), // (1,21): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method return type. // partial public enum E { } Diagnostic(ErrorCode.ERR_PartialMisplaced, "E").WithLocation(1, 21)); diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/Source/DelegateTests.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/Source/DelegateTests.cs index aea7c81f322db..7e991e74963ba 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/Source/DelegateTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/Source/DelegateTests.cs @@ -841,9 +841,6 @@ public static void Main() public void PartialPublicDelegate() { CreateCompilation("partial public delegate void M();").VerifyDiagnostics( - // (1,1): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method return type. - // partial public delegate void M(); - Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(1, 1), // (1,30): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method return type. // partial public delegate void M(); Diagnostic(ErrorCode.ERR_PartialMisplaced, "M").WithLocation(1, 30)); diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/StaticAbstractMembersInInterfacesTests.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/StaticAbstractMembersInInterfacesTests.cs index 7e4fa035b597c..4f36087f0d1c7 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/StaticAbstractMembersInInterfacesTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/StaticAbstractMembersInInterfacesTests.cs @@ -822,30 +822,18 @@ partial sealed static I3() {} // (9,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // partial sealed static I2(); Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(9, 5), - // (9,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. - // partial sealed static I2(); - Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(9, 5), // (9,27): error CS0106: The modifier 'sealed' is not valid for this item // partial sealed static I2(); Diagnostic(ErrorCode.ERR_BadMemberFlag, "I2").WithArguments("sealed").WithLocation(9, 27), // (14,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // partial static I2() {} Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(14, 5), - // (14,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. - // partial static I2() {} - Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(14, 5), // (14,20): error CS0111: Type 'I2' already defines a member called 'I2' with the same parameter types // partial static I2() {} Diagnostic(ErrorCode.ERR_MemberAlreadyExists, "I2").WithArguments("I2", "I2").WithLocation(14, 20), // (19,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // partial static I3(); Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(19, 5), - // (19,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. - // partial static I3(); - Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(19, 5), - // (24,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. - // partial sealed static I3() {} - Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(24, 5), // (24,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // partial sealed static I3() {} Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(24, 5), diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/SymbolErrorTests.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/SymbolErrorTests.cs index 17c58a0914d23..17fd0e8089b91 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/SymbolErrorTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/SymbolErrorTests.cs @@ -22309,24 +22309,12 @@ partial public static PartialPublicStaticCtor() { } // 6.cs(3,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // partial static public PartialStaticPublicCtor() { } Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(3, 5), - // 6.cs(3,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. - // partial static public PartialStaticPublicCtor() { } - Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(3, 5), - // 0.cs(3,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. - // partial static PartialStaticCtor() { } - Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(3, 5), // 0.cs(3,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // partial static PartialStaticCtor() { } Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(3, 5), // 7.cs(3,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // partial public static PartialPublicStaticCtor() { } Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(3, 5), - // 7.cs(3,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. - // partial public static PartialPublicStaticCtor() { } - Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(3, 5), - // 3.cs(3,12): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. - // public partial static PublicPartialStaticCtor() { } - Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(3, 12), // 3.cs(3,12): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // public partial static PublicPartialStaticCtor() { } Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(3, 12), @@ -22336,9 +22324,6 @@ partial public static PartialPublicStaticCtor() { } // 5.cs(3,12): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // static partial public StaticPartialPublicCtor() { } Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(3, 12), - // 5.cs(3,12): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. - // static partial public StaticPartialPublicCtor() { } - Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(3, 12), // 2.cs(3,19): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // public static partial PublicStaticPartialCtor() { } Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(3, 19), diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/UserDefinedOperatorErrorTests.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/UserDefinedOperatorErrorTests.cs index f17019698cf74..c718704faf5b5 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/UserDefinedOperatorErrorTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/UserDefinedOperatorErrorTests.cs @@ -195,9 +195,6 @@ partial class C 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), diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/AccessorDeclarationParsingTests.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/AccessorDeclarationParsingTests.cs index 02f9ea10db7c2..8d40e31bfd045 100644 --- a/src/Compilers/CSharp/Test/Syntax/Parsing/AccessorDeclarationParsingTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/AccessorDeclarationParsingTests.cs @@ -99,9 +99,6 @@ class C EOF(); CreateCompilation(source).VerifyDiagnostics( - // (3,13): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. - // int P { partial ref get; } - Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(3, 13), // (3,13): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // int P { partial ref get; } Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(3, 13), From f4e9537814c4510feb59c0952ba007214364ac96 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Wed, 26 Aug 2026 07:49:13 -0400 Subject: [PATCH 02/12] Update partial constructor diagnostic baseline Remove duplicate misplaced-partial expectations after modifier validation begins reporting each occurrence once. --- .../CSharp/Test/Emit3/PartialEventsAndConstructorsTests.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Compilers/CSharp/Test/Emit3/PartialEventsAndConstructorsTests.cs b/src/Compilers/CSharp/Test/Emit3/PartialEventsAndConstructorsTests.cs index 27bf1c59a803a..e43387319a4aa 100644 --- a/src/Compilers/CSharp/Test/Emit3/PartialEventsAndConstructorsTests.cs +++ b/src/Compilers/CSharp/Test/Emit3/PartialEventsAndConstructorsTests.cs @@ -747,12 +747,6 @@ partial static C() { } // (3,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // partial static C(); Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(3, 5), - // (3,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. - // partial static C(); - Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(3, 5), - // (4,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. - // partial static C() { } - Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(4, 5), // (4,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // partial static C() { } Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(4, 5), From ebd33f18df918117dfe8526f72780090e2f7113e Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Wed, 26 Aug 2026 09:34:05 -0400 Subject: [PATCH 03/12] Centralize partial modifier diagnostics Let modifier conversion validate both whether partial is allowed and whether it is ordered correctly, so later modifier checks do not report the same token again. --- .../CSharp/Portable/Binder/Binder_Lambda.cs | 7 +- .../Declarations/DeclarationTreeBuilder.cs | 27 ++++--- .../Symbols/Source/LocalFunctionSymbol.cs | 6 +- .../Portable/Symbols/Source/ModifierUtils.cs | 81 ++++++++----------- .../CSharp/Test/Symbol/Symbols/EnumTests.cs | 4 +- .../Symbol/Symbols/Source/DelegateTests.cs | 4 +- 6 files changed, 65 insertions(+), 64 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs index 1703813ec77b2..3c508bb7e3c80 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs @@ -373,7 +373,12 @@ private UnboundLambda BindAnonymousFunction(AnonymousFunctionExpressionSyntax sy // Parser will only have accepted static/async as allowed modifiers on this construct. // However, it may have accepted duplicates of those modifiers. Ensure that any dupes // are reported now. - ModifierUtils.ToDeclarationModifiers(syntax.Modifiers, isForTypeDeclaration: false, diagnostics.DiagnosticBag ?? new DiagnosticBag()); + ModifierUtils.ToDeclarationModifiers( + syntax.Modifiers, + isForTypeDeclaration: false, + diagnostics.DiagnosticBag ?? new DiagnosticBag(), + isOrdinaryMethod: false, + allowsPartialModifier: false); if (data.HasSignature) { diff --git a/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs b/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs index 89d5822987077..e08e0157dd327 100644 --- a/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs +++ b/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs @@ -784,13 +784,12 @@ ClassDeclarationSyntax or MessageID.IDS_FeatureUnions.CheckFeatureAvailability(diagnostics, node, node.Keyword.GetLocation()); } - var modifiers = node.Modifiers.ToDeclarationModifiers(isForTypeDeclaration: true, diagnostics: diagnostics); - // Extensions never allow 'partial', so there is no need to report an ordering error. - // The invalid modifier is reported later. - if (kind != DeclarationKind.Extension) - { - node.Modifiers.CheckPartialModifierOrder(diagnostics, isOrdinaryMethod: false); - } + // Extensions never allow 'partial'. + var modifiers = node.Modifiers.ToDeclarationModifiers( + isForTypeDeclaration: true, + diagnostics, + isOrdinaryMethod: false, + allowsPartialModifier: kind != DeclarationKind.Extension); var quickAttributes = GetQuickAttributes(node.AttributeLists); @@ -856,7 +855,12 @@ public override SingleNamespaceOrTypeDeclaration VisitDelegateDeclaration(Delega declFlags |= SingleTypeDeclaration.TypeDeclarationFlags.HasAnyNontypeMembers; - var modifiers = node.Modifiers.ToDeclarationModifiers(isForTypeDeclaration: true, diagnostics: diagnostics); + var modifiers = node.Modifiers.ToDeclarationModifiers( + isForTypeDeclaration: true, + diagnostics, + isOrdinaryMethod: false, + allowsPartialModifier: false); + var quickAttributes = DeclarationTreeBuilder.GetQuickAttributes(node.AttributeLists); return new SingleTypeDeclaration( @@ -889,7 +893,12 @@ public override SingleNamespaceOrTypeDeclaration VisitEnumDeclaration(EnumDeclar var memberNames = GetEnumMemberNames(node, ref declFlags); var diagnostics = DiagnosticBag.GetInstance(); - var modifiers = node.Modifiers.ToDeclarationModifiers(isForTypeDeclaration: true, diagnostics: diagnostics); + var modifiers = node.Modifiers.ToDeclarationModifiers( + isForTypeDeclaration: true, + diagnostics, + isOrdinaryMethod: false, + allowsPartialModifier: false); + var quickAttributes = DeclarationTreeBuilder.GetQuickAttributes(node.AttributeLists); if (node.OpenBraceToken == default && node.CloseBraceToken == default && node.SemicolonToken != default) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs index 3d0cca2088af0..16b6224364f3c 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs @@ -50,7 +50,11 @@ public LocalFunctionSymbol( _declarationModifiers = DeclarationModifiers.Private | - syntax.Modifiers.ToDeclarationModifiers(isForTypeDeclaration: false, diagnostics: _declarationDiagnostics); + syntax.Modifiers.ToDeclarationModifiers( + isForTypeDeclaration: false, + diagnostics: _declarationDiagnostics, + isOrdinaryMethod: false, + allowsPartialModifier: false); var diagnostics = BindingDiagnosticBag.GetInstance(); Debug.Assert(diagnostics.DiagnosticBag is { }); diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs b/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs index ba88aea089f95..379e2977e5d14 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs @@ -24,15 +24,20 @@ internal static DeclarationModifiers MakeAndCheckNonTypeMemberModifiers( out bool hasExplicitAccessModifier) { var diagnosticBag = diagnostics.DiagnosticBag ?? new DiagnosticBag(); - var result = modifiers.ToDeclarationModifiers(isForTypeDeclaration: false, diagnosticBag); - // There is no need to report an ordering error when 'partial' is not allowed. - // The invalid modifier is reported below. - if ((allowedModifiers & DeclarationModifiers.Partial) != 0) - { - modifiers.CheckPartialModifierOrder(diagnosticBag, isOrdinaryMethod); - } - - result = CheckModifiers(isForTypeDeclaration: false, isForInterfaceMember, result, allowedModifiers, errorLocation, diagnostics, modifiers, out modifierErrors); + var result = modifiers.ToDeclarationModifiers( + isForTypeDeclaration: false, + diagnosticBag, + isOrdinaryMethod, + allowsPartialModifier: (allowedModifiers & DeclarationModifiers.Partial) != 0); + result = CheckModifiers( + isForTypeDeclaration: false, + isForInterfaceMember, + result, + allowedModifiers, + errorLocation, + diagnostics, + modifiers, + out modifierErrors); var readonlyToken = modifiers.FirstOrDefault(SyntaxKind.ReadOnlyKeyword); if (readonlyToken.Parent is MethodDeclarationSyntax or AccessorDeclarationSyntax or BasePropertyDeclarationSyntax or EventDeclarationSyntax) @@ -90,8 +95,9 @@ internal static DeclarationModifiers CheckModifiers( switch (oneError) { case DeclarationModifiers.Partial: - // Provide a specialized error message in the case of partial. - ReportPartialError(errorLocation, diagnostics, modifierTokens); + // ToDeclarationModifiers reports all invalid 'partial' modifiers. That check + // lives there because it validates both whether 'partial' is allowed and + // whether it appears in the correct position. break; case DeclarationModifiers.Abstract: @@ -151,22 +157,6 @@ internal static void CheckScopedModifierAvailability(CSharpSyntaxNode syntax, Sy } } - private static void ReportPartialError(Location errorLocation, BindingDiagnosticBag diagnostics, SyntaxTokenList? modifierTokens) - { - // If we can find the 'partial' token, report it on that. - if (modifierTokens != null) - { - var partialToken = modifierTokens.Value.FirstOrDefault(SyntaxKind.PartialKeyword); - if (partialToken != default) - { - diagnostics.Add(ErrorCode.ERR_PartialMisplaced, partialToken.GetLocation()); - return; - } - } - - diagnostics.Add(ErrorCode.ERR_PartialMisplaced, errorLocation); - } - internal static void ReportDefaultInterfaceImplementationModifiers( bool hasBody, DeclarationModifiers modifiers, @@ -473,7 +463,9 @@ private static DeclarationModifiers GetDeclarationModifiersAndCheckForDuplicateM public static DeclarationModifiers ToDeclarationModifiers( this SyntaxTokenList modifiers, bool isForTypeDeclaration, - DiagnosticBag diagnostics) + DiagnosticBag diagnostics, + bool isOrdinaryMethod, + bool allowsPartialModifier) { var result = GetDeclarationModifiersAndCheckForDuplicateModifiers(modifiers, diagnostics); if ((result & DeclarationModifiers.Partial) == DeclarationModifiers.Partial) @@ -483,6 +475,18 @@ public static DeclarationModifiers ToDeclarationModifiers( var messageId = isForTypeDeclaration ? MessageID.IDS_FeaturePartialTypes : MessageID.IDS_FeaturePartialMethod; messageId.CheckFeatureAvailability(diagnostics, modifier); + + // `partial` must always be the last modifier according to the language. However, there was a bug + // where we allowed `partial async` at the end of modifiers on methods. We keep this behavior for + // backcompat. + var isLast = i == modifiers.Count - 1; + var isPartialAsyncMethod = isOrdinaryMethod && i == modifiers.Count - 2 && modifiers[i + 1].ContextualKind() is SyntaxKind.AsyncKeyword; + if (!allowsPartialModifier || (!isLast && !isPartialAsyncMethod)) + { + diagnostics.Add( + ErrorCode.ERR_PartialMisplaced, + modifier.GetLocation()); + } } switch (result & DeclarationModifiers.AccessibilityMask) @@ -503,27 +507,6 @@ public static DeclarationModifiers ToDeclarationModifiers( return result; } - internal static void CheckPartialModifierOrder(this SyntaxTokenList modifiers, DiagnosticBag diagnostics, bool isOrdinaryMethod) - { - var i = modifiers.IndexOf(SyntaxKind.PartialKeyword); - if (i < 0) - { - return; - } - - // `partial` must always be the last modifier according to the language. However, there was a bug - // where we allowed `partial async` at the end of modifiers on methods. We keep this behavior for - // backcompat. - var isLast = i == modifiers.Count - 1; - var isPartialAsyncMethod = isOrdinaryMethod && i == modifiers.Count - 2 && modifiers[i + 1].ContextualKind() is SyntaxKind.AsyncKeyword; - if (!isLast && !isPartialAsyncMethod) - { - diagnostics.Add( - ErrorCode.ERR_PartialMisplaced, - modifiers[i].GetLocation()); - } - } - private static void ReportDuplicateModifiers( SyntaxToken modifierToken, DeclarationModifiers modifierKind, diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/EnumTests.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/EnumTests.cs index 6579e26499cb1..38ad26de9db4a 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/EnumTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/EnumTests.cs @@ -404,9 +404,9 @@ static void Main() public void PartialPublicEnum() { CreateCompilation("partial public enum E { }").VerifyDiagnostics( - // (1,21): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method return type. + // (1,1): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method return type. // partial public enum E { } - Diagnostic(ErrorCode.ERR_PartialMisplaced, "E").WithLocation(1, 21)); + Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(1, 1)); } } } diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/Source/DelegateTests.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/Source/DelegateTests.cs index 7e991e74963ba..f705bd5c8fab3 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/Source/DelegateTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/Source/DelegateTests.cs @@ -841,9 +841,9 @@ public static void Main() public void PartialPublicDelegate() { CreateCompilation("partial public delegate void M();").VerifyDiagnostics( - // (1,30): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method return type. + // (1,1): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method return type. // partial public delegate void M(); - Diagnostic(ErrorCode.ERR_PartialMisplaced, "M").WithLocation(1, 30)); + Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(1, 1)); } [Fact] From 0e18b3c00317f0e24d726ecac52fbb3524f6c7fe Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Wed, 26 Aug 2026 09:35:58 -0400 Subject: [PATCH 04/12] Group modifier flags before diagnostics --- .../CSharp/Portable/Binder/Binder_Lambda.cs | 4 ++-- .../Portable/Declarations/DeclarationTreeBuilder.cs | 12 ++++++------ .../Portable/Symbols/Source/LocalFunctionSymbol.cs | 4 ++-- .../CSharp/Portable/Symbols/Source/ModifierUtils.cs | 10 +++++----- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs index 3c508bb7e3c80..2730a69c638e5 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs @@ -376,9 +376,9 @@ private UnboundLambda BindAnonymousFunction(AnonymousFunctionExpressionSyntax sy ModifierUtils.ToDeclarationModifiers( syntax.Modifiers, isForTypeDeclaration: false, - diagnostics.DiagnosticBag ?? new DiagnosticBag(), isOrdinaryMethod: false, - allowsPartialModifier: false); + allowsPartialModifier: false, + diagnostics.DiagnosticBag ?? new DiagnosticBag()); if (data.HasSignature) { diff --git a/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs b/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs index e08e0157dd327..7a42133915f2b 100644 --- a/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs +++ b/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs @@ -787,9 +787,9 @@ ClassDeclarationSyntax or // Extensions never allow 'partial'. var modifiers = node.Modifiers.ToDeclarationModifiers( isForTypeDeclaration: true, - diagnostics, isOrdinaryMethod: false, - allowsPartialModifier: kind != DeclarationKind.Extension); + allowsPartialModifier: kind != DeclarationKind.Extension, + diagnostics); var quickAttributes = GetQuickAttributes(node.AttributeLists); @@ -857,9 +857,9 @@ public override SingleNamespaceOrTypeDeclaration VisitDelegateDeclaration(Delega var modifiers = node.Modifiers.ToDeclarationModifiers( isForTypeDeclaration: true, - diagnostics, isOrdinaryMethod: false, - allowsPartialModifier: false); + allowsPartialModifier: false, + diagnostics); var quickAttributes = DeclarationTreeBuilder.GetQuickAttributes(node.AttributeLists); @@ -895,9 +895,9 @@ public override SingleNamespaceOrTypeDeclaration VisitEnumDeclaration(EnumDeclar var diagnostics = DiagnosticBag.GetInstance(); var modifiers = node.Modifiers.ToDeclarationModifiers( isForTypeDeclaration: true, - diagnostics, isOrdinaryMethod: false, - allowsPartialModifier: false); + allowsPartialModifier: false, + diagnostics); var quickAttributes = DeclarationTreeBuilder.GetQuickAttributes(node.AttributeLists); diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs index 16b6224364f3c..5a275b623af57 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs @@ -52,9 +52,9 @@ public LocalFunctionSymbol( DeclarationModifiers.Private | syntax.Modifiers.ToDeclarationModifiers( isForTypeDeclaration: false, - diagnostics: _declarationDiagnostics, isOrdinaryMethod: false, - allowsPartialModifier: false); + allowsPartialModifier: false, + diagnostics: _declarationDiagnostics); var diagnostics = BindingDiagnosticBag.GetInstance(); Debug.Assert(diagnostics.DiagnosticBag is { }); diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs b/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs index 379e2977e5d14..c1cdd403bdc25 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs @@ -26,9 +26,9 @@ internal static DeclarationModifiers MakeAndCheckNonTypeMemberModifiers( var diagnosticBag = diagnostics.DiagnosticBag ?? new DiagnosticBag(); var result = modifiers.ToDeclarationModifiers( isForTypeDeclaration: false, - diagnosticBag, - isOrdinaryMethod, - allowsPartialModifier: (allowedModifiers & DeclarationModifiers.Partial) != 0); + isOrdinaryMethod: isOrdinaryMethod, + allowsPartialModifier: (allowedModifiers & DeclarationModifiers.Partial) != 0, + diagnostics: diagnosticBag); result = CheckModifiers( isForTypeDeclaration: false, isForInterfaceMember, @@ -463,9 +463,9 @@ private static DeclarationModifiers GetDeclarationModifiersAndCheckForDuplicateM public static DeclarationModifiers ToDeclarationModifiers( this SyntaxTokenList modifiers, bool isForTypeDeclaration, - DiagnosticBag diagnostics, bool isOrdinaryMethod, - bool allowsPartialModifier) + bool allowsPartialModifier, + DiagnosticBag diagnostics) { var result = GetDeclarationModifiersAndCheckForDuplicateModifiers(modifiers, diagnostics); if ((result & DeclarationModifiers.Partial) == DeclarationModifiers.Partial) From 2c34015df53bc5860917ea25c8fe19705e3aa7ec Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Wed, 26 Aug 2026 09:53:27 -0400 Subject: [PATCH 05/12] Remove ordinary-method modifier flag --- .../CSharp/Portable/Binder/Binder_Lambda.cs | 1 - .../Portable/Declarations/DeclarationTreeBuilder.cs | 3 --- .../Portable/Symbols/Source/LocalFunctionSymbol.cs | 1 - .../CSharp/Portable/Symbols/Source/ModifierUtils.cs | 13 +++++-------- .../Symbols/Source/SourceConstructorSymbol.cs | 2 +- .../Symbols/Source/SourceDestructorSymbol.cs | 2 +- .../Portable/Symbols/Source/SourceEventSymbol.cs | 2 +- .../Symbols/Source/SourceMemberFieldSymbol.cs | 2 +- .../Symbols/Source/SourceOrdinaryMethodSymbol.cs | 2 +- .../Symbols/Source/SourcePropertyAccessorSymbol.cs | 2 +- .../Portable/Symbols/Source/SourcePropertySymbol.cs | 2 +- .../Source/SourceUserDefinedOperatorSymbolBase.cs | 2 +- 12 files changed, 13 insertions(+), 21 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs index 2730a69c638e5..01c7e36c96cdc 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs @@ -376,7 +376,6 @@ private UnboundLambda BindAnonymousFunction(AnonymousFunctionExpressionSyntax sy ModifierUtils.ToDeclarationModifiers( syntax.Modifiers, isForTypeDeclaration: false, - isOrdinaryMethod: false, allowsPartialModifier: false, diagnostics.DiagnosticBag ?? new DiagnosticBag()); diff --git a/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs b/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs index 7a42133915f2b..c5671e34e14d3 100644 --- a/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs +++ b/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs @@ -787,7 +787,6 @@ ClassDeclarationSyntax or // Extensions never allow 'partial'. var modifiers = node.Modifiers.ToDeclarationModifiers( isForTypeDeclaration: true, - isOrdinaryMethod: false, allowsPartialModifier: kind != DeclarationKind.Extension, diagnostics); @@ -857,7 +856,6 @@ public override SingleNamespaceOrTypeDeclaration VisitDelegateDeclaration(Delega var modifiers = node.Modifiers.ToDeclarationModifiers( isForTypeDeclaration: true, - isOrdinaryMethod: false, allowsPartialModifier: false, diagnostics); @@ -895,7 +893,6 @@ public override SingleNamespaceOrTypeDeclaration VisitEnumDeclaration(EnumDeclar var diagnostics = DiagnosticBag.GetInstance(); var modifiers = node.Modifiers.ToDeclarationModifiers( isForTypeDeclaration: true, - isOrdinaryMethod: false, allowsPartialModifier: false, diagnostics); diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs index 5a275b623af57..4d861481f9c43 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs @@ -52,7 +52,6 @@ public LocalFunctionSymbol( DeclarationModifiers.Private | syntax.Modifiers.ToDeclarationModifiers( isForTypeDeclaration: false, - isOrdinaryMethod: false, allowsPartialModifier: false, diagnostics: _declarationDiagnostics); diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs b/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs index c1cdd403bdc25..c053e9d5d5783 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs @@ -13,7 +13,6 @@ namespace Microsoft.CodeAnalysis.CSharp.Symbols internal static class ModifierUtils { internal static DeclarationModifiers MakeAndCheckNonTypeMemberModifiers( - bool isOrdinaryMethod, bool isForInterfaceMember, SyntaxTokenList modifiers, DeclarationModifiers defaultAccess, @@ -26,7 +25,6 @@ internal static DeclarationModifiers MakeAndCheckNonTypeMemberModifiers( var diagnosticBag = diagnostics.DiagnosticBag ?? new DiagnosticBag(); var result = modifiers.ToDeclarationModifiers( isForTypeDeclaration: false, - isOrdinaryMethod: isOrdinaryMethod, allowsPartialModifier: (allowedModifiers & DeclarationModifiers.Partial) != 0, diagnostics: diagnosticBag); result = CheckModifiers( @@ -463,7 +461,6 @@ private static DeclarationModifiers GetDeclarationModifiersAndCheckForDuplicateM public static DeclarationModifiers ToDeclarationModifiers( this SyntaxTokenList modifiers, bool isForTypeDeclaration, - bool isOrdinaryMethod, bool allowsPartialModifier, DiagnosticBag diagnostics) { @@ -476,12 +473,12 @@ public static DeclarationModifiers ToDeclarationModifiers( var messageId = isForTypeDeclaration ? MessageID.IDS_FeaturePartialTypes : MessageID.IDS_FeaturePartialMethod; messageId.CheckFeatureAvailability(diagnostics, modifier); - // `partial` must always be the last modifier according to the language. However, there was a bug - // where we allowed `partial async` at the end of modifiers on methods. We keep this behavior for - // backcompat. + // `partial` normally must be the last modifier. For compatibility, do not report an ordering error + // for a trailing `partial async`, which has historically been allowed on ordinary methods. Other + // declarations either disallow `partial` or diagnose `async` separately. var isLast = i == modifiers.Count - 1; - var isPartialAsyncMethod = isOrdinaryMethod && i == modifiers.Count - 2 && modifiers[i + 1].ContextualKind() is SyntaxKind.AsyncKeyword; - if (!allowsPartialModifier || (!isLast && !isPartialAsyncMethod)) + var isPartialAsync = i == modifiers.Count - 2 && modifiers[i + 1].ContextualKind() is SyntaxKind.AsyncKeyword; + if (!allowsPartialModifier || (!isLast && !isPartialAsync)) { diagnostics.Add( ErrorCode.ERR_PartialMisplaced, diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceConstructorSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceConstructorSymbol.cs index b573c41a8b5a1..8d02215f04dd8 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceConstructorSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceConstructorSymbol.cs @@ -158,7 +158,7 @@ private static DeclarationModifiers MakeModifiers( } bool isInterface = containingType.IsInterface; - var mods = ModifierUtils.MakeAndCheckNonTypeMemberModifiers(isOrdinaryMethod: false, isForInterfaceMember: isInterface, syntax.Modifiers, defaultAccess, allowedModifiers, location, diagnostics, out modifierErrors, out hasExplicitAccessModifier); + var mods = ModifierUtils.MakeAndCheckNonTypeMemberModifiers(isForInterfaceMember: isInterface, syntax.Modifiers, defaultAccess, allowedModifiers, location, diagnostics, out modifierErrors, out hasExplicitAccessModifier); report_ERR_StaticConstructorWithAccessModifiers = false; if (methodKind == MethodKind.StaticConstructor) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceDestructorSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceDestructorSymbol.cs index 129fd2433001d..819c64cb5b521 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceDestructorSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceDestructorSymbol.cs @@ -137,7 +137,7 @@ private static DeclarationModifiers MakeModifiers(NamedTypeSymbol containingType { // Check that the set of modifiers is allowed const DeclarationModifiers allowedModifiers = DeclarationModifiers.Extern | DeclarationModifiers.Unsafe | DeclarationModifiers.Safe; - var mods = ModifierUtils.MakeAndCheckNonTypeMemberModifiers(isOrdinaryMethod: false, isForInterfaceMember: containingType.IsInterface, modifiers, DeclarationModifiers.None, allowedModifiers, location, diagnostics, out modifierErrors, out _); + var mods = ModifierUtils.MakeAndCheckNonTypeMemberModifiers(isForInterfaceMember: containingType.IsInterface, modifiers, DeclarationModifiers.None, allowedModifiers, location, diagnostics, out modifierErrors, out _); mods = (mods & ~DeclarationModifiers.AccessibilityMask) | DeclarationModifiers.Protected; // we mark destructors protected in the symbol table diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceEventSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceEventSymbol.cs index 25e53ab7bf07f..11f48975c95d0 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceEventSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceEventSymbol.cs @@ -592,7 +592,7 @@ private DeclarationModifiers MakeModifiers(SyntaxTokenList modifiers, bool expli allowedModifiers |= DeclarationModifiers.Extern; } - var mods = ModifierUtils.MakeAndCheckNonTypeMemberModifiers(isOrdinaryMethod: false, isForInterfaceMember: isInterface, + var mods = ModifierUtils.MakeAndCheckNonTypeMemberModifiers(isForInterfaceMember: isInterface, modifiers, defaultAccess, allowedModifiers, location, diagnostics, out modifierErrors, out hasExplicitAccessModifier); diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberFieldSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberFieldSymbol.cs index 54fdb8b008889..700dc2260f4a3 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberFieldSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberFieldSymbol.cs @@ -226,7 +226,7 @@ internal static DeclarationModifiers MakeModifiers(NamedTypeSymbol containingTyp var errorLocation = new SourceLocation(firstIdentifier); DeclarationModifiers result = ModifierUtils.MakeAndCheckNonTypeMemberModifiers( - isOrdinaryMethod: false, isForInterfaceMember: isInterface, + isForInterfaceMember: isInterface, modifiers, defaultAccess, allowedModifiers, errorLocation, diagnostics, out modifierErrors, out _); if ((result & DeclarationModifiers.Abstract) != 0) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceOrdinaryMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceOrdinaryMethodSymbol.cs index 3d6b6355a8455..908adf52a58f1 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceOrdinaryMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceOrdinaryMethodSymbol.cs @@ -455,7 +455,7 @@ private SyntaxList AttributeDeclarationSyntaxList private static DeclarationModifiers MakeDeclarationModifiers(MethodDeclarationSyntax syntax, NamedTypeSymbol containingType, Location location, DeclarationModifiers allowedModifiers, BindingDiagnosticBag diagnostics) { - return ModifierUtils.MakeAndCheckNonTypeMemberModifiers(isOrdinaryMethod: true, isForInterfaceMember: containingType.IsInterface, + return ModifierUtils.MakeAndCheckNonTypeMemberModifiers(isForInterfaceMember: containingType.IsInterface, syntax.Modifiers, defaultAccess: DeclarationModifiers.None, allowedModifiers, location, diagnostics, out _, out _); } diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertyAccessorSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertyAccessorSymbol.cs index 66ab36ef6ff34..b273eaa41baa7 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertyAccessorSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertyAccessorSymbol.cs @@ -545,7 +545,7 @@ private static DeclarationModifiers MakeModifiers(NamedTypeSymbol containingType defaultInterfaceImplementationModifiers = DeclarationModifiers.AccessibilityMask; } - var mods = ModifierUtils.MakeAndCheckNonTypeMemberModifiers(isOrdinaryMethod: false, isForInterfaceMember: isInterface, + var mods = ModifierUtils.MakeAndCheckNonTypeMemberModifiers(isForInterfaceMember: isInterface, modifiers, defaultAccess, allowedModifiers, location, diagnostics, out modifierErrors, out _); if ((mods & DeclarationModifiers.Unsafe) != 0) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs index b6866e6da233a..d503e20f5ab75 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs @@ -452,7 +452,7 @@ private static (DeclarationModifiers modifiers, bool hasExplicitAccessMod) MakeM allowedModifiers |= DeclarationModifiers.Extern | DeclarationModifiers.Safe; bool hasExplicitAccessMod; - var mods = ModifierUtils.MakeAndCheckNonTypeMemberModifiers(isOrdinaryMethod: false, isForInterfaceMember: isInterface, + var mods = ModifierUtils.MakeAndCheckNonTypeMemberModifiers(isForInterfaceMember: isInterface, modifiers, defaultAccess, allowedModifiers, location, diagnostics, out modifierErrors, out hasExplicitAccessMod); if ((mods & DeclarationModifiers.Partial) != 0) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceUserDefinedOperatorSymbolBase.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceUserDefinedOperatorSymbolBase.cs index a0b6253305110..a0b5e6ceeaa33 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceUserDefinedOperatorSymbolBase.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceUserDefinedOperatorSymbolBase.cs @@ -229,7 +229,7 @@ protected static DeclarationModifiers MakeDeclarationModifiers(bool isCompoundAs } var result = ModifierUtils.MakeAndCheckNonTypeMemberModifiers( - isOrdinaryMethod: false, isForInterfaceMember: inInterface, + isForInterfaceMember: inInterface, syntax.Modifiers, defaultAccess, allowedModifiers, location, diagnostics, modifierErrors: out _, hasExplicitAccessModifier: out _); if (inInterface) From 557272e2fa1bc79742446c3d4937bd7603683b8f Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Wed, 26 Aug 2026 09:56:50 -0400 Subject: [PATCH 06/12] Clarify legal partial modifier positions --- .../Portable/Symbols/Source/ModifierUtils.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs b/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs index c053e9d5d5783..fc6f7ed617e15 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs @@ -470,19 +470,18 @@ public static DeclarationModifiers ToDeclarationModifiers( var i = modifiers.IndexOf(SyntaxKind.PartialKeyword); var modifier = modifiers[i]; - var messageId = isForTypeDeclaration ? MessageID.IDS_FeaturePartialTypes : MessageID.IDS_FeaturePartialMethod; - messageId.CheckFeatureAvailability(diagnostics, modifier); + messageId.CheckFeatureAvailability( + diagnostics, isForTypeDeclaration ? MessageID.IDS_FeaturePartialTypes : MessageID.IDS_FeaturePartialMethod); // `partial` normally must be the last modifier. For compatibility, do not report an ordering error // for a trailing `partial async`, which has historically been allowed on ordinary methods. Other // declarations either disallow `partial` or diagnose `async` separately. - var isLast = i == modifiers.Count - 1; - var isPartialAsync = i == modifiers.Count - 2 && modifiers[i + 1].ContextualKind() is SyntaxKind.AsyncKeyword; - if (!allowsPartialModifier || (!isLast && !isPartialAsync)) + var isLegalLocation = + i == modifiers.Count - 1 || + (i == modifiers.Count - 2 && modifiers[i + 1].ContextualKind() is SyntaxKind.AsyncKeyword); + if (!allowsPartialModifier || !isLegalLocation) { - diagnostics.Add( - ErrorCode.ERR_PartialMisplaced, - modifier.GetLocation()); + diagnostics.Add(ErrorCode.ERR_PartialMisplaced, modifier.GetLocation()); } } From 83f1aebb8247f60a0474b165ecda4b79abd52c47 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Wed, 26 Aug 2026 10:10:05 -0400 Subject: [PATCH 07/12] Derive partial feature context from syntax --- src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs | 1 - .../Portable/Declarations/DeclarationTreeBuilder.cs | 11 ++--------- .../Portable/Symbols/Source/LocalFunctionSymbol.cs | 1 - .../CSharp/Portable/Symbols/Source/ModifierUtils.cs | 6 ++---- 4 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs index 01c7e36c96cdc..a0f7fdde96ba4 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs @@ -375,7 +375,6 @@ private UnboundLambda BindAnonymousFunction(AnonymousFunctionExpressionSyntax sy // are reported now. ModifierUtils.ToDeclarationModifiers( syntax.Modifiers, - isForTypeDeclaration: false, allowsPartialModifier: false, diagnostics.DiagnosticBag ?? new DiagnosticBag()); diff --git a/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs b/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs index c5671e34e14d3..8e3182bcf05e7 100644 --- a/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs +++ b/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs @@ -785,10 +785,7 @@ ClassDeclarationSyntax or } // Extensions never allow 'partial'. - var modifiers = node.Modifiers.ToDeclarationModifiers( - isForTypeDeclaration: true, - allowsPartialModifier: kind != DeclarationKind.Extension, - diagnostics); + var modifiers = node.Modifiers.ToDeclarationModifiers(allowsPartialModifier: kind != DeclarationKind.Extension, diagnostics); var quickAttributes = GetQuickAttributes(node.AttributeLists); @@ -855,7 +852,6 @@ public override SingleNamespaceOrTypeDeclaration VisitDelegateDeclaration(Delega declFlags |= SingleTypeDeclaration.TypeDeclarationFlags.HasAnyNontypeMembers; var modifiers = node.Modifiers.ToDeclarationModifiers( - isForTypeDeclaration: true, allowsPartialModifier: false, diagnostics); @@ -891,10 +887,7 @@ public override SingleNamespaceOrTypeDeclaration VisitEnumDeclaration(EnumDeclar var memberNames = GetEnumMemberNames(node, ref declFlags); var diagnostics = DiagnosticBag.GetInstance(); - var modifiers = node.Modifiers.ToDeclarationModifiers( - isForTypeDeclaration: true, - allowsPartialModifier: false, - diagnostics); + var modifiers = node.Modifiers.ToDeclarationModifiers(allowsPartialModifier: false, diagnostics); var quickAttributes = DeclarationTreeBuilder.GetQuickAttributes(node.AttributeLists); diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs index 4d861481f9c43..ba2a12178d230 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs @@ -51,7 +51,6 @@ public LocalFunctionSymbol( _declarationModifiers = DeclarationModifiers.Private | syntax.Modifiers.ToDeclarationModifiers( - isForTypeDeclaration: false, allowsPartialModifier: false, diagnostics: _declarationDiagnostics); diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs b/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs index fc6f7ed617e15..d660179b82b16 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs @@ -24,7 +24,6 @@ internal static DeclarationModifiers MakeAndCheckNonTypeMemberModifiers( { var diagnosticBag = diagnostics.DiagnosticBag ?? new DiagnosticBag(); var result = modifiers.ToDeclarationModifiers( - isForTypeDeclaration: false, allowsPartialModifier: (allowedModifiers & DeclarationModifiers.Partial) != 0, diagnostics: diagnosticBag); result = CheckModifiers( @@ -460,7 +459,6 @@ private static DeclarationModifiers GetDeclarationModifiersAndCheckForDuplicateM public static DeclarationModifiers ToDeclarationModifiers( this SyntaxTokenList modifiers, - bool isForTypeDeclaration, bool allowsPartialModifier, DiagnosticBag diagnostics) { @@ -470,8 +468,8 @@ public static DeclarationModifiers ToDeclarationModifiers( var i = modifiers.IndexOf(SyntaxKind.PartialKeyword); var modifier = modifiers[i]; - messageId.CheckFeatureAvailability( - diagnostics, isForTypeDeclaration ? MessageID.IDS_FeaturePartialTypes : MessageID.IDS_FeaturePartialMethod); + var messageId = SyntaxFacts.IsTypeDeclaration(modifier.Parent.Kind()) ? MessageID.IDS_FeaturePartialTypes : MessageID.IDS_FeaturePartialMethod; + messageId.CheckFeatureAvailability(diagnostics, modifier); // `partial` normally must be the last modifier. For compatibility, do not report an ordering error // for a trailing `partial async`, which has historically been allowed on ordinary methods. Other From 7624122a1dd2e18a3baed152762b61193ef0f8d0 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Wed, 26 Aug 2026 10:12:26 -0400 Subject: [PATCH 08/12] Condense modifier helper calls --- .../Portable/Declarations/DeclarationTreeBuilder.cs | 4 +--- .../Portable/Symbols/Source/LocalFunctionSymbol.cs | 4 +--- .../CSharp/Portable/Symbols/Source/ModifierUtils.cs | 10 +--------- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs b/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs index 8e3182bcf05e7..94bc806517cd3 100644 --- a/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs +++ b/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs @@ -851,9 +851,7 @@ public override SingleNamespaceOrTypeDeclaration VisitDelegateDeclaration(Delega declFlags |= SingleTypeDeclaration.TypeDeclarationFlags.HasAnyNontypeMembers; - var modifiers = node.Modifiers.ToDeclarationModifiers( - allowsPartialModifier: false, - diagnostics); + var modifiers = node.Modifiers.ToDeclarationModifiers(allowsPartialModifier: false, diagnostics); var quickAttributes = DeclarationTreeBuilder.GetQuickAttributes(node.AttributeLists); diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs index ba2a12178d230..0a078edfb46a3 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs @@ -50,9 +50,7 @@ public LocalFunctionSymbol( _declarationModifiers = DeclarationModifiers.Private | - syntax.Modifiers.ToDeclarationModifiers( - allowsPartialModifier: false, - diagnostics: _declarationDiagnostics); + syntax.Modifiers.ToDeclarationModifiers(allowsPartialModifier: false, diagnostics: _declarationDiagnostics); var diagnostics = BindingDiagnosticBag.GetInstance(); Debug.Assert(diagnostics.DiagnosticBag is { }); diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs b/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs index d660179b82b16..efc2688e6ae08 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs @@ -26,15 +26,7 @@ internal static DeclarationModifiers MakeAndCheckNonTypeMemberModifiers( var result = modifiers.ToDeclarationModifiers( allowsPartialModifier: (allowedModifiers & DeclarationModifiers.Partial) != 0, diagnostics: diagnosticBag); - result = CheckModifiers( - isForTypeDeclaration: false, - isForInterfaceMember, - result, - allowedModifiers, - errorLocation, - diagnostics, - modifiers, - out modifierErrors); + result = CheckModifiers(isForTypeDeclaration: false, isForInterfaceMember, result, allowedModifiers, errorLocation, diagnostics, modifiers, out modifierErrors); var readonlyToken = modifiers.FirstOrDefault(SyntaxKind.ReadOnlyKeyword); if (readonlyToken.Parent is MethodDeclarationSyntax or AccessorDeclarationSyntax or BasePropertyDeclarationSyntax or EventDeclarationSyntax) From e0553b9f96818708f38370fb9fad91d56033a029 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Wed, 26 Aug 2026 10:13:00 -0400 Subject: [PATCH 09/12] Condense modifier helper signature --- src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs b/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs index efc2688e6ae08..0472e591c16ce 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs @@ -450,9 +450,7 @@ private static DeclarationModifiers GetDeclarationModifiersAndCheckForDuplicateM } public static DeclarationModifiers ToDeclarationModifiers( - this SyntaxTokenList modifiers, - bool allowsPartialModifier, - DiagnosticBag diagnostics) + this SyntaxTokenList modifiers, bool allowsPartialModifier, DiagnosticBag diagnostics) { var result = GetDeclarationModifiersAndCheckForDuplicateModifiers(modifiers, diagnostics); if ((result & DeclarationModifiers.Partial) == DeclarationModifiers.Partial) From 8cc2aade2b0f4b3c38e5472ae8962a98aef35c3e Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Wed, 26 Aug 2026 10:41:13 -0400 Subject: [PATCH 10/12] Update remaining partial diagnostic baselines --- .../CSharp/Portable/Binder/Binder_Lambda.cs | 5 +---- .../Test/Symbol/Symbols/Source/DelegateTests.cs | 8 ++++---- .../CSharp/Test/Symbol/Symbols/TypeTests.cs | 12 ++++++------ .../Syntax/Parsing/ClosedModifierParsingTests.cs | 4 ++-- .../Test/Syntax/Parsing/DeclarationParsingTests.cs | 4 ++-- .../Test/Syntax/Parsing/ExtensionsParsingTests.cs | 4 ++-- .../Test/Syntax/Parsing/FileModifierParsingTests.cs | 4 ++-- .../Test/Syntax/Parsing/ParserErrorMessageTests.cs | 12 ++++++------ .../CSharp/Test/Syntax/Parsing/ScriptParsingTests.cs | 4 ++-- .../Syntax/Parsing/TopLevelStatementsParsingTests.cs | 8 ++++---- 10 files changed, 31 insertions(+), 34 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs index a0f7fdde96ba4..5b60a9c000e3f 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs @@ -373,10 +373,7 @@ private UnboundLambda BindAnonymousFunction(AnonymousFunctionExpressionSyntax sy // Parser will only have accepted static/async as allowed modifiers on this construct. // However, it may have accepted duplicates of those modifiers. Ensure that any dupes // are reported now. - ModifierUtils.ToDeclarationModifiers( - syntax.Modifiers, - allowsPartialModifier: false, - diagnostics.DiagnosticBag ?? new DiagnosticBag()); + ModifierUtils.ToDeclarationModifiers(syntax.Modifiers, allowsPartialModifier: false, diagnostics.DiagnosticBag ?? new DiagnosticBag()); if (data.HasSignature) { diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/Source/DelegateTests.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/Source/DelegateTests.cs index f705bd5c8fab3..ec30ca0569750 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/Source/DelegateTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/Source/DelegateTests.cs @@ -850,18 +850,18 @@ public void PartialPublicDelegate() public void PublicPartialDelegate() { CreateCompilation("public partial delegate void M();").VerifyDiagnostics( - // (1,30): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method return type. + // (1,8): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // public partial delegate void M(); - Diagnostic(ErrorCode.ERR_PartialMisplaced, "M").WithLocation(1, 30)); + Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(1, 8)); } [Fact] public void PartialDelegate() { CreateCompilation("public partial delegate void M();").VerifyDiagnostics( - // (1,30): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method return type. + // (1,8): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // public partial delegate void M(); - Diagnostic(ErrorCode.ERR_PartialMisplaced, "M").WithLocation(1, 30)); + Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(1, 8)); } } } diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/TypeTests.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/TypeTests.cs index bb512ab23e71d..6defd101a489e 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/TypeTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/TypeTests.cs @@ -2693,12 +2693,12 @@ partial record struct; // (7,22): error CS0261: Partial declarations of '.' must be all classes, all record classes, all structs, all record structs, or all interfaces // partial interface; Diagnostic(ErrorCode.ERR_PartialTypeKindConflict, "").WithArguments("N..").WithLocation(7, 22), + // (8,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. + // partial enum { } + Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(8, 5), // (8,18): error CS1001: Identifier expected // partial enum { } Diagnostic(ErrorCode.ERR_IdentifierExpected, "{").WithLocation(8, 18), - // (8,18): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method return type. - // partial enum { } - Diagnostic(ErrorCode.ERR_PartialMisplaced, "").WithLocation(8, 18), // (8,18): error CS0542: '': member names cannot be the same as their enclosing type // partial enum { } Diagnostic(ErrorCode.ERR_MemberNameSameAsType, "").WithArguments("").WithLocation(8, 18), @@ -2741,12 +2741,12 @@ partial record struct; // (20,22): error CS1001: Identifier expected // partial interface; Diagnostic(ErrorCode.ERR_IdentifierExpected, ";").WithLocation(20, 22), + // (21,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. + // partial enum { } + Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(21, 5), // (21,18): error CS1001: Identifier expected // partial enum { } Diagnostic(ErrorCode.ERR_IdentifierExpected, "{").WithLocation(21, 18), - // (21,18): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method return type. - // partial enum { } - Diagnostic(ErrorCode.ERR_PartialMisplaced, "").WithLocation(21, 18), // (21,18): error CS0542: '': member names cannot be the same as their enclosing type // partial enum { } Diagnostic(ErrorCode.ERR_MemberNameSameAsType, "").WithArguments("").WithLocation(21, 18), diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/ClosedModifierParsingTests.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/ClosedModifierParsingTests.cs index 873d3a127a08c..e13f98a320fad 100644 --- a/src/Compilers/CSharp/Test/Syntax/Parsing/ClosedModifierParsingTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/ClosedModifierParsingTests.cs @@ -158,9 +158,9 @@ public void ClosedModifier_02_Enum() closed partial enum C { } """, expectedBindingDiagnostics: [ - // (1,21): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. + // (1,8): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // closed partial enum C { } - Diagnostic(ErrorCode.ERR_PartialMisplaced, "C").WithLocation(1, 21), + Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(1, 8), // (1,21): error CS0106: The modifier 'closed' is not valid for this item // closed partial enum C { } Diagnostic(ErrorCode.ERR_BadMemberFlag, "C").WithArguments("closed").WithLocation(1, 21) diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/DeclarationParsingTests.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/DeclarationParsingTests.cs index 60f2cd992fabb..b9d70cb435572 100644 --- a/src/Compilers/CSharp/Test/Syntax/Parsing/DeclarationParsingTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/DeclarationParsingTests.cs @@ -5811,9 +5811,9 @@ public void TestPartialEnum() { var text = @"partial enum E{}"; CreateCompilationWithMscorlib461(text).VerifyDiagnostics( - // (1,14): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method return type. + // (1,1): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // partial enum E{} - Diagnostic(ErrorCode.ERR_PartialMisplaced, "E").WithLocation(1, 14)); + Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(1, 1)); } [WorkItem(539120, "http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/539120")] diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/ExtensionsParsingTests.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/ExtensionsParsingTests.cs index 0b46272bbbc9c..f0ba66bec1317 100644 --- a/src/Compilers/CSharp/Test/Syntax/Parsing/ExtensionsParsingTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/ExtensionsParsingTests.cs @@ -2092,9 +2092,9 @@ partial extension(Type) { } var comp = CreateCompilation(src); comp.VerifyEmitDiagnostics( - // (3,13): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. + // (3,5): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // partial extension(Type) { } - Diagnostic(ErrorCode.ERR_PartialMisplaced, "extension").WithLocation(3, 13), + Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(3, 5), // (3,23): error CS0246: The type or namespace name 'Type' could not be found (are you missing a using directive or an assembly reference?) // partial extension(Type) { } Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "Type").WithArguments("Type").WithLocation(3, 23) diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/FileModifierParsingTests.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/FileModifierParsingTests.cs index 65b437214e81c..337e72d79bed5 100644 --- a/src/Compilers/CSharp/Test/Syntax/Parsing/FileModifierParsingTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/FileModifierParsingTests.cs @@ -99,9 +99,9 @@ file partial enum C { } """, expectedBindingDiagnostics: new[] { - // (1,19): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method return type. + // (1,6): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // file partial enum C { } - Diagnostic(ErrorCode.ERR_PartialMisplaced, "C").WithLocation(1, 19) + Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(1, 6) }); N(SyntaxKind.CompilationUnit); { diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/ParserErrorMessageTests.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/ParserErrorMessageTests.cs index 713f6ee6e3f19..cd7cfdaa2e0c3 100644 --- a/src/Compilers/CSharp/Test/Syntax/Parsing/ParserErrorMessageTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/ParserErrorMessageTests.cs @@ -622,9 +622,9 @@ partial enum E { } "; CreateCompilation(test).VerifyDiagnostics( - // (2,14): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method return type. + // (2,1): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // partial enum E { } - Diagnostic(ErrorCode.ERR_PartialMisplaced, "E").WithLocation(2, 14)); + Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(2, 1)); } [Fact] @@ -651,9 +651,9 @@ partial delegate E { } // (2,20): error CS8803: Top-level statements must precede namespace and type declarations. // partial delegate E { } Diagnostic(ErrorCode.ERR_TopLevelStatementAfterNamespaceOrType, "{ }").WithLocation(2, 20), - // (2,20): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method return type. + // (2,1): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // partial delegate E { } - Diagnostic(ErrorCode.ERR_PartialMisplaced, "").WithLocation(2, 20), + Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(2, 1), // (2,18): error CS0246: The type or namespace name 'E' could not be found (are you missing a using directive or an assembly reference?) // partial delegate E { } Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "E").WithArguments("E").WithLocation(2, 18)); @@ -667,9 +667,9 @@ public void CS0267ERR_PartialMisplaced_Delegate2() "; CreateCompilation(test).VerifyDiagnostics( - // (2,23): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method return type. + // (2,1): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // partial delegate void E(); - Diagnostic(ErrorCode.ERR_PartialMisplaced, "E").WithLocation(2, 23)); + Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(2, 1)); } // TODO: Extra errors diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/ScriptParsingTests.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/ScriptParsingTests.cs index 58b5759b42ad8..a0b3928c48da0 100644 --- a/src/Compilers/CSharp/Test/Syntax/Parsing/ScriptParsingTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/ScriptParsingTests.cs @@ -2185,9 +2185,9 @@ public void EnumDeclaration() partial enum @en {}; "; CreateCompilation(test).VerifyDiagnostics( - // (2,14): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method return type. + // (2,1): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. // partial enum @en {}; - Diagnostic(ErrorCode.ERR_PartialMisplaced, "@en").WithLocation(2, 14)); + Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(2, 1)); } [Fact] diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/TopLevelStatementsParsingTests.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/TopLevelStatementsParsingTests.cs index aedda8d3b2ba6..c342372128bd0 100644 --- a/src/Compilers/CSharp/Test/Syntax/Parsing/TopLevelStatementsParsingTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/TopLevelStatementsParsingTests.cs @@ -969,6 +969,9 @@ public void CS0267ERR_PartialMisplaced_Delegate1() partial delegate E { } "; CreateCompilation(test).VerifyDiagnostics( + // (2,1): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', 'event', an instance constructor name, or a method or property return type. + // partial delegate E { } + Diagnostic(ErrorCode.ERR_PartialMisplaced, "partial").WithLocation(2, 1), // (2,18): error CS0246: The type or namespace name 'E' could not be found (are you missing a using directive or an assembly reference?) // partial delegate E { } Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "E").WithArguments("E").WithLocation(2, 18), @@ -986,10 +989,7 @@ partial delegate E { } Diagnostic(ErrorCode.ERR_SemicolonExpected, "{").WithLocation(2, 20), // (2,20): error CS8803: Top-level statements must precede namespace and type declarations. // partial delegate E { } - Diagnostic(ErrorCode.ERR_TopLevelStatementAfterNamespaceOrType, "{ }").WithLocation(2, 20), - // (2,20): error CS0267: The 'partial' modifier can only appear immediately before 'class', 'record', 'struct', 'interface', or a method return type. - // partial delegate E { } - Diagnostic(ErrorCode.ERR_PartialMisplaced, "").WithLocation(2, 20) + Diagnostic(ErrorCode.ERR_TopLevelStatementAfterNamespaceOrType, "{ }").WithLocation(2, 20) ); UsingTree(test, // (2,20): error CS1001: Identifier expected From 593fd8fa990e7b33d82493e07df2fdff01a7b4f3 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Wed, 26 Aug 2026 15:14:51 -0400 Subject: [PATCH 11/12] Explain partial async compatibility --- .../CSharp/Portable/Symbols/Source/ModifierUtils.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs b/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs index 0472e591c16ce..62b10bd9e19a7 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs @@ -461,9 +461,9 @@ public static DeclarationModifiers ToDeclarationModifiers( var messageId = SyntaxFacts.IsTypeDeclaration(modifier.Parent.Kind()) ? MessageID.IDS_FeaturePartialTypes : MessageID.IDS_FeaturePartialMethod; messageId.CheckFeatureAvailability(diagnostics, modifier); - // `partial` normally must be the last modifier. For compatibility, do not report an ordering error - // for a trailing `partial async`, which has historically been allowed on ordinary methods. Other - // declarations either disallow `partial` or diagnose `async` separately. + // `partial` normally must be last. Preserve the historical exception for ordinary methods ending in + // `partial async`. Ordinary methods are the only declarations that allow both modifiers; elsewhere, + // either `partial` is rejected here or `async` is rejected by normal modifier validation. var isLegalLocation = i == modifiers.Count - 1 || (i == modifiers.Count - 2 && modifiers[i + 1].ContextualKind() is SyntaxKind.AsyncKeyword); From fb7eba369e04043a5e71d5ebbdbc645a3477f003 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Wed, 26 Aug 2026 15:20:36 -0400 Subject: [PATCH 12/12] Name async modifier validation method --- src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs b/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs index 62b10bd9e19a7..15c68869db939 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs @@ -463,7 +463,7 @@ public static DeclarationModifiers ToDeclarationModifiers( // `partial` normally must be last. Preserve the historical exception for ordinary methods ending in // `partial async`. Ordinary methods are the only declarations that allow both modifiers; elsewhere, - // either `partial` is rejected here or `async` is rejected by normal modifier validation. + // either `partial` is rejected here or `async` is rejected by ModifierUtils.CheckModifiers. var isLegalLocation = i == modifiers.Count - 1 || (i == modifiers.Count - 2 && modifiers[i + 1].ContextualKind() is SyntaxKind.AsyncKeyword);