Skip to content

fix: refuse a generated member written by hand wherever a value object reaches it - #193

Closed
CaffeinatedCoder wants to merge 3 commits into
release/2.4.0from
fix/interface-explicit-implementation-refused
Closed

CaffeinatedCoder wants to merge 3 commits into
release/2.4.0from
fix/interface-explicit-implementation-refused

Conversation

@CaffeinatedCoder

@CaffeinatedCoder CaffeinatedCoder commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Closes #191, closes #195, closes #197

Problem

The generators refuse a member they introduce when it's written by hand, and an explicit implementation of an interface they implement (CMTK1011, CMTK1008 for comparison). The checks read the value object's own members, and for explicit implementations its base records' too. Everywhere else the value object reaches such a member, the generated one replaced it, or it kept answering beside the generated one, without a word:

  • An interface's explicit implementation of a generated interface member is replaced without a diagnostic #191, an interface's explicit implementation. Every type that declares the interface reaches that default until it declares a public member. Max<Bounded>() read the generated MaxValue where IBounded<TSelf>'s threw, and ((IFormattable)Formats.From(1)).ToString(null, null) printed 1 where IFormats answered its own text. Measured for all eleven interfaces of the input class.
  • Another assembly's types, an interface's and a base record's, which the rule has missed since phase 11. Metalama lists no private member of a type in another assembly, and an explicit implementation is private. A closed set over a referenced base class that implements IClosedSet<TSelf> explicitly listed two members in All and none through T.All, and EF Core stored the base's name and read no member's name back.
  • A base record's public implementation of a generated member is kept as the value object's, or crashes the aspect #197, a base record's public member. A base's static MinValue/MaxValue was the value object's, and none was generated. Its public Value, From, Parse, TryParse, ToString(string?, IFormatProvider?), TryFormat or CompareTo(object) crashed the aspect (LAMA0500). From another assembly a public static one was refused but called "the explicit implementation of …".
  • A base record's explicit Create from another project leaves two rule sets #195, a base record's Create. Where the value object reaches the marker only through a base record of another project that implements Create explicitly, TryFrom("x") accepted (its own Create) and generic T.Create("x") refused (the base's).
  • Declared through an interface, IConvertible crashed the aspect (LAMA0041), and a closed set's IParsable<TSelf> too (LAMA0512). A closed set's interface that implements IClosedSet<TSelf>.Name or TryParse explicitly was replaced without a word.

Change

  • ValueObjectDeclaration.ImplementationsOf (was ExplicitImplementationsOf) asks the interface mapping (INamedType.TryFindImplementationForInterfaceMember) where each member of a generated interface lands. Any implementation other than the value object's own public member is refused, explicit or public, on the value object, a base record or an interface, in any assembly. The error names it, "in its base type X" or "in its interface X", and names a public one by its type and member.
    • An interface's == maps to the record's own, which replaces it in every record, generated or not (measured with a plain record), so it stays allowed with no special case.
    • An interface's explicit implementation of a member of an interface of its own library is not refused. That is .NET's INumberBase<TSelf>, whose UTF-8 bridges are the only such members in .NET 10 (scanned), and which every .NET number replaces. Refusing them would leave no value object able to implement INumberBase<TSelf>. The test compares assembly symbols, never names.
  • The names and signatures checked on the value object are checked on each base record, for a base member that implements no interface (a MaxValue alone). MembersNamed leaves an explicit-looking member to the mapping, so a member of another assembly is named once.
  • CreateDeclarations asks the mapping where T.Create lands. An explicit one in a base or an interface is CMTK1011, and so is a base's public one beside the value object's own. The error says to declare the marker on the value object itself, which re-implements it. A base's public Create alone is one rule set and builds.
  • IConvertible declared through an interface, on the value object or a base record, and a closed set's IParsable<TSelf> through one, are CMTK1011 naming the interface. ClosedSetTypes uses the same lookup for IClosedSet<TSelf>.
  • Checked, unchanged: ValueObjectContractAspect.ExplicitlyImplementsEquatableBetween. A record base's synthesized Equals(TBase) implements IEquatable<TBase> before any interface's default.
  • Decision 124 records it, with the exception to decision 34 under decision 119. A build that worked on 2.3.0 can fail on 2.4.0, and each failure names code that never runs or answers beside the generated member. The changelog's Changed section says so. The Generators README ("Write your own members", "Comparison", the Build errors table) and design/aspects.md follow, with four claims in test/oracles/claims.json.

The comparison interfaces answer CMTK1008, as they do for an explicit implementation on the value object itself.

Merge after #188, #189 and #190. Decision numbers 0121 to 0123 are theirs, so DesignRecordTests.The_records_are_numbered_without_a_gap fails here until they land. If this one goes first, the record is renumbered to 0121.

Commits: 466c541 (#191, Found-By: VA), 38b316f (#197 and the closed-set base test, Found-By: VA), 1e09f8d (#195, Found-By: OR).

Tests

BuildOutcomeTests, beside the existing CMTK1011 cases, 279 in all. The fixture now builds a library that references the base package alone, beside its consumers: a base type there that carries a marker or IClosedSet<TSelf> is no value object or set of its own, as in a consumer's shared project.

  • An interface's explicit implementation, 22 rows: one value object per interface of the input class, generic interfaces over TSelf, two interfaces down, through a base record's interface, a record class over string, and from another assembly (CodoMetis.TypeKit.Generators.Probes).
  • A base record's member: other-assembly explicit ones (IFormattable, IMinMaxValue), and public ones of each kind. That covers MinValue/MaxValue through IMinMaxValue and by name alone, ToString(string?, IFormatProvider?), CompareTo(object), Parse/TryParse, Value, From and TryFormat, plus a public static from the library, named once as public.
  • Create: an explicit one in the library's base with and without the value object's own, and a public one beside it. Building: the marker declared again over the explicit base, and a public base Create alone.
  • Closed sets: IClosedSet<TSelf> through an interface of either assembly, over the library's base class, and IParsable<TSelf> through an interface.
  • What builds: a ToString() seam, a Uri's missing ordering, an interface's ==, and a full INumberBase<TSelf> value object. These pass on the old code too, so they pin behaviour. MetersAsNumber is also the guard of the INumberBase exclusion.

Reverted (verify-the-guard). Each seed was run against BuildOutcomeTests:

Seed Fails
The source back to release/2.4.0 (263 tests, before the second round) 30: the 22 interface rows, 2 other-assembly base records, 4 set rows, plus Every_error_is_one_of_the_intended_ones and Nested_value_objects_... (LAMA0041)
Skip implementations declared in an interface 22: the 19 interface rows that aren't IConvertible, and 3 set interface rows
Skip implementations from another assembly 6: the library interfaces, base records and set
Drop the INumberBase exclusion 3: MetersAsNumber, and the CS0535 guards
Drop IConvertible through an interface / through a base's interface 4 / 3: those rows, plus the LAMA0041 guards
Drop a set's IParsable through an interface 3: that row, plus the LAMA0512 guards
The source back to 466c541 (279 tests) 15: the 9 public base rows, the named-once fact, the 3 Create rows, plus the LAMA guards
Skip a base's public implementation in the mapping 5: InheritsPublicObjectOrder, InheritsLibraryPublicMinMax, the named-once fact, plus the LAMA guards
Check names on the value object alone 6: Value, From, MinValue/MaxValue by name, TryFormat, plus the LAMA guards
Name a public member as Metalama displays it 3: the library rows and the named-once fact
Let the name check report explicit-looking members 1: the named-once fact
Skip Create in the mapping 5: the three Create rows, plus the LAMA guards
Refuse a base's public Create alone 1: OnlyPublicBase

Variants. Input class GTS: a generated member or an explicit implementation of one, by where it sits (the value object, a base record, an interface, two interfaces down, a base record's interface, another assembly) and by which member or interface, over a record struct and a record class, plain and validated, over int, long, double, string and Uri.

Generation review (weaving-review). For each way the refusal could hit correct code there is a control above. A hand-written CompareTo(TSelf) on the value object is its own public member, which the mapping leaves to the existing checks. No name strings, no run-time change, no Native AOT surface.

Not in this PR:

Both #194 and #196 get PRs of their own.

🤖 Generated with Claude Code

… a value object reaches it

CMTK1011, and CMTK1008 for comparison, listed the members of the value
object and its base records. An interface's explicit implementation of
a member the generators implement is the default every implementing
type reaches until it declares a public member, so the generated one
replaced it without a word: Max<Bounded>() read the generated MaxValue
where IBounded<TSelf>'s threw, and an IFormattable call printed 1 where
the interface answered its own text. Metalama lists no private member
of a type in another assembly, so a base record from a referenced
project was missed too, replaced (IFormattable) or kept answering
beside the generated member (IMinMaxValue).

ExplicitImplementationsOf now asks the interface mapping
(TryFindImplementationForInterfaceMember) for each member of a
generated interface the type implements, and refuses an explicit
implementation, naming where it is: "in its base type X" or "in its
interface X". An interface's == maps to the record's own, so it stays
allowed. INumberBase<TSelf>'s UTF-8 bridges, the only such members in
.NET's interfaces, are not refused, compared by assembly symbol.
IConvertible declared through an interface, and a closed set's
IParsable<TSelf> through one, are CMTK1011 where the aspect crashed
(LAMA0041, LAMA0512). Decision 124 records it, an exception to
decision 34 under decision 119.

Input class (GTS): an explicit implementation of a generated
interface's member, by where it sits. Catalogue in BuildOutcomeTests:
the eleven interfaces of #191 on a record struct, plain and validated;
two interfaces down; through a base record's interface; on a record
class over string; from another assembly (interface and base record,
in Generators.Probes); a closed set through an interface of either
assembly. Four controls build: a ToString() seam, a Uri's missing
ordering, an interface's ==, an INumberBase<TSelf> value object.
Siblings found and fixed here: another assembly's base record (missed
since phase 11), IConvertible through an interface (LAMA0041), a
closed set's IClosedSet<TSelf> and IParsable<TSelf> through an
interface. ExplicitlyImplementsEquatableBetween has no gap: a record
base's synthesized Equals(TBase) wins over any interface default.
Left for its own issue: IValueObject<,> through a user interface
still fails with LAMA0512.

Reverting the three source files fails 30 of 263 BuildOutcomeTests;
each guard's own seed fails only its rows.

Refs #191

Defect-Class: GTS
Found-By: VA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CaffeinatedCoder and others added 2 commits October 30, 2026 17:07
… one as such

A base record's public member the generators introduce counted for
nothing. Its static MinValue and MaxValue, with IMinMaxValue<TSelf> or
without, were the value object's, and no MinValue or MaxValue was
generated; its public Value, From, Parse, TryParse, ToString(string?,
IFormatProvider?), TryFormat or CompareTo(object) failed the aspect
(LAMA0500), naming nothing the user wrote. From another project, a
public static member that implements a static abstract one reads as an
explicit implementation, since the compiler records every such
implementation as one, so the error called it "the explicit
implementation of ...".

ImplementationsOf (was ExplicitImplementationsOf) now refuses any
implementation the interface mapping finds outside the value object's
own public members, public or explicit, and names a public one by its
type and member. The names and signatures checked on the value object
are checked on each base record too, for a member that implements no
interface. MembersNamed leaves an explicit-looking member to the
mapping, so a member of another assembly is named once.

The build-outcome fixture builds a library that references the base
package alone, beside its consumers, since a base type there that
carries a marker or IClosedSet<TSelf> is no value object or set of its
own. A closed set over its base class that implements IClosedSet<TSelf>
explicitly, an outside review's report, is refused since 466c541; the
test holds it now.

Input class (GTS): a generated member declared in a base record, by
member and by where the base is. Catalogue: MinValue/MaxValue through
IMinMaxValue and by name alone, ToString(string?, IFormatProvider?),
CompareTo(object), Parse/TryParse, Value, From and TryFormat, in this
project, and a public static from the library. A base's TryFrom, a
seam, stays with #194.

Reverting this commit's source fails 12 of 274 BuildOutcomeTests; each
guard's own seed fails only its rows (the mapping's public case, the
names on base records, the public naming, the explicit-looking names).

Refs #197, #191

Defect-Class: GTS
Found-By: VA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…de the value object's

Where a validated value object reaches the marker only through a base
record of another project that implements Create explicitly, every
generated way in called the value object's own Create, which accepted
"x", and generic code calling T.Create reached the base's, which
refused it. CreateDeclarations read the value object's own methods,
and Metalama lists no private member of another assembly's type.

CreateDeclarations asks the interface mapping where T.Create lands. An
explicit implementation in a base record or an interface is CMTK1011,
and so is a base's public Create beside the value object's own; a
base's public Create alone is the one rule set the generated code calls
too, and builds. The error says to declare the marker on the value
object itself, which re-implements it, since the base cannot be
changed from there.

Input class (GTS): where the Create that T.Create reaches is declared.
Catalogue: an explicit one in the library's base with and without the
value object's own, a public one beside it, and, building, the marker
declared again over the explicit base and a public base Create alone.
In the same project such a base carries the marker and is CMTK1022.

Reverting the CreateDeclarations mapping fails the three refusal rows;
dropping the public-alone condition fails OnlyPublicBase.

Refs #195

Defect-Class: GTS
Found-By: OR
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CaffeinatedCoder CaffeinatedCoder changed the title fix: refuse an explicit implementation of a generated member wherever a value object reaches it fix: refuse a generated member written by hand wherever a value object reaches it Oct 30, 2026
@CaffeinatedCoder

Copy link
Copy Markdown
Owner Author

Replaced by #201: #191, #195 and #197 moved to the 2.3.1 patch (epic #198), so the same three commits now land on release/2.3.1, stacked on #200, with the decision record renumbered to 0121. #196's fix is #202.

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

Labels

area/value-objects Value-object contracts and what the aspects generate for them bug Something isn't working package/generators Affects CodoMetis.TypeKit.Generators severity/medium Inconsistency or failure in a narrow scenario silent Compiles or answers and does less than it appears to

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant