Repository navigation
fix: refuse a generated member written by hand wherever a value object reaches it - #201
Merged
CaffeinatedCoder merged 4 commits intoOct 30, 2026
Conversation
… 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 121 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. Cherry-picked from 466c541 (#193, on release/2.4.0) for 2.3.1, since the defect shipped in 2.3.0: decision 121 (124 on release/2.4.0, renumbered here so 2.3.1's records run without a gap) records the exception to decision 34 as taken in the patch, and the changelog line sits in 2.3.1's section. Refs #191 Defect-Class: GTS Found-By: VA Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 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 4a3d9a6; 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>
This was referenced Oct 30, 2026
…face-explicit-implementation-refused-2-3-1
CaffeinatedCoder
deleted the
fix/interface-explicit-implementation-refused-2-3-1
branch
October 30, 2026 17:31
This was referenced Oct 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
Max<Bounded>()read the generatedMaxValuewhereIBounded<TSelf>'s threw, and((IFormattable)Formats.From(1)).ToString(null, null)printed1whereIFormatsanswered its own text. Measured for all eleven interfaces of the input class.IClosedSet<TSelf>explicitly listed two members inAlland none throughT.All, and EF Core stored the base's name and read no member's name back.MinValue/MaxValuewas the value object's, and none was generated. Its publicValue,From,Parse,TryParse,ToString(string?, IFormatProvider?),TryFormatorCompareTo(object)crashed the aspect (LAMA0500). From another assembly a public static one was refused but called "the explicit implementation of …".Create. Where the value object reaches the marker only through a base record of another project that implementsCreateexplicitly,TryFrom("x")accepted (its ownCreate) and genericT.Create("x")refused (the base's).IConvertiblecrashed the aspect (LAMA0041), and a closed set'sIParsable<TSelf>too (LAMA0512). A closed set's interface that implementsIClosedSet<TSelf>.NameorTryParseexplicitly was replaced without a word.Change
ValueObjectDeclaration.ImplementationsOf(wasExplicitImplementationsOf) 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 typeX" or "in its interfaceX", and names a public one by its type and member.==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.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 implementINumberBase<TSelf>. The test compares assembly symbols, never names.MaxValuealone).MembersNamedleaves an explicit-looking member to the mapping, so a member of another assembly is named once.CreateDeclarationsasks the mapping whereT.Createlands. 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 publicCreatealone is one rule set and builds.IConvertibledeclared through an interface, on the value object or a base record, and a closed set'sIParsable<TSelf>through one, are CMTK1011 naming the interface.ClosedSetTypesuses the same lookup forIClosedSet<TSelf>.ValueObjectContractAspect.ExplicitlyImplementsEquatableBetween. A record base's synthesizedEquals(TBase)implementsIEquatable<TBase>before any interface's default.design/aspects.mdfollow, with four claims intest/oracles/claims.json.The comparison interfaces answer CMTK1008, as they do for an explicit implementation on the value object itself.
Stacked on #200 (
build/open-2-3-1, which opens 2.3.1 fromv2.3.0): merge it first. This replaces #193, which targetedrelease/2.4.0; the fixes moved to the 2.3.1 patch with their issues (epic #198).Commits:
4a3d9a6(#191,Found-By: VA, cherry-picked from #193's466c541),e606d64(#197 and the closed-set base test,Found-By: VA),03e199b(#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 orIClosedSet<TSelf>is no value object or set of its own, as in a consumer's shared project.TSelf, two interfaces down, through a base record's interface, a record class overstring, and from another assembly (CodoMetis.TypeKit.Generators.Probes).IFormattable,IMinMaxValue), and public ones of each kind. That coversMinValue/MaxValuethroughIMinMaxValueand by name alone,ToString(string?, IFormatProvider?),CompareTo(object),Parse/TryParse,Value,FromandTryFormat, 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 baseCreatealone.IClosedSet<TSelf>through an interface of either assembly, over the library's base class, andIParsable<TSelf>through an interface.ToString()seam, aUri's missing ordering, an interface's==, and a fullINumberBase<TSelf>value object. These pass on the old code too, so they pin behaviour.MetersAsNumberis also the guard of theINumberBaseexclusion.Reverted (
verify-the-guard). Each seed was run againstBuildOutcomeTests:release/2.4.0(263 tests, before the second round)Every_error_is_one_of_the_intended_onesandNested_value_objects_...(LAMA0041)IConvertible, and 3 set interface rowsINumberBaseexclusionMetersAsNumber, and the CS0535 guardsIConvertiblethrough an interface / through a base's interfaceIParsablethrough an interface466c541(279 tests)Createrows, plus the LAMA guardsInheritsPublicObjectOrder,InheritsLibraryPublicMinMax, the named-once fact, plus the LAMA guardsValue,From,MinValue/MaxValueby name,TryFormat, plus the LAMA guardsCreatein the mappingCreaterows, plus the LAMA guardsCreatealoneOnlyPublicBaseVariants. 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,stringandUri.Generation review (
weaving-review). For each way the refusal could hit correct code there is a control above. A hand-writtenCompareTo(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:
IValueObject<,>declared through your own interface still fails the aspect with LAMA0512 (A value object implementing an interface derived from IValueObject<,> fails with a Metalama error naming nothing #192).CompareTo(TSelf)andTryFrom, is An inherited CompareTo(TSelf) is not seen: EF Core orders by the column, and < overflows the stack #194, which also overflows the stack on<.Equals(TBase)is A hand-written Equals(TBase) on a value object is kept, and a collection of the base compares by it #196.Both #194 and #196 get PRs of their own.
🤖 Generated with Claude Code