Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ 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_<Namespace>*.g.cs`.

## Core Concepts

- **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.

Expand Down Expand Up @@ -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))]
Expand Down Expand Up @@ -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))]
Expand All @@ -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.
Expand Down Expand Up @@ -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:

Expand Down
15 changes: 14 additions & 1 deletion docs/acceptance-criterias.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

22 changes: 18 additions & 4 deletions docs/generator.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_<Namespace>.g.cs.

Expand All @@ -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).
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public sealed class Bug_Class_1_Type_3;

public interface Bug_Class_1_Type_4<TConstraint>;

[OverloadMatcher]
internal interface Bug_Class_1_ICommandRuntimeFactoryMatchers
{
[GenerateOverloads(Begin = nameof(param_3))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Tenekon.MethodOverloads.AcceptanceCriterias;

[OverloadMatcher]
internal interface Class_16_Matcher
{
[GenerateOverloads(nameof(param_b))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Tenekon.MethodOverloads.AcceptanceCriterias;

[OverloadGenerationOptions(RangeAnchorMatchMode = RangeAnchorMatchMode.TypeAndName)]
[OverloadMatcher]
internal interface Class_18_Matcher
{
[GenerateOverloads(nameof(param_2))]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace Tenekon.MethodOverloads.AcceptanceCriterias;

[OverloadMatcher]
internal interface Class_19_Matcher
{
[GenerateOverloads(nameof(param_b))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Tenekon.MethodOverloads.AcceptanceCriterias;

[OverloadMatcher]
internal interface Class_20_Matcher
{
[GenerateOverloads(nameof(param_b))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public static void Duplicate(int value) { }
public static void Parameterless() { }
}

[OverloadMatcher]
internal interface Class_21_Matcher
{
[GenerateOverloads(nameof(m))]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace Tenekon.MethodOverloads.AcceptanceCriterias;

[OverloadMatcher]
internal interface Class_23_Matcher
{
[GenerateOverloads(nameof(param_b))]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace Tenekon.MethodOverloads.AcceptanceCriterias;

[OverloadMatcher]
internal interface Class_24_Matcher
{
[GenerateOverloads(nameof(param_b))]
Expand Down
2 changes: 2 additions & 0 deletions ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_25.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Tenekon.MethodOverloads.AcceptanceCriterias;

[OverloadMatcher]
internal interface Class_25_MatcherUnused
{
[GenerateOverloads(nameof(param_a))]
Expand All @@ -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))]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace Tenekon.MethodOverloads.AcceptanceCriterias;

[OverloadMatcher]
internal interface Class_26_Matcher
{
[GenerateOverloads(nameof(param_b))]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace Tenekon.MethodOverloads.AcceptanceCriterias;

[OverloadMatcher]
internal interface Class_27_Matcher
{
[GenerateOverloads(nameof(param_b))]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace Tenekon.MethodOverloads.AcceptanceCriterias;

[OverloadMatcher]
internal interface Class_28_Matcher
{
[GenerateOverloads(Begin = nameof(param_a))]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace Tenekon.MethodOverloads.AcceptanceCriterias;

[OverloadGenerationOptions(RangeAnchorMatchMode = RangeAnchorMatchMode.TypeAndName)]
[OverloadMatcher]
internal interface Class_29_Matcher
{
[GenerateOverloads(nameof(param_2))]
Expand Down
1 change: 1 addition & 0 deletions ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_3.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias;
/// <summary>
///
/// </summary>
[OverloadMatcher]
internal interface Class_3_Matcher
{
[GenerateOverloads(EndExclusive = nameof(param_b))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Tenekon.MethodOverloads.AcceptanceCriterias;

[OverloadMatcher]
internal interface Class_34_Matcher
{
[GenerateOverloads(nameof(param_b))]
Expand Down
2 changes: 2 additions & 0 deletions ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_36.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias;
/// <summary>
/// Matcher type A for matcher-union testing.
/// </summary>
[OverloadMatcher]
internal interface Class_36_MatcherA
{
/// <summary>
Expand All @@ -15,6 +16,7 @@ internal interface Class_36_MatcherA
/// <summary>
/// Matcher type B for matcher-union testing.
/// </summary>
[OverloadMatcher]
internal interface Class_36_MatcherB
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias;
/// <summary>
/// Matcher type for mixed direct + matcher generation.
/// </summary>
[OverloadMatcher]
internal interface Class_37_Matcher
{
/// <summary>
Expand Down
2 changes: 2 additions & 0 deletions ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_38.cs
Original file line number Diff line number Diff line change
@@ -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))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias;
/// SubsequenceStrategy precedence between target and matcher frames.
/// </summary>
[OverloadGenerationOptions(SubsequenceStrategy = OverloadSubsequenceStrategy.PrefixOnly)]
[OverloadMatcher]
internal interface Class_39_Matcher
{
[OverloadGenerationOptions(SubsequenceStrategy = OverloadSubsequenceStrategy.UniqueBySignature)]
Expand Down
1 change: 1 addition & 0 deletions ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_4.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace Tenekon.MethodOverloads.AcceptanceCriterias;

[OverloadMatcher]
internal interface Class_4_Matcher
{
[GenerateOverloads(EndExclusive = nameof(param_b))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ namespace Tenekon.MethodOverloads.AcceptanceCriterias;
/// OverloadVisibility precedence between target and matcher frames.
/// </summary>
[OverloadGenerationOptions(OverloadVisibility = OverloadVisibility.Internal)]
[OverloadMatcher]
internal interface Class_40_Matcher
{
[GenerateOverloads(nameof(param_a))]
Expand Down
38 changes: 38 additions & 0 deletions ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_46.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Diagnostics.CodeAnalysis;

namespace Tenekon.MethodOverloads.AcceptanceCriterias;

/// <summary>
/// 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.
/// </summary>
public interface Class_46_Service
{
[GenerateOverloads(nameof(param_b))]
void Case_1(int param_a, string? param_b);
}

/// <summary>
/// An unmarked type in Matchers is ignored and produces MOG020 at every typeof reference: here once on the type and
/// once on Case_2.
/// </summary>
[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) { }
}

/// <summary>
/// Overloads are expected for Class_46_Service only; Class_46 gets none.
/// </summary>
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);
}
}
21 changes: 21 additions & 0 deletions ref/Tenekon.MethodOverloads.AcceptanceCriterias/Class_47.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Diagnostics.CodeAnalysis;

namespace Tenekon.MethodOverloads.AcceptanceCriterias;

/// <summary>
/// A matcher type is never a target: GenerateMethodOverloads on a type marked with [OverloadMatcher] produces MOG021
/// and is ignored.
/// </summary>
[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.
}
Loading
Loading