From f4620685976564a5b162537d19050af5527e7632 Mon Sep 17 00:00:00 2001 From: Teneko Date: Fri, 9 Oct 2026 08:44:09 +0200 Subject: [PATCH 1/3] feat!: require [OverloadMatcher] on matcher types A type became a matcher implicitly as soon as any target listed it in Matchers. The generator then skipped the type's own [GenerateOverloads] methods as targets, so referencing a type elsewhere removed its own overloads. The analyzer, which analyzes each type on its own, treated an unreferenced matcher as a direct target and reported false generation diagnostics (two MOG006 in the acceptance project). The matcher role is now explicit and local to the type: - New [OverloadMatcher] marker (class, struct, interface). A marked type is a matcher and never a target, whether or not anything references it (TypeModel.IsMatcher replaces the global set of referenced matcher types in the plan builder). - An unmarked type in Matchers is not used as a matcher and keeps its own role. The analyzer reports MOG020 (Error) at every typeof reference to it. - [GenerateMethodOverloads] on a matcher type, or Matchers on one of its methods, is ignored and reported as MOG021 (Error). Other [GenerateOverloads] attributes of that matcher method still apply. Formal checks of matcher attributes now run once, in the matcher type itself (new ValidateMatcherMethods step), independent of any target. Targets skip invalid matcher attributes silently instead of reporting diagnostics that the analyzer discarded anyway. MOG020 and MOG021 are declaration checks done directly in the analyzer, so generator inputs carry no new diagnostics or locations. All matcher types in the acceptance project are marked. Analyzer diagnostics on the acceptance sources: the two false MOG006 at Class_4_Matcher and Bug_Class_1_ICommandRuntimeFactoryMatchers are gone, the other 37 are unchanged, generated overloads are unchanged. BREAKING CHANGE: types listed in Matchers must be marked with [OverloadMatcher]; unmarked types produce MOG020 and are ignored. Co-Authored-By: Claude Opus 5.5 --- .../Bug/Bug_Class_1.cs | 1 + .../Class_16.cs | 1 + .../Class_18.cs | 1 + .../Class_19.cs | 1 + .../Class_20.cs | 1 + .../Class_21.cs | 1 + .../Class_23.cs | 1 + .../Class_24.cs | 1 + .../Class_25.cs | 2 + .../Class_26.cs | 1 + .../Class_27.cs | 1 + .../Class_28.cs | 1 + .../Class_29.cs | 1 + .../Class_3.cs | 1 + .../Class_34.cs | 1 + .../Class_36.cs | 2 + .../Class_37.cs | 1 + .../Class_38.cs | 2 + .../Class_39.cs | 1 + .../Class_4.cs | 1 + .../Class_40.cs | 1 + .../Class_5.cs | 1 + .../Class_6.cs | 1 + .../Class_7.cs | 1 + .../ExcludeAny/ExcludeAny_Class_6.cs | 1 + .../ExcludeAny/ExcludeAny_Class_7.cs | 1 + .../MatcherUsage/Class_MatcherUsage_1.cs | 1 + .../SupplyParameterType_Class_10.cs | 1 + .../SupplyParameterType_Class_11.cs | 1 + .../SupplyParameterType_Class_12.cs | 2 + .../SupplyParameterType_Class_13.cs | 1 + .../OverloadMatcherAttribute.cs | 15 ++ .../Generation/OverloadPlanBuilder.cs | 151 +++++++++++------- .../GeneratorAttributesSource.cs | 3 + .../GeneratorDiagnostics.cs | 16 ++ .../MethodOverloadsDiagnosticsAnalyzer.cs | 80 +++++++++- .../MethodOverloadsGenerator.cs | 4 + .../Models/TypeModel.cs | 3 +- .../Parsing/Parser.ModelMapper.cs | 16 +- .../Parsing/TargetFactory.cs | 16 +- .../DiagnosticSuppressionTests.cs | 1 + ...sts.Diagnostics_HaveNoChanges.verified.txt | 16 ++ 42 files changed, 297 insertions(+), 59 deletions(-) create mode 100644 src/Tenekon.MethodOverloads.Attributes/OverloadMatcherAttribute.cs diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Bug/Bug_Class_1.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Bug/Bug_Class_1.cs index 7b85ce7..71b06ab 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Bug/Bug_Class_1.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Bug/Bug_Class_1.cs @@ -16,6 +16,7 @@ public sealed class Bug_Class_1_Type_3; public interface Bug_Class_1_Type_4; +[OverloadMatcher] internal interface Bug_Class_1_ICommandRuntimeFactoryMatchers { [GenerateOverloads(Begin = nameof(param_3))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_16.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_16.cs index b5fd839..94093d0 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_16.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_16.cs @@ -2,6 +2,7 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias; +[OverloadMatcher] internal interface Class_16_Matcher { [GenerateOverloads(nameof(param_b))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_18.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_18.cs index ad3219d..b5eac3b 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_18.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_18.cs @@ -3,6 +3,7 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias; [OverloadGenerationOptions(RangeAnchorMatchMode = RangeAnchorMatchMode.TypeAndName)] +[OverloadMatcher] internal interface Class_18_Matcher { [GenerateOverloads(nameof(param_2))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_19.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_19.cs index f7726d6..b06bf61 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_19.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_19.cs @@ -1,5 +1,6 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias; +[OverloadMatcher] internal interface Class_19_Matcher { [GenerateOverloads(nameof(param_b))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_20.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_20.cs index fa529a3..682a31d 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_20.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_20.cs @@ -2,6 +2,7 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias; +[OverloadMatcher] internal interface Class_20_Matcher { [GenerateOverloads(nameof(param_b))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_21.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_21.cs index 1f89949..8ac229f 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_21.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_21.cs @@ -39,6 +39,7 @@ public static void Duplicate(int value) { } public static void Parameterless() { } } +[OverloadMatcher] internal interface Class_21_Matcher { [GenerateOverloads(nameof(m))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_23.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_23.cs index ed0379e..43943bd 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_23.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_23.cs @@ -1,5 +1,6 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias; +[OverloadMatcher] internal interface Class_23_Matcher { [GenerateOverloads(nameof(param_b))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_24.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_24.cs index ad01a88..da92585 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_24.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_24.cs @@ -1,5 +1,6 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias; +[OverloadMatcher] internal interface Class_24_Matcher { [GenerateOverloads(nameof(param_b))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_25.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_25.cs index 303f595..01dc127 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_25.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_25.cs @@ -2,6 +2,7 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias; +[OverloadMatcher] internal interface Class_25_MatcherUnused { [GenerateOverloads(nameof(param_a))] @@ -28,6 +29,7 @@ public sealed class Class_25_MixedTarget public void Case_1(int param_1, string? param_2, bool param_3) { } } +[OverloadMatcher] internal interface Class_25_MatcherMixed { [GenerateOverloads(nameof(param_b))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_26.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_26.cs index 099fcd3..6bf6ff1 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_26.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_26.cs @@ -1,5 +1,6 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias; +[OverloadMatcher] internal interface Class_26_Matcher { [GenerateOverloads(nameof(param_b))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_27.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_27.cs index 66e0b4b..dbbb2ec 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_27.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_27.cs @@ -1,5 +1,6 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias; +[OverloadMatcher] internal interface Class_27_Matcher { [GenerateOverloads(nameof(param_b))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_28.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_28.cs index f6bfab5..dc1c097 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_28.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_28.cs @@ -1,5 +1,6 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias; +[OverloadMatcher] internal interface Class_28_Matcher { [GenerateOverloads(Begin = nameof(param_a))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_29.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_29.cs index e2520c5..3aea3b5 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_29.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_29.cs @@ -1,6 +1,7 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias; [OverloadGenerationOptions(RangeAnchorMatchMode = RangeAnchorMatchMode.TypeAndName)] +[OverloadMatcher] internal interface Class_29_Matcher { [GenerateOverloads(nameof(param_2))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_3.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_3.cs index b925f87..9e1fa64 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_3.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_3.cs @@ -3,6 +3,7 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias; /// /// /// +[OverloadMatcher] internal interface Class_3_Matcher { [GenerateOverloads(EndExclusive = nameof(param_b))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_34.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_34.cs index e7b0d6b..5e84855 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_34.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_34.cs @@ -2,6 +2,7 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias; +[OverloadMatcher] internal interface Class_34_Matcher { [GenerateOverloads(nameof(param_b))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_36.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_36.cs index b6301da..4c1a2c8 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_36.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_36.cs @@ -3,6 +3,7 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias; /// /// Matcher type A for matcher-union testing. /// +[OverloadMatcher] internal interface Class_36_MatcherA { /// @@ -15,6 +16,7 @@ internal interface Class_36_MatcherA /// /// Matcher type B for matcher-union testing. /// +[OverloadMatcher] internal interface Class_36_MatcherB { /// diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_37.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_37.cs index bd08b51..7d9417e 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_37.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_37.cs @@ -3,6 +3,7 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias; /// /// Matcher type for mixed direct + matcher generation. /// +[OverloadMatcher] internal interface Class_37_Matcher { /// diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_38.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_38.cs index 1cb3475..1861494 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_38.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_38.cs @@ -1,11 +1,13 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias; +[OverloadMatcher] internal interface Class_38_MatcherA { [GenerateOverloads(nameof(param_b))] void Matcher_1(int param_a, string param_b); } +[OverloadMatcher] internal interface Class_38_MatcherB { [GenerateOverloads(nameof(param_c))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_39.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_39.cs index da70956..ae173e7 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_39.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_39.cs @@ -22,6 +22,7 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias; /// SubsequenceStrategy precedence between target and matcher frames. /// [OverloadGenerationOptions(SubsequenceStrategy = OverloadSubsequenceStrategy.PrefixOnly)] +[OverloadMatcher] internal interface Class_39_Matcher { [OverloadGenerationOptions(SubsequenceStrategy = OverloadSubsequenceStrategy.UniqueBySignature)] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_4.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_4.cs index 855eb4f..016e753 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_4.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_4.cs @@ -1,5 +1,6 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias; +[OverloadMatcher] internal interface Class_4_Matcher { [GenerateOverloads(EndExclusive = nameof(param_b))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_40.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_40.cs index 6b8c120..9be9fd7 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_40.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_40.cs @@ -14,6 +14,7 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias; /// OverloadVisibility precedence between target and matcher frames. /// [OverloadGenerationOptions(OverloadVisibility = OverloadVisibility.Internal)] +[OverloadMatcher] internal interface Class_40_Matcher { [GenerateOverloads(nameof(param_a))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_5.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_5.cs index d5fa88b..f6db579 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_5.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_5.cs @@ -7,6 +7,7 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias; /// Expects overloads only when the selected match mode fits. /// [OverloadGenerationOptions(RangeAnchorMatchMode = RangeAnchorMatchMode.TypeOnly)] +[OverloadMatcher] internal interface Class_5_Matcher { [GenerateOverloads(EndExclusive = nameof(param_2))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_6.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_6.cs index 294b016..e8e57a4 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_6.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_6.cs @@ -5,6 +5,7 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias; /// Expects overloads only when effective match mode allows. /// [OverloadGenerationOptions(RangeAnchorMatchMode = RangeAnchorMatchMode.TypeAndName)] +[OverloadMatcher] internal interface Class_6_Matcher { [OverloadGenerationOptions(RangeAnchorMatchMode = RangeAnchorMatchMode.TypeOnly)] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_7.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_7.cs index 27fe581..3ee7ac3 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_7.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_7.cs @@ -5,6 +5,7 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias; /// Expects overloads only when effective match mode allows. /// [OverloadGenerationOptions(RangeAnchorMatchMode = RangeAnchorMatchMode.TypeOnly)] +[OverloadMatcher] internal interface Class_7_Matcher { [OverloadGenerationOptions(RangeAnchorMatchMode = RangeAnchorMatchMode.TypeAndName)] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/ExcludeAny/ExcludeAny_Class_6.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/ExcludeAny/ExcludeAny_Class_6.cs index e0456f9..bf2ad00 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/ExcludeAny/ExcludeAny_Class_6.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/ExcludeAny/ExcludeAny_Class_6.cs @@ -5,6 +5,7 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias.ExcludeAny; public sealed class ExcludeAny_Class_6_Param_1; public sealed class ExcludeAny_Class_6_Param_2; +[OverloadMatcher] public interface ExcludeAny_Class_6_Matcher { [GenerateOverloads(nameof(param_2))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/ExcludeAny/ExcludeAny_Class_7.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/ExcludeAny/ExcludeAny_Class_7.cs index e0c7be9..f73eb2a 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/ExcludeAny/ExcludeAny_Class_7.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/ExcludeAny/ExcludeAny_Class_7.cs @@ -4,6 +4,7 @@ public sealed class ExcludeAny_Class_7_Param_1; public sealed class ExcludeAny_Class_7_Param_2; public sealed class ExcludeAny_Class_7_Param_3; +[OverloadMatcher] public interface ExcludeAny_Class_7_Matcher { [GenerateOverloads(ExcludeAny = [nameof(param_2)])] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/MatcherUsage/Class_MatcherUsage_1.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/MatcherUsage/Class_MatcherUsage_1.cs index a4197e4..92be4bc 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/MatcherUsage/Class_MatcherUsage_1.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/MatcherUsage/Class_MatcherUsage_1.cs @@ -5,6 +5,7 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias.MatcherUsage; /// /// Matcher matches target, but overload generation is skipped due to defaults inside window. /// +[OverloadMatcher] internal interface Class_MatcherUsage_1_Matcher { [GenerateOverloads(nameof(optional))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/SupplyParameterType/SupplyParameterType_Class_10.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/SupplyParameterType/SupplyParameterType_Class_10.cs index ebce8e4..3820dde 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/SupplyParameterType/SupplyParameterType_Class_10.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/SupplyParameterType/SupplyParameterType_Class_10.cs @@ -1,5 +1,6 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias.SupplyParameterType; +[OverloadMatcher] public interface SupplyParameterType_Class_10_Matcher { [GenerateOverloads(nameof(optionalObject))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/SupplyParameterType/SupplyParameterType_Class_11.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/SupplyParameterType/SupplyParameterType_Class_11.cs index 694428d..f9b86fa 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/SupplyParameterType/SupplyParameterType_Class_11.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/SupplyParameterType/SupplyParameterType_Class_11.cs @@ -9,6 +9,7 @@ public sealed class SupplyParameterType_Class_11_Case_1_Param_1; public sealed class SupplyParameterType_Class_11_Case_2_Param_1; [SupplyParameterType(nameof(TConstraint), typeof(SupplyParameterType_Class_11_Constraint_4))] +[OverloadMatcher] public interface SupplyParameterType_Class_11_Matcher { [GenerateOverloads(nameof(param_2))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/SupplyParameterType/SupplyParameterType_Class_12.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/SupplyParameterType/SupplyParameterType_Class_12.cs index 62b0346..dfd2bf4 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/SupplyParameterType/SupplyParameterType_Class_12.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/SupplyParameterType/SupplyParameterType_Class_12.cs @@ -5,6 +5,7 @@ public sealed class SupplyParameterType_Class_12_Constraint_2; public sealed class SupplyParameterType_Class_12_Param; +[OverloadMatcher] public interface SupplyParameterType_Class_12_Matcher_1 { [GenerateOverloads(nameof(optionalObject))] @@ -14,6 +15,7 @@ void Case_1( object? optionalObject); } +[OverloadMatcher] public interface SupplyParameterType_Class_12_Matcher_2 { [GenerateOverloads(nameof(optionalObject))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/SupplyParameterType/SupplyParameterType_Class_13.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/SupplyParameterType/SupplyParameterType_Class_13.cs index d8ab6d8..d8bf0da 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/SupplyParameterType/SupplyParameterType_Class_13.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/SupplyParameterType/SupplyParameterType_Class_13.cs @@ -5,6 +5,7 @@ public sealed class SupplyParameterType_Class_13_Constraint_2; public sealed class SupplyParameterType_Class_13_Param; +[OverloadMatcher] public interface SupplyParameterType_Class_13_Matcher { [GenerateOverloads(Begin = nameof(optionalObject))] diff --git a/src/Tenekon.MethodOverloads.Attributes/OverloadMatcherAttribute.cs b/src/Tenekon.MethodOverloads.Attributes/OverloadMatcherAttribute.cs new file mode 100644 index 0000000..c0a91ad --- /dev/null +++ b/src/Tenekon.MethodOverloads.Attributes/OverloadMatcherAttribute.cs @@ -0,0 +1,15 @@ +#nullable enable +namespace Tenekon.MethodOverloads; + +/// +/// Marks a type as a matcher. Its methods describe windows for the targets that +/// list the type in Matchers. A matcher type is never a generation target itself. +/// +[global::Microsoft.CodeAnalysis.Embedded] +[global::System.AttributeUsage( + global::System.AttributeTargets.Class + | global::System.AttributeTargets.Struct + | global::System.AttributeTargets.Interface)] +internal sealed class OverloadMatcherAttribute : global::System.Attribute +{ +} diff --git a/src/Tenekon.MethodOverloads.SourceGenerator/Generation/OverloadPlanBuilder.cs b/src/Tenekon.MethodOverloads.SourceGenerator/Generation/OverloadPlanBuilder.cs index 612c324..5785cce 100644 --- a/src/Tenekon.MethodOverloads.SourceGenerator/Generation/OverloadPlanBuilder.cs +++ b/src/Tenekon.MethodOverloads.SourceGenerator/Generation/OverloadPlanBuilder.cs @@ -13,7 +13,6 @@ internal sealed class OverloadPlanBuilder private readonly Dictionary _typesByDisplay; private readonly Dictionary _typeTargetsByDisplay; private readonly Dictionary _matcherTypesByDisplay; - private readonly HashSet _matcherTypeDisplays; private readonly Dictionary> _methodsByGroup; private readonly Dictionary _matchedMatchersByGroup; private readonly List _diagnostics; @@ -33,7 +32,6 @@ public OverloadPlanBuilder(GeneratorModel model) target => target.Type.DisplayName, target => target, StringComparer.Ordinal); - _matcherTypeDisplays = new HashSet(_matcherTypesByDisplay.Keys, StringComparer.Ordinal); _methodsByGroup = new Dictionary>(); _matchedMatchersByGroup = new Dictionary(); _diagnostics = []; @@ -42,6 +40,7 @@ public OverloadPlanBuilder(GeneratorModel model) public OverloadPlan Build() { BuildMethods(); + ValidateMatcherMethods(); return new OverloadPlan( _methodsByGroup, _matchedMatchersByGroup, @@ -81,7 +80,8 @@ private void BuildMethods() { if (!method.IsOrdinary) continue; - if (_matcherTypeDisplays.Contains(method.ContainingTypeDisplay)) continue; + // Methods of matcher types are never targets; ValidateMatcherMethods checks their attributes. + if (IsDeclaredInMatcherType(method)) continue; if (method.DeclaredAccessibility == Accessibility.Private || method.DeclaredAccessibility == Accessibility.Protected) @@ -202,28 +202,20 @@ private void BuildMethods() { if (!_matcherTypesByDisplay.TryGetValue(matcherTypeDisplay, out var matcherType)) continue; + // Invalid matcher attributes are skipped silently here. ValidateMatcherMethods reports them once, + // in the matcher type itself. foreach (var matcherMethod in SelectMatcherMethods( matcherType.MatcherMethods, method.Parameters.Items.Length)) { var matcherMethodModel = matcherMethod.Method; - if (!matcherMethodModel.IsOrdinary) continue; + if (!matcherMethodModel.IsOrdinary || matcherMethodModel.Parameters.Items.Length == 0) continue; var matcherAttributes = SelectGenerateAttributes( matcherMethod.GenerateAttributesFromAttribute, matcherMethod.GenerateAttributesFromSyntax); - if (matcherMethodModel.Parameters.Items.Length == 0) - { - var location = GetAttributeLocation(matcherAttributes) - ?? matcherMethodModel.IdentifierLocation; - Report(GeneratorDiagnostics.ParameterlessTargetMethod, location, matcherMethodModel.Name); - continue; - } - - if (HasMatchersWindowConflict(matcherMethodModel, matcherAttributes)) - continue; - + // Matchers on a matcher method are ignored (MOG021). var matcherDirectAttributes = matcherAttributes.Items.Where(attribute => !attribute.HasMatchers) .ToArray(); if (matcherDirectAttributes.Length == 0) continue; @@ -242,7 +234,7 @@ private void BuildMethods() } var matcherHasSupply = HasAnySupply(matcherMethodModel); - var matcherGroupMaps = BuildSupplyMapsByGroup(matcherMethodModel); + var matcherGroupMaps = BuildSupplyMapsByGroup(matcherMethodModel, report: false); var groupKeys = new HashSet(targetGroupMaps.Keys); groupKeys.UnionWith(matcherGroupMaps.Keys); if (groupKeys.Count == 0 && !hasAnySupply && !matcherHasSupply) @@ -269,11 +261,7 @@ private void BuildMethods() containingType, matcherMethodModel, matcherType); - if (matcherOptions.BucketType is { IsValid: false } invalidMatcherBucket) - { - ReportInvalidBucketType(invalidMatcherBucket, matcherMethodModel.IdentifierLocation); - continue; - } + if (matcherOptions.BucketType is { IsValid: false }) continue; var matches = FindSubsequenceMatches( effectiveMatcherMethod, @@ -308,26 +296,13 @@ private void BuildMethods() attribute.Args, match, out var windowSpec, - out var windowFailure)) - { + out _)) windowSpecs.Add( new WindowSpec( windowSpec.StartIndex, windowSpec.EndIndex, windowSpec.ExcludeAnyIndices, groupKey)); - - if (windowFailure.Kind == WindowSpecFailureKind.RedundantAnchors) - Report( - GeneratorDiagnostics.RedundantBeginEndAnchors, - attribute.Args.AttributeLocation ?? attribute.Args.SyntaxAttributeLocation - ?? matcherMethodModel.IdentifierLocation, - matcherMethodModel.Name); - } - else - { - ReportWindowFailure(windowFailure, attribute.Args, matcherMethodModel.Name); - } } } } @@ -368,6 +343,70 @@ private void BuildMethods() } } + /// + /// Checks the attributes of matcher methods once, independent of any target. Targets skip invalid matcher + /// attributes silently, so these diagnostics are reported only here, in the matcher type itself. + /// + private void ValidateMatcherMethods() + { + foreach (var methodTarget in _model.MethodTargets.Items) + { + var method = methodTarget.Method; + if (!method.IsOrdinary) continue; + + if (!_typesByDisplay.TryGetValue(method.ContainingTypeDisplay, out var matcherType) + || !matcherType.IsMatcher) + continue; + + BuildSupplyMapsByGroup(method); + + // Matcher frame: options on the matcher method take precedence over options on the matcher type. + if ((method.Options.BucketType ?? matcherType.Options.BucketType) is { IsValid: false } invalidBucket) + { + ReportInvalidBucketType(invalidBucket, method.IdentifierLocation); + continue; + } + + var attributes = SelectGenerateAttributes( + methodTarget.GenerateAttributesFromAttribute, + methodTarget.GenerateAttributesFromSyntax); + + if (method.Parameters.Items.Length == 0) + { + var location = GetAttributeLocation(attributes) ?? method.IdentifierLocation; + Report(GeneratorDiagnostics.ParameterlessTargetMethod, location, method.Name); + continue; + } + + // Matchers on a matcher method are ignored (MOG021, reported by the analyzer). + foreach (var attribute in attributes.Items.Where(attribute => !attribute.HasMatchers)) + if (TryCreateWindowSpecFromArgs( + method, + method, + attribute.Args, + ParameterMatch.Identity(method.Parameters.Items.Length), + out _, + out var windowFailure)) + { + if (windowFailure.Kind == WindowSpecFailureKind.RedundantAnchors) + Report( + GeneratorDiagnostics.RedundantBeginEndAnchors, + attribute.Args.AttributeLocation ?? attribute.Args.SyntaxAttributeLocation + ?? method.IdentifierLocation, + method.Name); + } + else + { + ReportWindowFailure(windowFailure, attribute.Args, method.Name); + } + } + } + + private bool IsDeclaredInMatcherType(MethodModel method) + { + return _typesByDisplay.TryGetValue(method.ContainingTypeDisplay, out var type) && type.IsMatcher; + } + private IEnumerable GenerateOverloadsForMethod( MethodModel baseMethod, List windowSpecs, @@ -940,10 +979,11 @@ private void ReportInvalidBucketType(BucketTypeModel bucketType, SourceLocationM } private Dictionary> BuildSupplyMapsByGroup( - MethodModel method) + MethodModel method, + bool report = true) { - var typeReplacements = BuildContainingTypeSupplyMapsByGroup(method); - var methodReplacements = BuildMethodSupplyMapsByGroup(method); + var typeReplacements = BuildContainingTypeSupplyMapsByGroup(method, report); + var methodReplacements = BuildMethodSupplyMapsByGroup(method, report); if (methodReplacements.Count == 0) return typeReplacements; @@ -974,7 +1014,7 @@ private void ReportInvalidBucketType(BucketTypeModel bucketType, SourceLocationM } private Dictionary> - BuildContainingTypeSupplyMapsByGroup(MethodModel method) + BuildContainingTypeSupplyMapsByGroup(MethodModel method, bool report) { if (method.ContainingTypeSupplyParameterTypes.Items.Length == 0) return new Dictionary>(); @@ -1000,7 +1040,7 @@ private void ReportInvalidBucketType(BucketTypeModel bucketType, SourceLocationM var reason = string.IsNullOrWhiteSpace(supply.InvalidReason) ? "Invalid SupplyParameterType." : supply.InvalidReason!; - Report(GeneratorDiagnostics.InvalidSupplyParameterType, location, method.Name, reason); + if (report) Report(GeneratorDiagnostics.InvalidSupplyParameterType, location, method.Name, reason); continue; } @@ -1009,11 +1049,12 @@ private void ReportInvalidBucketType(BucketTypeModel bucketType, SourceLocationM var location = supply.NameLocation ?? supply.AttributeLocation ?? method.IdentifierLocation; - Report( - GeneratorDiagnostics.SupplyParameterTypeMissingTypeParameter, - location, - supply.TypeParameterName, - method.Name); + if (report) + Report( + GeneratorDiagnostics.SupplyParameterTypeMissingTypeParameter, + location, + supply.TypeParameterName, + method.Name); continue; } @@ -1031,7 +1072,7 @@ private void ReportInvalidBucketType(BucketTypeModel bucketType, SourceLocationM scopeConflicts[supply.Group] = conflicts; } - if (conflicts.Add(supply.TypeParameterName)) + if (conflicts.Add(supply.TypeParameterName) && report) Report( GeneratorDiagnostics.SupplyParameterTypeConflicting, supply.AttributeLocation ?? method.IdentifierLocation, @@ -1071,7 +1112,8 @@ private void ReportInvalidBucketType(BucketTypeModel bucketType, SourceLocationM } private Dictionary> BuildMethodSupplyMapsByGroup( - MethodModel method) + MethodModel method, + bool report) { if (method.SupplyParameterTypes.Items.Length == 0) return new Dictionary>(); @@ -1094,7 +1136,7 @@ private void ReportInvalidBucketType(BucketTypeModel bucketType, SourceLocationM var reason = string.IsNullOrWhiteSpace(supply.InvalidReason) ? "Invalid SupplyParameterType." : supply.InvalidReason!; - Report(GeneratorDiagnostics.InvalidSupplyParameterType, location, method.Name, reason); + if (report) Report(GeneratorDiagnostics.InvalidSupplyParameterType, location, method.Name, reason); continue; } @@ -1107,11 +1149,12 @@ private void ReportInvalidBucketType(BucketTypeModel bucketType, SourceLocationM var location = supply.NameLocation ?? supply.AttributeLocation ?? method.IdentifierLocation; - Report( - GeneratorDiagnostics.SupplyParameterTypeMissingTypeParameter, - location, - supply.TypeParameterName, - method.Name); + if (report) + Report( + GeneratorDiagnostics.SupplyParameterTypeMissingTypeParameter, + location, + supply.TypeParameterName, + method.Name); continue; } @@ -1129,7 +1172,7 @@ private void ReportInvalidBucketType(BucketTypeModel bucketType, SourceLocationM conflictsByGroup[supply.Group] = conflicts; } - if (conflicts.Add(name)) + if (conflicts.Add(name) && report) Report( GeneratorDiagnostics.SupplyParameterTypeConflicting, supply.AttributeLocation ?? method.IdentifierLocation, diff --git a/src/Tenekon.MethodOverloads.SourceGenerator/GeneratorAttributesSource.cs b/src/Tenekon.MethodOverloads.SourceGenerator/GeneratorAttributesSource.cs index b3c593e..bb587a8 100644 --- a/src/Tenekon.MethodOverloads.SourceGenerator/GeneratorAttributesSource.cs +++ b/src/Tenekon.MethodOverloads.SourceGenerator/GeneratorAttributesSource.cs @@ -17,6 +17,9 @@ internal static class GeneratorAttributesSource public const string MatcherUsageAttribute = "Tenekon.MethodOverloads.MatcherUsageAttribute.cs"; + public const string OverloadMatcherAttribute = + "Tenekon.MethodOverloads.OverloadMatcherAttribute.cs"; + public const string SupplyParameterTypeAttribute = "Tenekon.MethodOverloads.SupplyParameterTypeAttribute.cs"; } diff --git a/src/Tenekon.MethodOverloads.SourceGenerator/GeneratorDiagnostics.cs b/src/Tenekon.MethodOverloads.SourceGenerator/GeneratorDiagnostics.cs index 547e2b2..3db3dab 100644 --- a/src/Tenekon.MethodOverloads.SourceGenerator/GeneratorDiagnostics.cs +++ b/src/Tenekon.MethodOverloads.SourceGenerator/GeneratorDiagnostics.cs @@ -155,4 +155,20 @@ internal static class GeneratorDiagnostics "MethodOverloadsGenerator", DiagnosticSeverity.Error, isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor UnmarkedMatcherType = new( + "MOG020", + "Matcher type is not marked with OverloadMatcher", + "'{0}' is not marked with [OverloadMatcher] and cannot be used as a matcher in '{1}'", + "MethodOverloadsGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor MatcherTypeAsTarget = new( + "MOG021", + "Matcher type cannot be a generation target", + "'{0}' is marked with [OverloadMatcher] and cannot be a generation target; the attribute is ignored", + "MethodOverloadsGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); } diff --git a/src/Tenekon.MethodOverloads.SourceGenerator/MethodOverloadsDiagnosticsAnalyzer.cs b/src/Tenekon.MethodOverloads.SourceGenerator/MethodOverloadsDiagnosticsAnalyzer.cs index 872c4f0..3161798 100644 --- a/src/Tenekon.MethodOverloads.SourceGenerator/MethodOverloadsDiagnosticsAnalyzer.cs +++ b/src/Tenekon.MethodOverloads.SourceGenerator/MethodOverloadsDiagnosticsAnalyzer.cs @@ -37,7 +37,9 @@ public sealed class MethodOverloadsDiagnosticsAnalyzer : DiagnosticAnalyzer GeneratorDiagnostics.SupplyParameterTypeConflicting, GeneratorDiagnostics.MatchersAndExcludeAnyConflict, GeneratorDiagnostics.InvalidExcludeAnyParameter, - GeneratorDiagnostics.InvalidExcludeAnyEntry + GeneratorDiagnostics.InvalidExcludeAnyEntry, + GeneratorDiagnostics.UnmarkedMatcherType, + GeneratorDiagnostics.MatcherTypeAsTarget ]; /// @@ -62,6 +64,8 @@ private static void AnalyzeType(SymbolAnalysisContext context) if (context.Symbol is not INamedTypeSymbol typeSymbol) return; var cancellationToken = context.CancellationToken; + ReportMatcherRoleDiagnostics(context, typeSymbol); + var typeTargets = ImmutableArray.CreateBuilder(); var methodTargets = ImmutableArray.CreateBuilder(); @@ -112,6 +116,80 @@ private static void AnalyzeType(SymbolAnalysisContext context) } } + /// + /// Reports how this type uses the matcher role. These are declaration checks that do not need the overload plan: + /// a matcher type must not be a target (MOG021), and every type listed in Matchers must be marked (MOG020). + /// + private static void ReportMatcherRoleDiagnostics(SymbolAnalysisContext context, INamedTypeSymbol typeSymbol) + { + var isMatcherType = Parser.IsMatcherType(typeSymbol); + + foreach (var attribute in RoslynHelpers.GetAttributes(typeSymbol, "GenerateMethodOverloadsAttribute")) + if (isMatcherType) + ReportMatcherTypeAsTarget(context, typeSymbol, attribute); + else + ReportUnmarkedMatcherTypes(context, typeSymbol, attribute); + + foreach (var member in typeSymbol.GetMembers()) + { + if (member is not IMethodSymbol methodSymbol) continue; + + foreach (var attribute in RoslynHelpers.GetAttributes(methodSymbol, "GenerateOverloadsAttribute")) + if (!isMatcherType) + ReportUnmarkedMatcherTypes(context, typeSymbol, attribute); + else if (GetMatchersArgument(attribute) is not null) + ReportMatcherTypeAsTarget(context, typeSymbol, attribute); + } + } + + private static void ReportMatcherTypeAsTarget( + SymbolAnalysisContext context, + INamedTypeSymbol typeSymbol, + AttributeData attribute) + { + if (attribute.ApplicationSyntaxReference is not { } reference) return; + + context.ReportDiagnostic( + Diagnostic.Create( + GeneratorDiagnostics.MatcherTypeAsTarget, + reference.GetSyntax(context.CancellationToken).GetLocation(), + typeSymbol.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat))); + } + + private static void ReportUnmarkedMatcherTypes( + SymbolAnalysisContext context, + INamedTypeSymbol typeSymbol, + AttributeData attribute) + { + if (GetMatchersArgument(attribute) is not { } matchers + || attribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken) is not AttributeSyntax syntax) + return; + + for (var index = 0; index < matchers.Length; index++) + { + if (matchers[index].Value is not INamedTypeSymbol matcherType || Parser.IsMatcherType(matcherType)) + continue; + + context.ReportDiagnostic( + Diagnostic.Create( + GeneratorDiagnostics.UnmarkedMatcherType, + (GetMatchersElement(syntax, index) ?? syntax).GetLocation(), + matcherType.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat), + typeSymbol.Name)); + } + } + + private static ImmutableArray? GetMatchersArgument(AttributeData attribute) + { + foreach (var named in attribute.NamedArguments) + if (string.Equals(named.Key, "Matchers", StringComparison.Ordinal)) + return named.Value.Kind == TypedConstantKind.Array && !named.Value.IsNull + ? named.Value.Values + : ImmutableArray.Empty; + + return null; + } + /// /// Maps a stored source location to an in-source location when it lies directly in this type, so that /// #pragma and [SuppressMessage] apply. Returns null for locations owned by other types. diff --git a/src/Tenekon.MethodOverloads.SourceGenerator/MethodOverloadsGenerator.cs b/src/Tenekon.MethodOverloads.SourceGenerator/MethodOverloadsGenerator.cs index 29e6081..6cd8e3d 100644 --- a/src/Tenekon.MethodOverloads.SourceGenerator/MethodOverloadsGenerator.cs +++ b/src/Tenekon.MethodOverloads.SourceGenerator/MethodOverloadsGenerator.cs @@ -47,6 +47,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context) postContext.AddSource( "MatcherUsageAttribute.g.cs", LoadAttributeSource(GeneratorAttributesSource.MatcherUsageAttribute)); + + postContext.AddSource( + "OverloadMatcherAttribute.g.cs", + LoadAttributeSource(GeneratorAttributesSource.OverloadMatcherAttribute)); }); var attributesOnlyProvider = context.AnalyzerConfigOptionsProvider.Select(static (provider, _) => diff --git a/src/Tenekon.MethodOverloads.SourceGenerator/Models/TypeModel.cs b/src/Tenekon.MethodOverloads.SourceGenerator/Models/TypeModel.cs index b2082a9..0afb052 100644 --- a/src/Tenekon.MethodOverloads.SourceGenerator/Models/TypeModel.cs +++ b/src/Tenekon.MethodOverloads.SourceGenerator/Models/TypeModel.cs @@ -9,4 +9,5 @@ internal readonly record struct TypeModel( Accessibility DeclaredAccessibility, EquatableArray Methods, EquatableArray MethodSignatures, - OverloadOptionsModel Options); + OverloadOptionsModel Options, + bool IsMatcher); diff --git a/src/Tenekon.MethodOverloads.SourceGenerator/Parsing/Parser.ModelMapper.cs b/src/Tenekon.MethodOverloads.SourceGenerator/Parsing/Parser.ModelMapper.cs index 10a1778..3936fb0 100644 --- a/src/Tenekon.MethodOverloads.SourceGenerator/Parsing/Parser.ModelMapper.cs +++ b/src/Tenekon.MethodOverloads.SourceGenerator/Parsing/Parser.ModelMapper.cs @@ -45,7 +45,17 @@ internal static TypeModel BuildTypeModel(INamedTypeSymbol typeSymbol, Cancellati typeSymbol.DeclaredAccessibility, new EquatableArray([.. methods]), new EquatableArray([.. signatures]), - options); + options, + IsMatcherType(typeSymbol)); + } + + /// + /// A type is a matcher only when it is marked with OverloadMatcherAttribute. Matcher types are never targets, and + /// unmarked types are never used as matchers. + /// + internal static bool IsMatcherType(INamedTypeSymbol typeSymbol) + { + return RoslynHelpers.GetAttribute(typeSymbol.OriginalDefinition, "OverloadMatcherAttribute") is not null; } internal static MethodModel BuildMethodModel(IMethodSymbol methodSymbol, CancellationToken cancellationToken) @@ -609,6 +619,10 @@ internal static (ImmutableArray Displays, ImmutableArray(matcherDisplays), @@ -56,7 +57,7 @@ internal static class TargetFactory var (attributeModels, syntaxModels) = Parser.ExtractGenerateOverloadsAttributes( methodSymbol, cancellationToken); - var (matcherDisplays, matcherModels) = Parser.ExtractMatcherTypes(attributes, cancellationToken); + var (matcherDisplays, matcherModels) = ExtractMatcherTypes(typeModel, attributes, cancellationToken); return new MethodTargetInput( methodModel, @@ -66,4 +67,15 @@ internal static class TargetFactory new EquatableArray(matcherDisplays), new EquatableArray(matcherModels)); } + + // A matcher type is never a target, so its Matchers are ignored; the analyzer reports them (MOG021). + private static (ImmutableArray Displays, ImmutableArray Models) ExtractMatcherTypes( + TypeModel ownerType, + ImmutableArray attributes, + CancellationToken cancellationToken) + { + return ownerType.IsMatcher + ? (ImmutableArray.Empty, ImmutableArray.Empty) + : Parser.ExtractMatcherTypes(attributes, cancellationToken); + } } \ No newline at end of file diff --git a/tests/Tenekon.MethodOverloads.SourceGenerator.Tests/DiagnosticSuppressionTests.cs b/tests/Tenekon.MethodOverloads.SourceGenerator.Tests/DiagnosticSuppressionTests.cs index 66ed835..840f972 100644 --- a/tests/Tenekon.MethodOverloads.SourceGenerator.Tests/DiagnosticSuppressionTests.cs +++ b/tests/Tenekon.MethodOverloads.SourceGenerator.Tests/DiagnosticSuppressionTests.cs @@ -64,6 +64,7 @@ public void Matcher_without_match_is_reported_at_typeof_reference_of_target() namespace Demo; + [OverloadMatcher] internal interface IMatcher { [GenerateOverloads(nameof(value))] diff --git a/tests/Tenekon.MethodOverloads.SourceGenerator.Tests/DiagnosticsTests.Diagnostics_HaveNoChanges.verified.txt b/tests/Tenekon.MethodOverloads.SourceGenerator.Tests/DiagnosticsTests.Diagnostics_HaveNoChanges.verified.txt index baae0b9..07a082a 100644 --- a/tests/Tenekon.MethodOverloads.SourceGenerator.Tests/DiagnosticsTests.Diagnostics_HaveNoChanges.verified.txt +++ b/tests/Tenekon.MethodOverloads.SourceGenerator.Tests/DiagnosticsTests.Diagnostics_HaveNoChanges.verified.txt @@ -150,5 +150,21 @@ Category: MethodOverloadsGenerator, DefaultSeverity: 3, IsEnabledByDefault: true + }, + { + Id: MOG020, + Title: Matcher type is not marked with OverloadMatcher, + MessageFormat: '{0}' is not marked with [OverloadMatcher] and cannot be used as a matcher in '{1}', + Category: MethodOverloadsGenerator, + DefaultSeverity: 3, + IsEnabledByDefault: true + }, + { + Id: MOG021, + Title: Matcher type cannot be a generation target, + MessageFormat: '{0}' is marked with [OverloadMatcher] and cannot be a generation target; the attribute is ignored, + Category: MethodOverloadsGenerator, + DefaultSeverity: 3, + IsEnabledByDefault: true } ] From 34971c4e7818e4741064296bf37076a66d23e0ac Mon Sep 17 00:00:00 2001 From: Teneko Date: Fri, 9 Oct 2026 08:44:23 +0200 Subject: [PATCH 2/3] test: declare explicit matcher role as acceptance criteria Acceptance cases for the matcher role: - Class_46: an unmarked type in Matchers is ignored and keeps its own role, so its own overload is generated; MOG020 at both typeof references (type and method level). - Class_47: [GenerateMethodOverloads] on a matcher type (MOG021). - Class_48: one matcher per path of the matcher validation step (MOG001, MOG008, MOG011, MOG013, MOG014) and Matchers on a matcher method (MOG021). The error kinds themselves are already covered at targets. A target still gets the overload from the valid attribute of the MOG021 matcher. - Class_49: an unused matcher in the shape of Class_4_Matcher produces no overloads and no MOG006. Matchers that expect diagnostics are abstract classes, because the acceptance infrastructure maps diagnostics only inside classes. The diagnostics comparison works on IDs per class and cannot count, so UnmarkedMatcherTests checks on the acceptance output that MOG020 is reported at both typeof references of Class_46. OverloadMatcher usages on class, struct and interface feed the attribute usage test, and the attributes-only test checks that the attribute is emitted. Mutation checks: disabling the validation step fails the five validation cases of Class_48; treating matcher methods as targets fails Class_49 (among others); skipping a matcher method because of its Matchers attribute fails Class_48. Co-Authored-By: Claude Opus 5.5 --- .../Attributes/AttributeTargets.cs | 15 +++ .../Class_46.cs | 38 +++++++ .../Class_47.cs | 21 ++++ .../Class_48.cs | 106 ++++++++++++++++++ .../Class_49.cs | 21 ++++ .../AttributeUsageTests.cs | 1 + .../AttributesOnlyTests.cs | 3 + .../UnmarkedMatcherTests.cs | 27 +++++ 8 files changed, 232 insertions(+) create mode 100644 ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_46.cs create mode 100644 ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_47.cs create mode 100644 ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_48.cs create mode 100644 ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_49.cs create mode 100644 tests/Tenekon.MethodOverloads.SourceGenerator.Tests/UnmarkedMatcherTests.cs diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Attributes/AttributeTargets.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Attributes/AttributeTargets.cs index f5834d0..0e2fa91 100644 --- a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Attributes/AttributeTargets.cs +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Attributes/AttributeTargets.cs @@ -61,6 +61,21 @@ public class MatcherUsageTargets { } +[OverloadMatcher] +public class OverloadMatcherOnClass +{ +} + +[OverloadMatcher] +public struct OverloadMatcherOnStruct +{ +} + +[OverloadMatcher] +public interface OverloadMatcherOnInterface +{ +} + public class SupplyParameterTypeTargets { [SupplyParameterType(nameof(TValue), typeof(object))] diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_46.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_46.cs new file mode 100644 index 0000000..1a73b5e --- /dev/null +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_46.cs @@ -0,0 +1,38 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Tenekon.MethodOverloads.AcceptanceCriterias; + +/// +/// Not marked with [OverloadMatcher], so it is no matcher and keeps its own role: +/// its GenerateOverloads method is a direct target, even though Class_46 lists the type in Matchers. +/// +public interface Class_46_Service +{ + [GenerateOverloads(nameof(param_b))] + void Case_1(int param_a, string? param_b); +} + +/// +/// An unmarked type in Matchers is ignored and produces MOG020 at every typeof reference: here once on the type and +/// once on Case_2. +/// +[SuppressMessage("MethodOverloadsGenerator", "MOG020")] +[GenerateMethodOverloads(Matchers = [typeof(Class_46_Service)])] +public sealed class Class_46 +{ + public void Case_1(int param_1, string? param_2) { } + + [GenerateOverloads(Matchers = [typeof(Class_46_Service)])] + public void Case_2(int param_1, string? param_2) { } +} + +/// +/// Overloads are expected for Class_46_Service only; Class_46 gets none. +/// +public static class Class_46_AcceptanceCriterias +{ + public static void Case_1(this Class_46_Service source, int param_a) + { + source.Case_1(param_a, param_b: null); + } +} diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_47.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_47.cs new file mode 100644 index 0000000..78489de --- /dev/null +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_47.cs @@ -0,0 +1,21 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Tenekon.MethodOverloads.AcceptanceCriterias; + +/// +/// A matcher type is never a target: GenerateMethodOverloads on a type marked with [OverloadMatcher] produces MOG021 +/// and is ignored. +/// +[OverloadMatcher] +[SuppressMessage("MethodOverloadsGenerator", "MOG021")] +[GenerateMethodOverloads] +public abstract class Class_47_Matcher +{ + [GenerateOverloads(nameof(param_b))] + public abstract void Case_1(int param_a, string? param_b); +} + +public static class Class_47_AcceptanceCriterias +{ + // No overloads expected because matcher types are never targets. +} diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_48.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_48.cs new file mode 100644 index 0000000..1da489b --- /dev/null +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_48.cs @@ -0,0 +1,106 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Tenekon.MethodOverloads.AcceptanceCriterias; + +// One matcher per path of the matcher validation step. Each matcher reports its error in the matcher type itself, +// whether or not a target uses it. The error kinds themselves are covered at targets (e.g. Class_21, Class_44, +// SupplyParameterType_Class_7). The matchers are abstract classes because the acceptance infrastructure maps +// diagnostics only inside classes. The last matcher is used by Class_48: its Matchers attribute is ignored (MOG021), +// its other attribute still applies. + +public sealed class Class_48_Bucket; + +public sealed class Class_48_Constraint; + +public sealed class Class_48_Param; + +/// +/// Invalid window (MOG001). Stands for all window errors (MOG001, MOG007, MOG009, MOG010, MOG018, MOG019), which +/// share one check. +/// +[OverloadMatcher] +[SuppressMessage("MethodOverloadsGenerator", "MOG001")] +public abstract class Class_48_InvalidWindowMatcher +{ + [GenerateOverloads(Begin = "missing")] + public abstract void Match(int value); +} + +/// +/// Valid window with identical Begin and End (MOG008). +/// +[OverloadMatcher] +[SuppressMessage("MethodOverloadsGenerator", "MOG008")] +public abstract class Class_48_RedundantAnchorsMatcher +{ + [GenerateOverloads(Begin = nameof(value), End = nameof(value))] + public abstract void Match(int value); +} + +/// +/// Matcher method without parameters (MOG011). +/// +[OverloadMatcher] +[SuppressMessage("MethodOverloadsGenerator", "MOG011")] +public abstract class Class_48_ParameterlessMatcher +{ + [GenerateOverloads] + public abstract void Match(); +} + +/// +/// Invalid bucket type (MOG013). +/// +[OverloadMatcher] +[SuppressMessage("MethodOverloadsGenerator", "MOG013")] +public abstract class Class_48_InvalidBucketMatcher +{ + [GenerateOverloads(nameof(value))] + [OverloadGenerationOptions(BucketType = typeof(Class_48_Bucket))] + public abstract void Match(int value); +} + +/// +/// Invalid SupplyParameterType (MOG014). Stands for all supply errors (MOG014, MOG015, MOG016), which share one +/// check. +/// +[OverloadMatcher] +[SuppressMessage("MethodOverloadsGenerator", "MOG014")] +public abstract class Class_48_InvalidSupplyMatcher +{ + [GenerateOverloads(nameof(optionalObject))] + [SupplyParameterType("TConstraint", typeof(Class_48_Constraint))] + public abstract void Match(Class_48_Param param, object? optionalObject); +} + +/// +/// Matchers on a matcher method are reported (MOG021) and ignored; the other attribute of the method still applies. +/// +[OverloadMatcher] +[SuppressMessage("MethodOverloadsGenerator", "MOG021")] +public abstract class Class_48_MatchersOnMatcherMethodMatcher +{ + [GenerateOverloads(nameof(param_b))] + [GenerateOverloads(Matchers = [typeof(Class_48_InvalidWindowMatcher)])] + public abstract void Match(int param_a, string? param_b); +} + +/// +/// Uses the matcher above and gets the overload from its valid attribute. +/// +[GenerateMethodOverloads(Matchers = [typeof(Class_48_MatchersOnMatcherMethodMatcher)])] +public sealed class Class_48 +{ + public void Case_1(int param_1, string? param_2) { } +} + +/// +/// Only Class_48 gets overloads; matcher types are never targets. +/// +public static class Class_48_AcceptanceCriterias +{ + public static void Case_1(this Class_48 source, int param_1) + { + source.Case_1(param_1, param_2: null); + } +} diff --git a/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_49.cs b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_49.cs new file mode 100644 index 0000000..2854cac --- /dev/null +++ b/ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_49.cs @@ -0,0 +1,21 @@ +namespace Tenekon.MethodOverloads.AcceptanceCriterias; + +/// +/// A matcher type is never a target, even when no target uses it: no overloads and no diagnostics. +/// As a target, the second method would produce MOG006 (its overload duplicates the first method). +/// The matcher is an abstract class because the acceptance infrastructure maps diagnostics only inside classes. +/// +[OverloadMatcher] +public abstract class Class_49_Matcher +{ + [GenerateOverloads(EndExclusive = nameof(param_b))] + public abstract void Matcher_2(bool param_a, CancellationToken param_b); + + [GenerateOverloads(EndExclusive = nameof(param_b))] + public abstract void Matcher_2(string? param_a, bool param_b, CancellationToken param_c); +} + +public static class Class_49_AcceptanceCriterias +{ + // No overloads expected because matcher types are never targets. +} diff --git a/tests/Tenekon.MethodOverloads.SourceGenerator.Tests/AttributeUsageTests.cs b/tests/Tenekon.MethodOverloads.SourceGenerator.Tests/AttributeUsageTests.cs index 3f911d1..dd79da2 100644 --- a/tests/Tenekon.MethodOverloads.SourceGenerator.Tests/AttributeUsageTests.cs +++ b/tests/Tenekon.MethodOverloads.SourceGenerator.Tests/AttributeUsageTests.cs @@ -31,6 +31,7 @@ public void Attribute_usage_matches_acceptance_targets() "GenerateOverloads", "OverloadGenerationOptions", "MatcherUsage", + "OverloadMatcher", "SupplyParameterType" }; diff --git a/tests/Tenekon.MethodOverloads.SourceGenerator.Tests/AttributesOnlyTests.cs b/tests/Tenekon.MethodOverloads.SourceGenerator.Tests/AttributesOnlyTests.cs index 072255d..89024f3 100644 --- a/tests/Tenekon.MethodOverloads.SourceGenerator.Tests/AttributesOnlyTests.cs +++ b/tests/Tenekon.MethodOverloads.SourceGenerator.Tests/AttributesOnlyTests.cs @@ -56,6 +56,9 @@ public void Test(int value, string? text) { } generatedTrees, tree => tree.Contains("class OverloadGenerationOptionsAttribute", StringComparison.Ordinal)); Assert.Contains(generatedTrees, tree => tree.Contains("class MatcherUsageAttribute", StringComparison.Ordinal)); + Assert.Contains( + generatedTrees, + tree => tree.Contains("class OverloadMatcherAttribute", StringComparison.Ordinal)); Assert.DoesNotContain( generatedTrees, tree => tree.Contains("public static class MethodOverloads", StringComparison.Ordinal)); diff --git a/tests/Tenekon.MethodOverloads.SourceGenerator.Tests/UnmarkedMatcherTests.cs b/tests/Tenekon.MethodOverloads.SourceGenerator.Tests/UnmarkedMatcherTests.cs new file mode 100644 index 0000000..646ad9f --- /dev/null +++ b/tests/Tenekon.MethodOverloads.SourceGenerator.Tests/UnmarkedMatcherTests.cs @@ -0,0 +1,27 @@ +using Microsoft.CodeAnalysis; +using Tenekon.MethodOverloads.SourceGenerator.Tests.Infrastructure; + +namespace Tenekon.MethodOverloads.SourceGenerator.Tests; + +public sealed class UnmarkedMatcherTests +{ + [Fact] + public void Unmarked_matcher_type_is_reported_at_every_typeof_reference() + { + var fixture = AcceptanceFixtureCache.Instance; + + // Class_46 lists the unmarked Class_46_Service twice: on the type and on Case_2. + var diagnostics = fixture.Diagnostics.Where(diagnostic => diagnostic.Id == "MOG020").ToArray(); + + Assert.Equal(2, diagnostics.Length); + Assert.All( + diagnostics, + diagnostic => Assert.Equal("typeof(Class_46_Service)", GetLocationText(diagnostic.Location))); + Assert.NotEqual(diagnostics[0].Location.SourceSpan, diagnostics[1].Location.SourceSpan); + } + + private static string GetLocationText(Location location) + { + return location.SourceTree!.GetText().ToString(location.SourceSpan); + } +} From ba3aded1804f0435715437ba9829ed277379a8fd Mon Sep 17 00:00:00 2001 From: Teneko Date: Fri, 9 Oct 2026 08:44:23 +0200 Subject: [PATCH 3/3] docs: document explicit matcher types Describe [OverloadMatcher], the matcher type rules, MOG020 and MOG021 in the README (including the breaking change) and in docs/generator.md, where formal diagnostics on matcher methods are now reported once in the matcher type. docs/acceptance-criterias.md explains where MOG020 is declared, why matchers in acceptance cases are abstract classes, and which test counts MOG020. Co-Authored-By: Claude Opus 5.5 --- README.md | 16 ++++++++++++++-- docs/acceptance-criterias.md | 15 ++++++++++++++- docs/generator.md | 22 ++++++++++++++++++---- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5b52c9e..5f0de7e 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ A C# source generator that creates extension overloads by treating a selected pa ## Quickstart -1. Add `[GenerateOverloads]` to a method, or `[GenerateMethodOverloads(Matchers = ...)]` to a type. +1. Add `[GenerateOverloads]` to a method, or `[GenerateMethodOverloads(Matchers = ...)]` to a type. Mark matcher types with `[OverloadMatcher]`. 2. (Optional) Add `[OverloadGenerationOptions(...)]` to control matching and output. 3. Build. Generated code appears as `MethodOverloads_*.g.cs`. @@ -25,7 +25,7 @@ A C# source generator that creates extension overloads by treating a selected pa - **Window**: the parameter range that can be omitted to produce overloads. - **ExcludeAny**: a list of parameter names that must be omitted in every overload within the window. -- **Matchers**: define windows on matcher methods and apply them to target methods. +- **Matchers**: define windows on matcher methods and apply them to target methods. Matcher types are marked with `[OverloadMatcher]` and are never targets themselves. - **Bucketization**: route generated methods into a specific static partial class. - **SupplyParameterType**: substitute method type parameters with concrete types before generation. @@ -99,6 +99,7 @@ public sealed class UserService public void UpdateUser(string id, string name, int level, bool active) { } } +[OverloadMatcher] internal interface UserMatchers { [GenerateOverloads(nameof(paramB))] @@ -131,6 +132,7 @@ public static class MathUtils public static void Multiply(int left, int right, bool checkedOverflow) { } } +[OverloadMatcher] internal interface MathMatchers { [GenerateOverloads(nameof(paramB))] @@ -152,6 +154,14 @@ public static class MethodOverloads } ``` +Matcher types: + +- A matcher type is marked with `[OverloadMatcher]`. Its `[GenerateOverloads]` methods only describe windows; the type itself never gets overloads, whether or not a target uses it. +- A type listed in `Matchers` without `[OverloadMatcher]` is reported (`MOG020`) and ignored. It keeps its own role, so its own `[GenerateOverloads]` methods still get overloads. +- `[GenerateMethodOverloads]` on a matcher type, or `Matchers` on one of its methods, is reported (`MOG021`) and ignored. + +> **Breaking change:** a type used to become a matcher implicitly as soon as any target listed it in `Matchers`, which also removed the overloads of its own `[GenerateOverloads]` methods. Matcher types must now be marked with `[OverloadMatcher]`. + ### 6) Range Anchor Match Mode `RangeAnchorMatchMode.TypeOnly` (default) matches by type only. @@ -279,6 +289,8 @@ Diagnostics are reported by the analyzer, live in the IDE and during build: - `MOG017` Matchers + ExcludeAny conflict. - `MOG018` ExcludeAny refers to missing/out-of-window parameter. - `MOG019` ExcludeAny contains invalid entries. +- `MOG020` Type in `Matchers` is not marked with `[OverloadMatcher]` (reported at the `typeof` reference). +- `MOG021` Matcher type used as a generation target (`[GenerateMethodOverloads]` on it, or `Matchers` on one of its methods). Diagnostics can be suppressed in source like any other analyzer diagnostic: diff --git a/docs/acceptance-criterias.md b/docs/acceptance-criterias.md index fd21393..4c5963b 100644 --- a/docs/acceptance-criterias.md +++ b/docs/acceptance-criterias.md @@ -80,5 +80,18 @@ Place it on the class or member that contains the diagnostic location. It does t - It suppresses the diagnostic in the real build and in the IDE. `MOG002` is reported at the `typeof` reference in the target's `Matchers`, so its `[SuppressMessage]` belongs on -the target class, not on the matcher. +the target class, not on the matcher. The same applies to `MOG020` (a type in `Matchers` without `[OverloadMatcher]`). + +## Matcher types + +Every type listed in `Matchers` must be marked with `[OverloadMatcher]`; otherwise the target gets `MOG020` and the +type is not used as a matcher (see `Class_46`). + +Diagnostics inside interfaces are not mapped to a case. Cases about matcher types therefore use an abstract class as +matcher: `Class_47` (MOG021), `Class_48` (one matcher per path of the matcher validation step; the error kinds +themselves are covered at targets) and `Class_49` (an unused matcher produces no overloads and no diagnostics). + +Diagnostics are compared by ID per class, so a case cannot express how often a diagnostic is reported. +`UnmarkedMatcherTests` checks on the acceptance output that `MOG020` is reported at every `typeof` reference of +`Class_46`. diff --git a/docs/generator.md b/docs/generator.md index 60e3c46..9a839fb 100644 --- a/docs/generator.md +++ b/docs/generator.md @@ -4,7 +4,7 @@ This doc explains what the generator looks at, how it decides what to emit, and ## Quickstart -1) Mark a method with [GenerateOverloads] or a type with [GenerateMethodOverloads(Matchers = [...])]. +1) Mark a method with [GenerateOverloads] or a type with [GenerateMethodOverloads(Matchers = [...])]. Mark matcher types with [OverloadMatcher]. 2) (Optional) Add [OverloadGenerationOptions(...)] to control matching and output. 3) Build the project. Generated overloads appear in MethodOverloads_.g.cs. @@ -17,6 +17,7 @@ Create extension overloads from a single method by treating a parameter span as - Target method: the method the generator is trying to add overloads for. - Optional window: a contiguous range of parameters that can be omitted. - Subsequence: an order-preserving omission set inside the optional window. +- Matcher type: a type marked with [OverloadMatcher]. Its GenerateOverloads methods are matcher methods. - Matcher method: a method signature used to match target methods by subsequence. - Direct generation: GenerateOverloads on the target method itself. - Matcher-based generation: GenerateMethodOverloads on a type (or Matchers on a method). @@ -27,12 +28,21 @@ The generator scans all syntax trees and records: - All declared types and their methods. - GenerateMethodOverloads on types (type-level matchers). Multiple attributes are allowed; matcher types are unioned. - GenerateOverloads on methods, including Matchers = [...] on the method (method-level matchers). +- OverloadMatcher on types (matcher types). + +Matcher types are explicit: +- Only types marked with [OverloadMatcher] are used as matchers. An unmarked type in Matchers is ignored and + reported (MOG020); it keeps its own role, so its own GenerateOverloads methods are still direct targets. +- A matcher type is a matcher whether or not a target lists it in Matchers. +- GenerateMethodOverloads on a matcher type, or Matchers on a GenerateOverloads of one of its methods, is ignored and + reported (MOG021). Other GenerateOverloads attributes of that matcher method still apply. +- Methods of a matcher type without GenerateOverloads are neither matcher methods nor targets. Only ordinary methods are considered (no constructors, operators, etc.). Methods are skipped if they are: - Declared private or protected -- Declared in a matcher type (matcher types are never targets) +- Declared in a type marked with [OverloadMatcher] (matcher types are never targets) ## 4) Optional window rules @@ -124,8 +134,12 @@ Diagnostics are reported by the analyzer (not by the generator): The analyzer analyzes each type on its own (symbol action) with the same parsing and planning logic as the generator: - Diagnostics are reported live in the IDE, not only at the end of a build. -- Each type reports only diagnostics located in its own declaration; diagnostics at a matcher are reported when the - matcher type itself is analyzed. +- Each type reports only diagnostics located in its own declaration. +- Formal diagnostics on matcher methods (MOG001, MOG007–MOG011, MOG013–MOG016, MOG018, MOG019) are reported once, + when the matcher type itself is analyzed, whether or not a target uses it. Targets skip invalid matcher attributes + silently. Generation diagnostics (MOG003–MOG006) are reported only at targets. +- MOG020 and MOG021 are declaration checks: MOG020 is reported at every `typeof` reference to an unmarked type in + `Matchers`; MOG021 is reported at the offending attribute in the matcher type. - Locations are in source, so `#pragma warning disable`, `[SuppressMessage]` and per-file `.editorconfig` severities apply. - MOG002 is evaluated per target: it is reported at the `typeof` reference in `Matchers` when a matcher method has