Repository navigation
Description
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:
ValueObjectDeclaration.MethodsWithSignature(src/CodoMetis.TypeKit.Generators/ValueObjectDeclaration.cs:681-687) matchesParse(string, IFormatProvider),TryParse,From,ToString(string, IFormatProvider)andTryFormat, on the value object and, throughDeclared, on its base types (A base record's public implementation of a generated member is kept as the value object's, or crashes the aspect #197).ClosedSetTypes.GeneratedMembersDeclaredByHand(ClosedSetTypes.cs:208-209,SameParametersat:224-226) matchesParseandTryParse.
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 overloadThe 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 theToString()seam: the value object printedToStringOverloadedByArity { Value = a }, as a record does, and a closed set printed its type name. - A generic or by-
inCompareTo(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 explicitIEquatable<TBase>: aHashSet<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
CaffeinatedCoder commentedon Oct 31, 2026
Fixed by #236, merged into
release/2.3.1.