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 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/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_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/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/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/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 } ] 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); + } +}