Skip to content

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

Closed

Description

@CaffeinatedCoder

Found by a pre-release sweep of release/2.3.1 @ dbdd44e plus #216, #217 and #218 as landed (lens: new-check-vs-invariant); reproduced in a scratch generator test against both v2.3.0 and 2.3.1.

Package

CodoMetis.TypeKit.Generators

Version

release/2.3.1 @ dbdd44e (a regression from 2.3.0, which generated these types correctly)

How does it fail?

The build fails (CMTK1011, CMTK1008) on code that 2.3.0 built and generated correctly, and the 2.3.1 changelog doesn't announce it.

Problem

The checks 2.3.1 added for a base type's members read every member of every base, at any accessibility:

C# hides a private member of a base from the derived type, so a private member stands in for no generated member: 2.3.0 generated each type below, and its tests pass. The changelog announces only base members that stood in for generated ones ("a base record's public MinValue or MaxValue, which stood in for the generated one", "a CompareTo… a value object inherits"). The closed-set version of this check skips private base fields and properties (ClosedSetTypes.cs:222, member.Accessibility != Accessibility.Private), and its remarks say a private member of a base "is out of the set's sight and builds".

protected and private protected members are not part of this: 2.3.0 failed on them with LAMA0500/LAMA0506 (measured), so CMTK1011 there is the announced improvement.

Scenario

public abstract record BoundedBase
{
    private const int MaxValue = 100;
    private static int Parse(string text, IFormatProvider? provider) => int.Parse(text, provider);
    protected static bool InRange(int value) => value is >= 0 and <= MaxValue && Parse("0", null) == 0;
}
public sealed partial record Level : BoundedBase, IValue<int>;

public abstract record CheckingBase
{
    private static bool TryFrom(int value) => value >= 0;
    protected static bool Accepts(int value) => TryFrom(value);
}
public sealed partial record Percent : CheckingBase, IValidatedValue<Percent, int, PercentFault>
{
    public static Result<Percent, PercentFault> Create(int value) =>
        Accepts(value) && value <= 100 ? new Percent(value) : Result.Error(PercentFault.OutOfRange);
}

public abstract record ComparingBase
{
    private int CompareTo(object? other) => 0;
    protected int Same(object? other) => CompareTo(other);
}
public sealed partial record Rank : ComparingBase, IValue<int>;

Measured, with a test that asserts Level.MaxValue.Value == int.MaxValue, Level.Parse("7", null), Percent.TryFrom(50) is some and TryFrom(101) none, and Rank.From(1) < Rank.From(2):

  • v2.3.0: TESTS OK: 1 passed.
  • 2.3.1: BUILD FAILED:
    • CMTK1011 'Level' declares BoundedBase.Parse(string, IFormatProvider?) in its base type BoundedBase, BoundedBase.MaxValue in its base type BoundedBase, so it is not generated…
    • CMTK1011 'Percent' declares CheckingBase.TryFrom(int) in its base type CheckingBase, so it is not generated…
    • CMTK1008 'Rank' declares ComparingBase.CompareTo(object?) in its base type ComparingBase…

The closed-set version, ClosedSetTypes.GeneratedMembersDeclaredByHand's method lines (a private Parse(string) in a set's base class), comes from #218 and is a comment on #205.

Change

Each base-member check skips a base member the value object can't see: Accessibility.Private (the same filter as ClosedSetTypes.cs:222). The value object's own members, and protected/private protected/internal/public base members, are refused as they are now.

Test

Input class (decision 95): a base member with a generated member's name, by accessibility. Rows in InheritedMembersTests for private, private protected, protected, internal and public, for each generated name (Value, From, MinValue/MaxValue, Parse/TryParse over string, span and UTF-8, ToString(string?, IFormatProvider?), TryFormat, TryFrom, Revalidate, CompareTo(object), CompareTo(TSelf)). A private row must generate and behave as above, and every other row must keep its CMTK error. Reverted per verify-the-guard.

Environment

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

In the fix (#244)

Activity

  1. added this to the 2.3.1 milestone on Oct 31, 2026
  2. added
    bugSomething isn't working
    severity/mediumInconsistency or failure in a narrow scenario
    package/generatorsAffects CodoMetis.TypeKit.Generators
    area/value-objectsValue-object contracts and what the aspects generate for them
    on Oct 31, 2026
  3. CaffeinatedCoder commented on Oct 31, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Fixed by #244, merged into release/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 scenario

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions