Skip to content

A closed set over a base class that declares a generated member fails with a Metalama error, or its ToString() is replaced #205

Closed

Description

@CaffeinatedCoder

Found by a sweep of release/2.3.1 @ dbdd44e (lens: sibling-parity, generators) for the defect class of #191-#197; reproduced in a scratch build-outcome test.

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 but naming nothing you wrote (LAMA0500, LAMA0502, LAMA0512), and silently for an unsealed ToString()

Problem

ClosedSetTypes.GeneratedMembersDeclaredByHand (src/CodoMetis.TypeKit.Generators/ClosedSetTypes.cs:203-216) reads Parse, TryParse and Match from set.Methods, and IParsable<TSelf> from set.ImplementedInterfaces; ClosedSetTypes.DeclaresToString (:237-238) reads set.Methods. None reads a base class. The value-object side does since #197 and #194 (ValueObjectDeclaration.SelfAndBases, InheritedToString, UnsealedInheritedToString), and a closed set's base Name is already refused through ImplementationsOf.

Scenario

Base classes in the same project as the set:

public abstract class SealedToStringSetBase { public sealed override string ToString() => "base"; }
public sealed partial class OverSealedToString : SealedToStringSetBase, IClosedSet<OverSealedToString> { public static readonly OverSealedToString A = new(); }

public abstract class ParsingSetBase<TSelf> { public static TSelf Parse(string text) => throw new NotSupportedException(); }
public sealed partial class OverParsingBase : ParsingSetBase<OverParsingBase>, IClosedSet<OverParsingBase> { public static readonly OverParsingBase A = new(); }

public abstract class ParsableSetBase<TSelf> : IParsable<TSelf> where TSelf : ParsableSetBase<TSelf>
{
    static TSelf IParsable<TSelf>.Parse(string s, IFormatProvider? p) => throw new NotSupportedException();
    static bool IParsable<TSelf>.TryParse(string? s, IFormatProvider? p, out TSelf r) { r = null!; return false; }
}
public sealed partial class OverParsableBase : ParsableSetBase<OverParsableBase>, IClosedSet<OverParsableBase> { public static readonly OverParsableBase A = new(); }

public abstract class OpenToStringSetBase { public override string ToString() => "***"; }
public sealed partial class OverOpenToString : OpenToStringSetBase, IClosedSet<OverOpenToString> { public static readonly OverOpenToString A = new(); }

Measured in BuildOutcomeTests @ dbdd44e:

  • OverSealedToString: LAMA0502 "cannot introduce member 'OverSealedToString.ToString()' ... already defined in type 'SealedToStringSetBase' and is static, non-virtual or sealed", plus 4 CS0535.
  • OverParsingBase: LAMA0500 "cannot introduce member 'OverParsingBase.Parse(string?)' ... already defined in type 'ParsingSetBase'", plus 4 CS0535.
  • OverParsableBase: LAMA0512 "cannot implement interface 'IParsable' ... already implements it and WhenExists is set to Fail", plus 4 CS0535.
  • OverOpenToString: builds with no diagnostic; the generated ToString() overrides the base's. The same declaration over a value object is refused (UnsealedInheritedToString).

No CMTK error names any of them. A base from another assembly takes the same path (set.Methods lists no base member at all): traced, not run.

Change

Walk ValueObjectDeclaration.SelfAndBases(set) for the Parse/TryParse/Match signatures, with "in its base type X" wording; find a base's IParsable<TSelf> through the base types' AllImplementedInterfaces (or pass it to ImplementationsOf); treat a base's sealed ToString() as the seam, as ValueObjectDeclaration.DeclaresToString does. For a base's unsealed ToString(), refuse it as value objects do, or take it as the seam: the fix decides, recorded beside decision 121.

Test

The four rows above in BuildOutcomeTests (each a CMTK1011 naming the set, no LAMA or CS error naming it), plus the same shapes with the base in the build-outcome Library project; reverted per verify-the-guard. Input class (GTS): a generated closed-set member, by where it is declared.

Environment

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

Fixed in #218 (9f749a4)

Measured beyond the report: a base class's Parse(string, bool), TryParse(string?, bool, out TSelf), Match, a static All (LAMA0503) and a protected _name failed the same way, and all four shapes failed from the library base too (run, not only traced). All are CMTK1011 naming the base. A base's sealed ToString() is the seam; one not sealed is CMTK1011, recorded in decision 121 as an exception to decision 34 (it built on 2.3.0). A base's private _name/_all and a [JsonConverter] on it build, as before (lead 2 of the sweep: harmless).

Activity

  1. added this to the 2.3.1 milestone on Oct 30, 2026
  2. added
    bugSomething isn't working
    silentCompiles or answers and does less than it appears to
    severity/mediumInconsistency or failure in a narrow scenario
    on Oct 30, 2026
  3. CaffeinatedCoder commented on Oct 31, 2026

    @CaffeinatedCoder
    OwnerAuthor

    A finding against #218 (9f749a4) from the pre-release sweep of release/2.3.1 (lens: new-check-vs-invariant); reproduced in a scratch generator test.

    ClosedSetTypes.GeneratedMembersDeclaredByHand's three method lines (src/CodoMetis.TypeKit.Generators/ClosedSetTypes.cs:224-226, Declared(...) over SelfAndBases(set) for Parse, TryParse and Match) don't filter on accessibility. The field and property line above them does (:222, member.Accessibility != Accessibility.Private), and the method's remarks say a private member of a base "is out of the set's sight and builds".

    public abstract class CodedSetBase
    {
        private static int Parse(string text) => int.Parse(text, CultureInfo.InvariantCulture);
        protected static int Code(string text) => Parse(text);
    }
    public sealed partial class CodedSet : CodedSetBase, IClosedSet<CodedSet>
    {
        public static readonly CodedSet First = new();
        public static readonly CodedSet Second = new();
    }

    Change: skip Accessibility.Private in Declared, as :222 does. Test: a private Parse/TryParse/Match in a set's base must generate, and a protected one is refused.

    The same root cause on value objects (#201, #203) is #230.

  4. CaffeinatedCoder commented on Oct 31, 2026

    @CaffeinatedCoder
    OwnerAuthor

    The finding above (a set's base class's private Parse, TryParse or Match refused by #218's method lines) is fixed in #244, stacked on #236 and #218, together with #230's value-object version: each base check counts only a member the type can see (IsAccessibleFrom), the field and property line included. No change to #218 is needed for it.

  5. CaffeinatedCoder commented on Oct 31, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Fixed by #218, merged into release/2.3.1 (48af3f2); ships in 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/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