Repository navigation
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
Description
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:
Declared(...)overSelfAndBases(target)inValueObjectDeclaration.GeneratedMembersDeclaredByHand(src/CodoMetis.TypeKit.Generators/ValueObjectDeclaration.cs:204-205, from fix: refuse a generated member written by hand wherever a value object reaches it #201 for A base record's public implementation of a generated member is kept as the value object's, or crashes the aspect #197), which feedsMembersNamed/MethodsWithSignatureforValue,From,MinValue/MaxValue,Parse,TryParse,ToString(string?, IFormatProvider?)andTryFormat;InheritedSeams(ValueObjectDeclaration.cs:435-442, from fix: refuse a seam a base record declares in place of the value object #203 for A base record's explicit Create from another project leaves two rule sets #195), forTryFromandRevalidate;- the base loop in
ValueObjectComparableAspect.HandWrittenComparisonMembers(src/CodoMetis.TypeKit.Generators/ValueObjectComparableAspect.cs:160-168, from fix: refuse a seam a base record declares in place of the value object #203 for An inherited CompareTo(TSelf) is not seen: EF Core orders by the column, and < overflows the stack #194), forCompareTo(object)andCompareTo(TSelf).
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)
- The filter is
IsAccessibleFrom(what the type can see), notAccessibility.Private. A value object nested in its base record sees the base's private members, as C# does, so it stays refused. A library's private and internal members were never listed by Metalama and build either way, measured. - The closed-set method lines (
ClosedSetTypes.GeneratedMembersDeclaredByHand, the finding on A closed set over a base class that declares a generated member fails with a Metalama error, or its ToString() is replaced #205) are fixed in the same PR, since it is stacked on fix: refuse a closed set's base-class member the generators introduce, and keep its sealed ToString() #218. The set's field and property line now uses the same filter. - Measured 2026-10-31 over the 6 accessibilities by every generated name (108 types): only the 19 private ones changed outcome.
Activity
CaffeinatedCoder commentedon Oct 31, 2026
Fixed by #244, merged into
release/2.3.1.