Skip to content

feat!: require [OverloadMatcher] on matcher types - #5

Draft
teneko wants to merge 3 commits into
fix/analyzer-diagnosticsfrom
feat/explicit-matcher-role
Draft

teneko wants to merge 3 commits into
fix/analyzer-diagnosticsfrom
feat/explicit-matcher-role

Conversation

@teneko

@teneko teneko commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

Stacked on #4 (fix/analyzer-diagnostics). Retarget this PR to main once #4 is merged.

Problem

A type became a matcher implicitly, only because some target listed it in Matchers = [typeof(T)]. The role was decided somewhere else, which caused two problems:

  • Generator: IUserService with [GenerateOverloads(nameof(name))] void Update(int id, string? name); gets Update(this IUserService, int). As soon as another class adds [GenerateOverloads(Matchers = [typeof(IUserService)])] to its own method, the IUserService overload disappears (OverloadPlanBuilder skipped every type that was referenced as a matcher anywhere).
  • Analyzer (fix: report analyzer diagnostics per type at in-source locations #4): each type is analyzed on its own, so an unreferenced-looking matcher type was treated as a direct target. This produced false generation diagnostics: two MOG006 in the acceptance project (Class_4_Matcher.Matcher_2, Bug_Class_1_ICommandRuntimeFactoryMatchers.Matcher). fix: report analyzer diagnostics per type at in-source locations #4 documents this as a known limitation.

Change

The matcher role is now explicit and local to the type:

[OverloadMatcher]
internal interface UserMatchers
{
    [GenerateOverloads(nameof(paramB))]
    void UpdateUser(int paramA, bool paramB);
}
Situation Behavior
Type marked with [OverloadMatcher] Matcher, never a target, whether or not anything references it
Unmarked type in Matchers Not used as a matcher, keeps its own role. MOG020 (Error) at every typeof reference
[GenerateMethodOverloads] on a matcher type, or Matchers on one of its methods Ignored. MOG021 (Error) at that attribute. Other [GenerateOverloads] of the matcher method still apply
Methods of a matcher type without [GenerateOverloads] Neither matcher methods nor targets (unchanged)

New [OverloadMatcher] attribute: Class | Struct | Interface, no AllowMultiple, embedded like the other attributes.

Implementation

  • TypeModel.IsMatcher comes from the marker (Parser.IsMatcherType). OverloadPlanBuilder skips methods of matcher types via this flag instead of the global set of referenced matcher types. Parser.ExtractMatcherTypes ignores unmarked types. TargetFactory ignores Matchers on matcher types.
  • Formal checks of matcher attributes run in one place: the new ValidateMatcherMethods step reports MOG001, MOG007–MOG011, MOG013–MOG016, MOG018 and MOG019 once, in the matcher type's own analysis, independent of any target. The matcher path of a target now skips invalid matcher attributes silently; before, it reported diagnostics at matcher locations that the analyzer discarded. Generation diagnostics (MOG003–MOG006) are reported only at targets.
  • MOG020 and MOG021 are declaration checks done directly in the analyzer. The analyzer reads them from symbols and syntax, so the locations are exact, and generator inputs carry no new diagnostics or source locations (see the planned "models without locations" PR).

Breaking change

Types listed in Matchers must be marked with [OverloadMatcher]. Unmarked types produce MOG020 and are ignored. Documented in the README.

Minor: a matcher method whose [GenerateOverloads] combines Matchers with window anchors or ExcludeAny was skipped entirely before (MOG012/MOG017, never visible at the matcher). Now only that attribute is ignored (MOG021).

Diagnostics before/after (acceptance sources, in-memory analyzer dump)

All 35 matcher types are marked. The suggested grep for Matchers = [...] missed SupplyParameterType_Class_12, which spans lines; MOG020 flagged its two matchers.

Before After
Total 39 39
In matcher types 2 × MOG006, live, false positives 0
In targets 37 the same 37 (identical apart from line shifts by the inserted attribute)
New – MOG020 in Class_46, MOG021 in Class_47 (new cases, suppressed as expected)

Generated overloads of all existing acceptance cases are unchanged.

Tests

Tests follow the acceptance-criteria approach (docs/testing.md):

  • Class_46: an unmarked type in Matchers is ignored and keeps its own role, so its own overload is generated. MOG020 is reported 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 (invalid window MOG001, redundant anchors MOG008, parameterless MOG011, invalid bucket MOG013, invalid SupplyParameterType 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 (the original false positive).
  • Matchers that expect diagnostics are abstract classes, because the acceptance infrastructure maps diagnostics only inside classes.
  • UnmarkedMatcherTests checks on the acceptance output that MOG020 is reported twice in Class_46. The diagnostics comparison works on IDs per class and cannot count.
  • OverloadMatcher usages on class, struct and interface for AttributeUsageTests. AttributesOnlyTests checks that the attribute is emitted. The diagnostics snapshot gains MOG020 and MOG021. DiagnosticSuppressionTests marks its in-memory matcher.
  • Mutation checks:
    • disabling ValidateMatcherMethods 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

Verification

  • dotnet test --nologo: 131 passed. The feat! commit alone: 120 passed.
  • dotnet build ref/...csproj -c Release -nodeReuse:false --no-incremental with TenekonMethodOverloadsSourceGeneratorAttributesOnly=false and =true: 0 errors, 0 MOG warnings. The CS8625 warnings are unchanged (59 in the full build).
  • Patch coverage measured locally: 89.0%. Uncovered are mostly the report guards for SupplyParameterType errors on matcher types. The acceptance project has no case for type-level SupplyParameterType errors at all (pre-existing). No tests were added only for coverage.

Out of scope

  • Generator models without source locations (separate PR).
  • Matcher types from other assemblies.
  • Acceptance infrastructure gaps (interfaces not mapped, comparison per class only, regex parsing of extension blocks, return type missing from the key).

🤖 Generated with Claude Code

@codecov

codecov Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.96104% with 17 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
....SourceGenerator/Generation/OverloadPlanBuilder.cs 81.15% 5 Missing and 8 partials ⚠️
...rceGenerator/MethodOverloadsDiagnosticsAnalyzer.cs 92.30% 0 Missing and 4 partials ⚠️

📢 Thoughts on this report? Let us know!

@teneko
teneko added this pull request to stack #6 October 9, 2026 06:20
@teneko
teneko force-pushed the feat/explicit-matcher-role branch from 46d1bf2 to 89e3843 Compare October 9, 2026 06:36
teneko and others added 3 commits October 9, 2026 08:44
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@teneko
teneko force-pushed the feat/explicit-matcher-role branch from 89e3843 to ba3aded Compare October 9, 2026 06:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant