Skip to content

One signature identity for every hand-written-member check, and [GeneratedMember] on every generated member - #279

Merged
CaffeinatedCoder merged 4 commits into
release/2.4.0from
refactor/generated-member-identity
Oct 31, 2026
Merged

CaffeinatedCoder merged 4 commits into
release/2.4.0from
refactor/generated-member-identity

Conversation

@CaffeinatedCoder

@CaffeinatedCoder CaffeinatedCoder commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Closes #239

Problem

Seven 2.3.1 bugs (#191, #195, #196, #197, #205, #226, #230) were one question answered in several places with different rules: is this member the user wrote the one a generator introduces? MethodsWithSignature compared parameter types with Equals. ClosedSetTypes.SameParameters and the base-record seam walk compared them after ToNonNullable(), which also turns int? into int. MembersNamed took every member of a name on a value object's bases, while the closed set's base walk took only fields, properties and events. The generated members' names were two lists kept by hand (ValueObjectDeclaration.GeneratedMemberNames, ClosedSetTypes.GeneratedNames), so a new generated member left off them was kept silently. Nothing marked a generated member as generated.

Change

The design is decision 128. The identity and the attribute turned out to be separable, so they land as two commits, identity first.

  • docs: decision 128, its index row (0028 and 0121 marked as refined by it), design/aspects.md, and the AGENTS.md line about new generated members.
  • fix: one identity (MemberIdentity), C#'s rules: a method of the generated method's name, arity and parameter types (nullable annotations ignored, Nullable<T> a type of its own, by reference against by value), or another kind of member of its name. A base's method beside a generated property doesn't count, because the property hides it. Only members visible from the value object count. GeneratedMembers has one entry per named member the aspects introduce. Each aspect introduces the member through its entry, which names it, checks the template's signature (a drift fails every build, LAMA0041) and marks it. The checks, the CMTK1005/CMTK1021 names and the refused interfaces all read the same entries, under the aspects' own predicates. Both name lists are gone.
  • feat: [GeneratedMember(typeof(Contract), "Member")] in CodoMetis.TypeKit.CompilerServices (base package, Metalama-free), on every generated member, the explicit implementations, operators, constructors and companion methods included. IsHandWritten reads it. A hand-written one is CMTK1011. Exception (measured): Metalama 2026.1 applies no attribute to an override it writes over a record's implicit ToString() (and Equals/GetHashCode() over a base record), and refuses IntroduceAttribute there. Those members carry neither attribute, just as they never carried the [CompilerGenerated] the aspects asked for.

Build outcomes that change (each measured against af3f95e in a throwaway consumer, and held by a new BuildOutcomeTests row):

Declaration Before After
a property or field named Parse, TryFormat, TryFrom or CompareTo, own or a base's LAMA0506 CMTK1011 naming it
a closed set's own Name() method LAMA0611 (CS0102) CMTK1011
a set base's nested type named as the generated converter LAMA0531 CMTK1011
a base record's TryFrom(int?) over an int; a set's Parse(string, bool?) CMTK1011 builds (C# overloads them; the value object's own TryFrom(int?) already built)
a base record's MinValue(), Value(), _value(); an entry point's overload __FromJson(string) CMTK1011 builds (hidden or overloaded; checked with warnings as errors, and each answers through the generated member)

No build that built fails now. The newly building forms are refusals of code C# compiles beside the generated member, the #230 family. Loosening an Error in a minor is allowed by decision 34. Every regression row of #191–#197, #205, #226 and #230 passes unchanged.

Tests

  • Generators: 1647 passed (1449 before; new: 14 build-outcome rows, 183 GeneratedMemberAttributeTests rows, one more fixture row). After the fix: commit alone, 1463 passed.
  • GeneratedSurface.verified.txt gains the contract member on each generated member's line and is otherwise byte-identical, checked by stripping the markers and diffing against af3f95e.
  • Whole solution through tools/BuildVerdict: everything passes except (a) the SQL Server container fixtures, the arm64-Mac environment issue (TYPEKIT_MSSQL_IMAGE), untouched by this change, and (b) DesignRecordTests.The_records_are_numbered_without_a_gap, which wants record 0127. That number is reserved for another 2.4.0 session, so this PR needs 0127 merged first, or a re-sync after it lands.
  • Seeded defects (verify-the-guard), each failing the generator tests: Nullable<T> merged into T (the two new "builds" rows fail); no cross-kind clash (31 rows); visibility ignored, ref kind ignored, arity ignored, a seam found by name only (the 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/A private member of a base record with a generated member's name, such as MaxValue, Parse, TryFrom or CompareTo(object), now fails the build (CMTK1011, CMTK1008), where 2.3.0 generated the value object #230 declarations fail with CMTK1011); a base's method clashing with a generated property (5 rows); a catalogue signature drifting from its template (LAMA0041 on every probe); no mark on catalogue members (182 rows); a hand-written mark not refused (its row).
  • Weaving review: the fabric's selection, the entry points, Create's routing and the materializer are unchanged. The new code finds members by symbol and by TypeKit's own member names only. The base package gains a plain attribute. Not run: the consumer smoke test (Native AOT); the attribute's typeof arguments only reference contract types the members already implement.

The analyzers' own name constants (TryFrom, Revalidate, FromKnownGood) are left alone. Before weaving, in the declaring project, no member carries the attribute yet, so they can't read it there.

🤖 Generated with Claude Code

CaffeinatedCoder and others added 3 commits October 31, 2026 21:10
…ted member

Every check of a hand-written member asks C#'s identity (name, arity,
parameter types without nullable annotations, by reference or by value,
another kind of member of its name), the named generated members are entries
the aspects introduce through and the checks read, and every generated member
carries [GeneratedMember]. Measured on af3f95e; refines decisions 28 and 121.

Refs #239

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… from what the aspects introduce

MethodsWithSignature, ClosedSetTypes.SameParameters, MembersNamed and the
base-type walks each decided arity, ref kind, nullability and kind by rules of
their own, and the generated members' names were two lists kept by hand.

- MemberIdentity is C#'s: a method of the generated one's name, arity and
  parameter types without nullable annotations (Nullable<T> its own type), by
  reference or by value; or another kind of member of its name, in the type or
  a base, except a base's method beside a generated property, which it hides.
  Visible from the value object only (#230).
- GeneratedMembers holds one entry per named member the aspects introduce. Each
  aspect introduces through its entry, which names the member and checks that
  its template gives it that signature (a drift fails every build, LAMA0041);
  the checks, the CMTK1005/CMTK1021 names and the refused interfaces read the
  same entries under the aspects' own predicates.

Input class: a hand-written member beside a generated one (GTS), over the
shapes kind x site (own, base, base of another assembly) x signature (arity,
ref kind, Nullable<T>, annotation) x visibility, measured against af3f95e in a
throwaway consumer. Siblings the unified identity found:
- built now, were CMTK1011: a base record's TryFrom(int?) over an int, a closed
  set's Parse(string, bool?), a base record's MinValue(), Value() or _value(),
  an entry point's overload (each builds with warnings as errors and answers
  through the generated member);
- CMTK1011 now, were LAMA0506/LAMA0531/LAMA0611: a property or field named
  Parse, TryFormat, TryFrom or CompareTo on the value object or a base, a set's
  Name() method, a set base's nested type of the converter's name.
Every regression row of #191 to #197, #205, #226 and #230 passes unchanged.
Seeded: Nullable<T> merged into T, no cross-kind clash, visibility, ref kind
and arity ignored, base methods clashing, a catalogue signature drifting, a
seam found by name; each fails the generator tests.

Refs #239

Defect-Class: GTS
Found-By: VA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…contract member it implements

GeneratedMemberAttribute(Type contract, string member), in the Metalama-free
base package's CompilerServices: the interface the member implements
(IParsable<OrderId>.Parse), or the marker that promises it
(IValidatedValue<Sku, string, SkuFault>.TryFrom). Not [GeneratedCode], which
analyzers skip. Every GeneratedMembers entry adds it beside
[CompilerGenerated]; explicit implementations, operators, constructors and
the companion's methods get it where they are introduced.
ValueObjectDeclaration.IsHandWritten reads it, and a hand-written one on a
value object's or a closed set's member is CMTK1011.

Metalama 2026.1 writes the override of a record's implicit ToString(), and of
Equals and GetHashCode() over a base record, over that declaration and applies
no attribute to it, nor introduces one there (measured), so those carry
neither attribute, as they carried no [CompilerGenerated] before.

GeneratedSurface.verified.txt names each member's contract member and is
otherwise unchanged; GeneratedMemberAttributeTests holds that every member
with [CompilerGenerated] carries the mark and none without it does, in this
assembly and the probes'. Seeded: no mark on catalogue members (182 rows
fail), a hand-written mark not refused (its row fails). A surface waiver
covers the attribute, which no document promises yet.

Refs #239

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CaffeinatedCoder CaffeinatedCoder added this to the 2.4.0 milestone Oct 31, 2026
@CaffeinatedCoder CaffeinatedCoder added chore Build, CI, tooling, packaging, refactoring with no behaviour change package/generators Affects CodoMetis.TypeKit.Generators area/value-objects Value-object contracts and what the aspects generate for them labels Oct 31, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CaffeinatedCoder
CaffeinatedCoder merged commit 020e48c into release/2.4.0 Oct 31, 2026
10 checks passed
@CaffeinatedCoder
CaffeinatedCoder deleted the refactor/generated-member-identity branch October 31, 2026 20:35
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 chore Build, CI, tooling, packaging, refactoring with no behaviour change package/generators Affects CodoMetis.TypeKit.Generators

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant