Skip to content

A generic overload such as Parse<TFormat>(string, IFormatProvider) is mistaken for the generated Parse, and the value object or closed set is refused #226

Closed

Description

@CaffeinatedCoder

Reported by an external review of release/2.3.1 @ dbdd44e, which reproduced it for value objects, inherited methods and closed sets; the cause confirmed here by reading the source, not yet run.

Package

CodoMetis.TypeKit.Generators

Version

2.3.0 (the same code in v2.3.0), and release/2.3.1 @ dbdd44e

How does it fail?

Loudly. A valid generic overload is refused as a hand-written generated member

Problem

The generators find a hand-written copy of a generated method by name and parameter types, ignoring the method's type parameters:

Parse<TFormat>(string, IFormatProvider) is a different method from the generated Parse(string, IFormatProvider): C# overloads by arity, and Metalama introduces the generated one beside it. It is reported as declared by hand and the value object is refused.

Scenario

public readonly partial record struct OrderNumber : IValue<int>
{
    public static OrderNumber Parse<TFormat>(string text, IFormatProvider? provider) where TFormat : IFormat => ...;
}
// refused: Parse(string, IFormatProvider) is declared by hand
// expected: generated, beside the generic overload

The same for the method inherited from a base record, and for a closed set's Parse<TFormat>(string).

Change

Match only a method without type parameters (TypeParameters.Count: 0) in MethodsWithSignature and in the closed set's Parse/TryParse checks, as ValueObjectParsableAspect already does for the wrapped type's Parse (ValueObjectParsableAspect.cs:252).

Test

A generic overload of each generated method the two checks name (From, Parse, TryParse in its three text forms, ToString(string, IFormatProvider), TryFormat; a closed set's Parse and TryParse), declared on the type and on a base record, compiles and keeps the generated member; reverted per verify-the-guard. Input class: a method that shares a generated member's name and parameters, by what differs (arity, a ref kind, nullability) and where it is declared (the type, a base, an interface), across the shape.* axes.

Environment

.NET SDK 10.0.4xx, Metalama 2026.1; any consumer.

Ref kind too, measured at d573eef

A scratch consumer built against the generators: each of these legal overloads is refused with CMTK1011, since MethodsWithSignature and SameParameters compare parameter types and not their ref kind:

public static bool TryParse(string? text, IFormatProvider? provider, ByValueTryParse fallback);  // beside the generated ..., out T result
public static InFrom From(in int value);                                                          // beside the generated From(int)
public static bool TryParse(string text, ByValueSet fallback);                                    // a closed set, beside TryParse(string, out T)

Correctly refused, the controls: Parse(string?, IFormatProvider?) and Parse(string, IFormatProvider) (nullability is no difference to C#), ToString(string, IFormatProvider), and TryParse(..., ref T result) (ref and out cannot overload). So the change compares ref kinds too, but only out/ref/in against by-value: ref against out stays a clash.

Measured in the fix, and the siblings (#236)

Measured 2026-10-31 in a scratch consumer: with the change, Metalama introduces the generated member beside each overload above, with no LAMA error, and each plain call reaches the generated one. ref or in against out, and another nullability, stay CMTK1011.

The input class's catalogue, run over the other checks that find a hand-written member by name and parameters, found the same cause in seven of them (Found-By: VA, fixed in the same PR). Three were silent:

  • A generic ToString<TFormat>() was taken for the ToString() seam: the value object printed ToStringOverloadedByArity { Value = a }, as a record does, and a closed set printed its type name.
  • A generic or by-in CompareTo(TSelf) was taken for the ordering seam: [HandWrittenOrdering] was attached, so EF Core refused to order by it, though the ordering was generated.
  • A generic Equals<T>(TBase?) stopped the value object re-implementing a deeper base's explicit IEquatable<TBase>: a HashSet<TBase> held two value objects holding 42.

The rest were refused (CMTK1011, CMTK1008, CMTK1023): a generic or by-reference CompareTo(object), a base record's generic CompareTo, TryFrom<T>(int), TryFrom(in int) and Revalidate<T>(), a generic Equals(TSelf), GetHashCode() or PrintMembers.

Activity

  1. added this to the 2.3.1 milestone on Oct 31, 2026
  2. added
    bugSomething isn't working
    severity/mediumInconsistency or failure in a narrow scenario
    package/generatorsAffects CodoMetis.TypeKit.Generators
    area/value-objectsValue-object contracts and what the aspects generate for them
    area/parsingParse, TryParse, formatting and the type converter
    on Oct 31, 2026
  3. CaffeinatedCoder commented on Oct 31, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Fixed by #236, merged into release/2.3.1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/parsingParse, TryParse, formatting and the type converterarea/value-objectsValue-object contracts and what the aspects generate for thembugSomething isn't workingpackage/generatorsAffects CodoMetis.TypeKit.Generatorsseverity/mediumInconsistency or failure in a narrow scenariosilentCompiles or answers and does less than it appears to

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions