diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f208d9..c98277d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ section is the GitHub release's notes. - Analyzers: CMTK0005 now also warns at `Array.CreateInstance(typeof(OrderId), n)`, `Array.CreateInstanceFromArrayType(typeof(OrderId[]), n)` and `ArrayPool.Rent(n)`, which create default slots as `new OrderId[n]` does. In a build that treats warnings as errors, that can report code that built before ([CMTK0005](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.Analyzers/README.md#cmtk0005)). - Generators: an overload of a generated member that C# tells apart by its type parameters, or by a parameter that is `ref`, `in` or `out` where the generated one's isn't (or the other way round), is now kept beside the generated member, on a value object, a closed set or a base of either, where the build failed with CMTK1011: a `Parse(string, IFormatProvider?)`, a `From(in int)`, a `TryParse` whose last parameter isn't `out` ([Write your own members](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.Generators/README.md#write-your-own-members)). - Generators: a generic `ToString()`, or a generic or by-`in` `CompareTo(TSelf)`, is no longer taken for the one you may write: a value object printed what a record prints (`Level { Value = 3 }`), a closed set its type name, and the ordering was marked hand-written, so EF Core refused to order by it. A generic or by-reference overload of `TryFrom`, `Revalidate`, `CompareTo(object)`, `Equals`, `GetHashCode` or `PrintMembers` is kept beside the generated member, where the build failed with CMTK1011 or CMTK1008 ([Write your own members](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.Generators/README.md#write-your-own-members)). +- Generators: a member of a base record or of a closed set's base class that the type can't see, a `private` one such as a `private const int MaxValue`, doesn't count as its own: it stands in for no generated member, so the checks above leave it alone and the type is generated as in 2.3.0 ([Write your own members](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.Generators/README.md#write-your-own-members)). ### Fixed diff --git a/design/aspects.md b/design/aspects.md index d03456c..70b4613 100644 --- a/design/aspects.md +++ b/design/aspects.md @@ -175,7 +175,9 @@ a generated name was kept as the value object's (`MaxValue`) or failed the aspect (LAMA0500); and a base record's `Create` from another project was a second rule set for generic `T.Create`. A seam counts where the value object declares it: a base record's `CompareTo(TSelf)`, `TryFrom` or `Revalidate` stood in for the - generated member, and an inherited `CompareTo` made `<` overflow the stack (#194). Found by the interface + generated member, and an inherited `CompareTo` made `<` overflow the stack (#194). A base's member the value + object cannot see (`IsAccessibleFrom`), a private one, reaches nothing: C# hides it, the generated member sits + beside it, and 2.3.0 generated the type, so it is not counted, on a value object or a closed set (#230). Found by the interface mapping (`TryFindImplementationForInterfaceMember`), never by listing members: Metalama lists no private member of a type in another assembly, so a base record's from a referenced project was missed as well. `IConvertible`, `IValueObject<,>`, `IValueObjectMaterializer<,>` and `IPlainValueObject<,>` declared through an diff --git a/src/CodoMetis.TypeKit.Generators/ClosedSetTypes.cs b/src/CodoMetis.TypeKit.Generators/ClosedSetTypes.cs index f152439..41d6c12 100644 --- a/src/CodoMetis.TypeKit.Generators/ClosedSetTypes.cs +++ b/src/CodoMetis.TypeKit.Generators/ClosedSetTypes.cs @@ -197,8 +197,8 @@ private static string WithoutComments(string text) /// /// A base class's count as the set's own (#205): its Parse, TryParse, Match or All, /// and an IParsable<TSelf> it implements, failed the aspect (LAMA0500, LAMA0503, LAMA0512) naming - /// nothing the user wrote, in this project and from another (measured 2026-10-30). A private member of a base, - /// such as a _name field, is out of the set's sight and builds. + /// nothing the user wrote, in this project and from another (measured 2026-10-30). A member of a base the set cannot + /// see, such as a private _name field or Parse(string), is out of its sight and builds (#230). /// /// public static IReadOnlyList GeneratedMembersDeclaredByHand(INamedType set, IReadOnlyList members) @@ -212,7 +212,7 @@ public static IReadOnlyList GeneratedMembersDeclaredByHand(INamedType se var bases = ValueObjectDeclaration.SelfAndBases(set).Skip(1).ToList(); IEnumerable Declared(Func> find) => - ValueObjectDeclaration.SelfAndBases(set).SelectMany(type => find(type).Select(method => type.Equals(set) + ValueObjectDeclaration.SelfAndBases(set).SelectMany(type => find(type).Where(method => method.IsAccessibleFrom(set)).Select(method => type.Equals(set) ? ValueObjectDeclaration.Describe(method) : $"{ValueObjectDeclaration.Describe(method)} in its base type {type.ToDisplayString()}")); @@ -220,7 +220,7 @@ IEnumerable Declared(Func> find) => [ .. ValueObjectDeclaration.MembersNamed(set, methods: false, [.. generatedNames]).Where(declared => !memberNames.Contains(declared)), .. bases.SelectMany(type => type.Fields.Concat(type.Properties).Concat(type.Events) - .Where(member => member is { IsImplicitlyDeclared: false, IsExplicitInterfaceImplementation: false } && member.Accessibility != Accessibility.Private && generatedNames.Contains(member.Name)) + .Where(member => member is { IsImplicitlyDeclared: false, IsExplicitInterfaceImplementation: false } && member.IsAccessibleFrom(set) && generatedNames.Contains(member.Name)) .Select(member => $"{member.ToDisplayString()} in its base type {type.ToDisplayString()}")), .. Declared(type => type.Methods.Where(method => IsOwn(method) && method.Name == "Parse" && (SameParameters(method, text) || SameParameters(method, text, boolean)))), .. Declared(type => type.Methods.Where(method => IsOwn(method) && method.Name == "TryParse" && (SameParameters(method, text, result) || SameParameters(method, text, boolean, result)))), diff --git a/src/CodoMetis.TypeKit.Generators/README.md b/src/CodoMetis.TypeKit.Generators/README.md index 53f439c..bae8fa8 100644 --- a/src/CodoMetis.TypeKit.Generators/README.md +++ b/src/CodoMetis.TypeKit.Generators/README.md @@ -237,7 +237,8 @@ covers: another. - **Any of these members in a base record**, public or explicit, in your project or another. A base's `MaxValue` would stand in for the generated one, and generic code calling `T.MaxValue` would reach - it. The error names the base. + it. The error names the base. A base's `private` member is fine: the value object can't see it, so + it stands in for nothing. An overload of a generated member is yours, and the generated one sits beside it. C# tells two methods apart by their type parameters and by whether a parameter is `ref`, `in` or `out`, so a @@ -471,8 +472,9 @@ public sealed partial class OrderStatus : IClosedSet, IParsable(string name)`, is kept too. Any other generated member written by hand fails the build (CMTK1011), in the set or in a - class it derives from. + generic `Parse(string name)`, is kept too. Any other generated member written by hand + fails the build (CMTK1011), in the set or in a class it derives from. A `private` one in that + class is fine, since the set can't see it. - **A base class's `ToString()`.** A `sealed` one is kept, since C# keeps it in every derived class. One that isn't sealed fails the build (CMTK1011), because the generated `ToString()` would replace it. Declare `ToString()` in the set, or seal the base's. diff --git a/src/CodoMetis.TypeKit.Generators/ValueObjectComparableAspect.cs b/src/CodoMetis.TypeKit.Generators/ValueObjectComparableAspect.cs index 8f30166..a0c3039 100644 --- a/src/CodoMetis.TypeKit.Generators/ValueObjectComparableAspect.cs +++ b/src/CodoMetis.TypeKit.Generators/ValueObjectComparableAspect.cs @@ -156,13 +156,14 @@ private static IEnumerable HandWrittenComparisonMembers(INamedType targe // A base record's CompareTo(object) failed the aspect (LAMA0500), and its CompareTo(TSelf), the seam, was // used without a word: no [HandWrittenOrdering], so EF Core ordered by the column, and the generated // operators called the generated CompareTo(object), which C# prefers to the base's, until the stack - // overflowed (#194, measured 2026-10-30). The seam is kept where the value object declares it. + // overflowed (#194, measured 2026-10-30). The seam is kept where the value object declares it. A base's + // private CompareTo is hidden from the value object, so it stands in for nothing (#230). foreach (var type in ValueObjectDeclaration.SelfAndBases(target).Skip(1)) { foreach (var method in type.Methods) { if (method is { IsStatic: false, IsExplicitInterfaceImplementation: false, Name: nameof(IComparable.CompareTo), Parameters: [{ } other] } - && ValueObjectDeclaration.IsByValueAndNotGeneric(method) && ValueObjectDeclaration.IsHandWritten(method) + && ValueObjectDeclaration.IsByValueAndNotGeneric(method) && ValueObjectDeclaration.IsHandWritten(method) && method.IsAccessibleFrom(target) && (other.Type.SpecialType == SpecialType.Object || other.Type.ToNonNullable().Equals(target))) yield return $"{method.ToDisplayString()} in its base type {type.ToDisplayString()}"; } diff --git a/src/CodoMetis.TypeKit.Generators/ValueObjectDeclaration.cs b/src/CodoMetis.TypeKit.Generators/ValueObjectDeclaration.cs index a240322..288a3f5 100644 --- a/src/CodoMetis.TypeKit.Generators/ValueObjectDeclaration.cs +++ b/src/CodoMetis.TypeKit.Generators/ValueObjectDeclaration.cs @@ -201,17 +201,18 @@ public static IReadOnlyList GeneratedMembersDeclaredByHand(INamedType ta // A base record's count as the value object's own (#197). Beside a generated one, its public // Value, From, Parse, TryParse, ToString(string?, IFormatProvider?) or TryFormat failed the aspect // (LAMA0500), naming nothing the user wrote, and its static MinValue and MaxValue were kept as the - // value object's, with no MinValue or MaxValue generated. + // value object's, with no MinValue or MaxValue generated. One the value object cannot see, a base's + // private member, stands in for nothing: C# hides it, and the generated one sits beside it (#230). IEnumerable Declared(Func> find) => SelfAndBases(target).SelectMany(type => find(type).Select(member => type.Equals(target) ? member : $"{member} in its base type {type.ToDisplayString()}")); // ValueObjectAspect: the field, the entry points; ValueObjectContractAspect: Value; the JSON aspect's nested converter. - found.AddRange(Declared(type => MembersNamed(type, "_value", "Value", "__FromJson", "__FromText", "__TryFromText", $"{target.Name}JsonConverter"))); + found.AddRange(Declared(type => MembersNamed(type, target, "_value", "Value", "__FromJson", "__FromText", "__TryFromText", $"{target.Name}JsonConverter"))); if (kind == ValueObjectKind.Plain) { - found.AddRange(Declared(type => MethodsWithSignature(type, "From", (valueType, false)))); - found.AddRange(Declared(type => MembersNamed(type, methods: false, "From"))); + found.AddRange(Declared(type => MethodsWithSignature(type, target, "From", (valueType, false)))); + found.AddRange(Declared(type => MembersNamed(type, target, methods: false, "From"))); } // A generated method's parameters, and whether each is out: an overload that takes one by value where the @@ -225,9 +226,9 @@ IEnumerable Declared(Func> find) => if (parse != ValueParseStrategy.Unsupported) { - found.AddRange(Declared(type => MethodsWithSignature(type, "Parse", stringType, provider))); - found.AddRange(Declared(type => MethodsWithSignature(type, "TryParse", stringType, provider, result))); - found.AddRange(Declared(type => MembersNamed(type, $"{target.Name}TypeConverter"))); + found.AddRange(Declared(type => MethodsWithSignature(type, target, "Parse", stringType, provider))); + found.AddRange(Declared(type => MethodsWithSignature(type, target, "TryParse", stringType, provider, result))); + found.AddRange(Declared(type => MembersNamed(type, target, $"{target.Name}TypeConverter"))); generatedInterfaces.Add(TypeFactory.GetNamedType(typeof(IParsable<>))); if (ValueObjectParsableAspect.SupportsSpan(valueType)) @@ -235,8 +236,8 @@ IEnumerable Declared(Func> find) => generatedInterfaces.Add(TypeFactory.GetNamedType(typeof(ISpanParsable<>))); (IType, bool) span = (TypeFactory.GetType(typeof(ReadOnlySpan)), false); - found.AddRange(Declared(type => MethodsWithSignature(type, "Parse", span, provider))); - found.AddRange(Declared(type => MethodsWithSignature(type, "TryParse", span, provider, result))); + found.AddRange(Declared(type => MethodsWithSignature(type, target, "Parse", span, provider))); + found.AddRange(Declared(type => MethodsWithSignature(type, target, "TryParse", span, provider, result))); } if (ValueObjectParsableAspect.SupportsUtf8(valueType)) @@ -244,26 +245,26 @@ IEnumerable Declared(Func> find) => generatedInterfaces.Add(TypeFactory.GetNamedType(typeof(IUtf8SpanParsable<>))); (IType, bool) utf8 = (TypeFactory.GetType(typeof(ReadOnlySpan)), false); - found.AddRange(Declared(type => MethodsWithSignature(type, "Parse", utf8, provider))); - found.AddRange(Declared(type => MethodsWithSignature(type, "TryParse", utf8, provider, result))); + found.AddRange(Declared(type => MethodsWithSignature(type, target, "Parse", utf8, provider))); + found.AddRange(Declared(type => MethodsWithSignature(type, target, "TryParse", utf8, provider, result))); } } // ValueObjectFormattableAspect, unless ToString() is hand-written and it generates nothing. if (!DeclaresToString(target)) { - found.AddRange(Declared(type => MethodsWithSignature(type, nameof(IFormattable.ToString), stringType, provider))); + found.AddRange(Declared(type => MethodsWithSignature(type, target, nameof(IFormattable.ToString), stringType, provider))); generatedInterfaces.Add(TypeFactory.GetNamedType(typeof(IFormattable))); if (ValueObjectFormattableAspect.ResolveStrategy(valueType) == ValueFormatStrategy.SpanFormattable) { - found.AddRange(Declared(type => MethodsWithSignature(type, nameof(ISpanFormattable.TryFormat), (TypeFactory.GetType(typeof(Span)), false), (TypeFactory.GetType(SpecialType.Int32), true), (TypeFactory.GetType(typeof(ReadOnlySpan)), false), provider))); + found.AddRange(Declared(type => MethodsWithSignature(type, target, nameof(ISpanFormattable.TryFormat), (TypeFactory.GetType(typeof(Span)), false), (TypeFactory.GetType(SpecialType.Int32), true), (TypeFactory.GetType(typeof(ReadOnlySpan)), false), provider))); generatedInterfaces.Add(TypeFactory.GetNamedType(typeof(ISpanFormattable))); } if (ValueObjectFormattableAspect.SupportsUtf8(valueType)) { - found.AddRange(Declared(type => MethodsWithSignature(type, nameof(IUtf8SpanFormattable.TryFormat), (TypeFactory.GetType(typeof(Span)), false), (TypeFactory.GetType(SpecialType.Int32), true), (TypeFactory.GetType(typeof(ReadOnlySpan)), false), provider))); + found.AddRange(Declared(type => MethodsWithSignature(type, target, nameof(IUtf8SpanFormattable.TryFormat), (TypeFactory.GetType(typeof(Span)), false), (TypeFactory.GetType(SpecialType.Int32), true), (TypeFactory.GetType(typeof(ReadOnlySpan)), false), provider))); generatedInterfaces.Add(TypeFactory.GetNamedType(typeof(IUtf8SpanFormattable))); } } @@ -271,7 +272,7 @@ IEnumerable Declared(Func> find) => // ValueObjectMinMaxValueAspect. if (kind == ValueObjectKind.Plain && ValueObjectMinMaxValueAspect.HasMinMaxValue(valueType)) { - found.AddRange(Declared(type => MembersNamed(type, nameof(IMinMaxValue.MinValue), nameof(IMinMaxValue.MaxValue)))); + found.AddRange(Declared(type => MembersNamed(type, target, nameof(IMinMaxValue.MinValue), nameof(IMinMaxValue.MaxValue)))); generatedInterfaces.Add(TypeFactory.GetNamedType(typeof(IMinMaxValue<>))); } @@ -433,13 +434,14 @@ private static string PublicName(IMember implementation, IMember implemented) /// base's counted, so it stood in for the value object's without a word: TryFrom answered whatever the /// base did, which need not call Create, and Revalidate threw where the base's did (#194, /// measured 2026-10-30). A base's FromKnownGood(T) is hidden by the generated one, whose caller - /// expression parameter makes it another signature, so it is not one. + /// expression parameter makes it another signature, so it is not one, and a base's private one is hidden from the + /// value object, which C# never calls it for (#230). /// public static IReadOnlyList InheritedSeams(INamedType target, INamedType valueType, ValueObjectKind kind) => kind != ValueObjectKind.Validated ? [] : [.. SelfAndBases(target).Skip(1).SelectMany(type => type.Methods - .Where(method => IsHandWritten(method) && !method.IsExplicitInterfaceImplementation && IsByValueAndNotGeneric(method) + .Where(method => IsHandWritten(method) && !method.IsExplicitInterfaceImplementation && IsByValueAndNotGeneric(method) && method.IsAccessibleFrom(target) && (method is { Name: "TryFrom", IsStatic: true, Parameters: [{ } value] } && value.Type.ToNonNullable().Equals(valueType.ToNonNullable()) || method is { Name: "Revalidate", IsStatic: false, Parameters.Count: 0 })) .Select(method => $"{method.ToDisplayString()} in its base type {type.ToDisplayString()}"))]; @@ -658,37 +660,43 @@ internal static bool IsHandWritten(IMember member) => internal static bool IsByValueAndNotGeneric(IMethod method) => method.TypeParameters.Count == 0 && method.Parameters.All(parameter => parameter.RefKind == RefKind.None); - /// Non-implicit members of with one of , of any kind. - private static IEnumerable MembersNamed(INamedType target, params string[] names) => MembersNamed(target, methods: true, names); + /// + /// Non-implicit members of with one of , of any kind, that + /// can see: a base's private member is hidden from the value object (#230). + /// + private static IEnumerable MembersNamed(INamedType target, INamedType seenFrom, params string[] names) => MembersNamed(target, seenFrom, methods: true, names); + + /// Non-implicit members of with one of . + internal static IEnumerable MembersNamed(INamedType target, bool methods, params string[] names) => MembersNamed(target, target, methods, names); /// /// An explicit implementation is 's, which names it, and so is a public static /// member of another assembly that implements a static abstract one, which reads as explicit there. /// - internal static IEnumerable MembersNamed(INamedType target, bool methods, params string[] names) + private static IEnumerable MembersNamed(INamedType target, INamedType seenFrom, bool methods, params string[] names) { foreach (var field in target.Fields) - if (!field.IsImplicitlyDeclared && Enumerable.Contains(names, field.Name)) yield return field.ToDisplayString(); + if (!field.IsImplicitlyDeclared && Enumerable.Contains(names, field.Name) && field.IsAccessibleFrom(seenFrom)) yield return field.ToDisplayString(); foreach (var property in target.Properties) - if (property is { IsImplicitlyDeclared: false, IsExplicitInterfaceImplementation: false } && Enumerable.Contains(names, property.Name)) yield return property.ToDisplayString(); + if (property is { IsImplicitlyDeclared: false, IsExplicitInterfaceImplementation: false } && Enumerable.Contains(names, property.Name) && property.IsAccessibleFrom(seenFrom)) yield return property.ToDisplayString(); foreach (var @event in target.Events) - if (@event is { IsImplicitlyDeclared: false, IsExplicitInterfaceImplementation: false } && Enumerable.Contains(names, @event.Name)) yield return @event.ToDisplayString(); + if (@event is { IsImplicitlyDeclared: false, IsExplicitInterfaceImplementation: false } && Enumerable.Contains(names, @event.Name) && @event.IsAccessibleFrom(seenFrom)) yield return @event.ToDisplayString(); foreach (var nested in target.Types) - if (Enumerable.Contains(names, nested.Name)) yield return $"the nested type {nested.ToDisplayString()}"; + if (Enumerable.Contains(names, nested.Name) && nested.IsAccessibleFrom(seenFrom)) yield return $"the nested type {nested.ToDisplayString()}"; if (!methods) yield break; foreach (var method in target.Methods) - if (!method.IsImplicitlyDeclared && !method.IsExplicitInterfaceImplementation && Enumerable.Contains(names, method.Name)) yield return method.ToDisplayString(); + if (!method.IsImplicitlyDeclared && !method.IsExplicitInterfaceImplementation && Enumerable.Contains(names, method.Name) && method.IsAccessibleFrom(seenFrom)) yield return method.ToDisplayString(); } /// /// Methods named whose parameter types are , - /// whatever they return: a generated member with that signature is either refused by Metalama or - /// skipped in favour of the hand-written one. + /// whatever they return, that can see: a generated member with that signature is + /// either refused by Metalama or skipped in favour of the hand-written one. /// /// /// The signature C# overloads by (#226): no type parameters, as the generated method has none, and each @@ -697,11 +705,12 @@ internal static IEnumerable MembersNamed(INamedType target, bool methods /// last parameter is by value are other methods, which the generated ones sit beside. ref or in /// against out is no overload in C# (CS0663), so it is the same method, as nullability is. /// - private static IEnumerable MethodsWithSignature(INamedType target, string name, params (IType Type, bool Out)[] parameters) => + private static IEnumerable MethodsWithSignature(INamedType target, INamedType seenFrom, string name, params (IType Type, bool Out)[] parameters) => target.Methods .Where(method => method is { IsImplicitlyDeclared: false, IsExplicitInterfaceImplementation: false, TypeParameters.Count: 0 } && method.Name == name && method.Parameters.Count == parameters.Length - && method.Parameters.Zip(parameters, (declared, expected) => declared.Type.Equals(expected.Type) && declared.RefKind != RefKind.None == expected.Out).All(same => same)) + && method.Parameters.Zip(parameters, (declared, expected) => declared.Type.Equals(expected.Type) && declared.RefKind != RefKind.None == expected.Out).All(same => same) + && method.IsAccessibleFrom(seenFrom)) .Select(method => method.ToDisplayString()); } diff --git a/test/CodoMetis.TypeKit.Generators.Tests/BaseMemberAccessibility.cs b/test/CodoMetis.TypeKit.Generators.Tests/BaseMemberAccessibility.cs new file mode 100644 index 0000000..2b04171 --- /dev/null +++ b/test/CodoMetis.TypeKit.Generators.Tests/BaseMemberAccessibility.cs @@ -0,0 +1,203 @@ +using System.Text; + +namespace CodoMetis.TypeKit.Generators.Tests; + +/// +/// A base type's member of a generated member's name or signature, by its accessibility (#230): what the value +/// object or the closed set can see stands in for the generated member and is refused, and what it cannot see, a +/// base's private member, is hidden by C# and stands in for nothing, so the type is generated as 2.3.0 generated +/// it. One base per member and accessibility, one value object or set over each, built in +/// ; what the private ones generate is '. +/// +internal static class BaseMemberAccessibility +{ + /// Every accessibility a member of a base can have, by the name the declarations use for it. + public static IReadOnlyList<(string Name, string Keyword)> Accessibilities { get; } = + [ + ("Private", "private"), + ("PrivateProtected", "private protected"), + ("Protected", "protected"), + ("Internal", "internal"), + ("ProtectedInternal", "protected internal"), + ("Public", "public"), + ]; + + /// + /// The generated members a base record's member stands in for, the declaration of each in a base record + /// <TSelf>, whether the value object is a validated one, and the error that refuses it. + /// + private static readonly (string Name, string Declaration, bool Validated, string Id)[] ValueObjectMembers = + [ + ("Value", "int Value => 0;", false, "CMTK1011"), + ("From", "static TSelf From(int value) => throw new System.NotSupportedException();", false, "CMTK1011"), + ("MinValue", "static int MinValue => 0;", false, "CMTK1011"), + ("MaxValue", "static int MaxValue => 0;", false, "CMTK1011"), + ("Parse", "static TSelf Parse(string text, System.IFormatProvider? provider) => throw new System.NotSupportedException();", false, "CMTK1011"), + ("TryParse", "static bool TryParse(string? text, System.IFormatProvider? provider, out TSelf result) => throw new System.NotSupportedException();", false, "CMTK1011"), + ("SpanParse", "static TSelf Parse(System.ReadOnlySpan text, System.IFormatProvider? provider) => throw new System.NotSupportedException();", false, "CMTK1011"), + ("SpanTryParse", "static bool TryParse(System.ReadOnlySpan text, System.IFormatProvider? provider, out TSelf result) => throw new System.NotSupportedException();", false, "CMTK1011"), + ("Utf8Parse", "static TSelf Parse(System.ReadOnlySpan text, System.IFormatProvider? provider) => throw new System.NotSupportedException();", false, "CMTK1011"), + ("Utf8TryParse", "static bool TryParse(System.ReadOnlySpan text, System.IFormatProvider? provider, out TSelf result) => throw new System.NotSupportedException();", false, "CMTK1011"), + ("Format", "string ToString(string? format, System.IFormatProvider? provider) => \"\";", false, "CMTK1011"), + ("TryFormat", "bool TryFormat(System.Span destination, out int charsWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) => throw new System.NotSupportedException();", false, "CMTK1011"), + ("ObjectCompareTo", "int CompareTo(object? other) => 0;", false, "CMTK1008"), + ("CompareTo", "int CompareTo(TSelf? other) => 0;", false, "CMTK1008"), + ("TryFrom", "static Option TryFrom(int value) => throw new System.NotSupportedException();", true, "CMTK1011"), + ("Revalidate", "Result Revalidate() => throw new System.NotSupportedException();", true, "CMTK1011"), + ]; + + /// The generated members of a closed set a base class's member stands in for, declared in a base class <TSelf>. + private static readonly (string Name, string Declaration)[] SetMembers = + [ + ("Parse", "static TSelf Parse(string name) => throw new System.NotSupportedException();"), + // The ignore-case form, which no interface declares: a base's protected TryParse(string?, out TSelf) is also + // CS0737 beside the refusal, as it can't implement IClosedSet.TryParse. + ("TryParse", "static bool TryParse(string? name, bool ignoreCase, out TSelf member) => throw new System.NotSupportedException();"), + ("Match", "TResult Match(System.Func a) => a();"), + ]; + + private static string ValueObject(string member, string accessibility) => $"{member}In{accessibility}Base"; + + private static string Set(string member, string accessibility) => $"SetWith{member}In{accessibility}Base"; + + /// The value objects, for the consumer's declarations build. + public static string ValueObjectSource() + { + var source = new StringBuilder( + """ + using CodoMetis.TypeKit; + using CodoMetis.TypeKit.ValueObjects; + + namespace Consumer; + + public enum AccessibilityFault { Negative } + + // A value object nested in its base record sees the base's private members, as C# does: refused. + public abstract record EnclosingBase + { + private static int MaxValue => 0; + + public sealed partial record NestedInItsBase : EnclosingBase, IValue; + } + + // A library's private and internal members, which no project but the library's sees. + public sealed partial record OverALibrarysHiddenMembers : Library.HiddenMembersBase, IValue; + + """); + + foreach (var (member, declaration, validated, _) in ValueObjectMembers) + { + foreach (var (accessibility, keyword) in Accessibilities) + { + var type = ValueObject(member, accessibility); + source.AppendLine($"public abstract record {type}Record where TSelf : notnull {{ {keyword} {declaration} }}"); + source.AppendLine(validated + ? $"public sealed partial record {type} : {type}Record<{type}>, IValidatedValue<{type}, int, AccessibilityFault> {{ public static Result<{type}, AccessibilityFault> Create(int value) => throw new System.NotSupportedException(); }}" + : $"public sealed partial record {type} : {type}Record<{type}>, IValue;"); + } + } + + return source.ToString(); + } + + /// The closed sets, for the consumer's refused-sets build, where a refused set's CS0535 is expected. + public static string SetSource() + { + var source = new StringBuilder( + """ + using CodoMetis.TypeKit.ClosedSets; + + namespace Consumer; + + public sealed partial class SetOverALibrarysHiddenMembers : Library.HiddenSetMembersBase, IClosedSet { public static readonly SetOverALibrarysHiddenMembers A = new(); } + + """); + + foreach (var (member, declaration) in SetMembers) + { + foreach (var (accessibility, keyword) in Accessibilities) + { + var type = Set(member, accessibility); + source.AppendLine($"public abstract class {type}Class {{ {keyword} {declaration} }}"); + source.AppendLine($"public sealed partial class {type} : {type}Class<{type}>, IClosedSet<{type}> {{ public static readonly {type} A = new(); }}"); + } + } + + return source.ToString(); + } + + /// What the library declares, appended to its file-scoped namespace, which the consumer cannot see. + public const string LibrarySource = + """ + + public abstract record HiddenMembersBase where TSelf : notnull + { + private static int MaxValue => 0; + internal static int MinValue => 0; + private static TSelf Parse(string text, System.IFormatProvider? provider) => throw new System.NotSupportedException(); + internal static bool TryParse(string? text, System.IFormatProvider? provider, out TSelf result) => throw new System.NotSupportedException(); + private int CompareTo(object? other) => 0; + internal int CompareTo(TSelf? other) => 0; + protected int Uses() => MaxValue + CompareTo(null) + (Parse("", null) is null ? 0 : 1); + } + + public abstract class HiddenSetMembersBase + { + private static TSelf Parse(string name) => throw new System.NotSupportedException(); + internal static bool TryParse(string? name, out TSelf member) => throw new System.NotSupportedException(); + protected static TSelf Uses() => Parse(""); + } + """; + + /// Each value object or set over a member it can see, with the error that refuses it and the member's name. + public static TheoryData Refused() + { + var rows = new TheoryData(); + + foreach (var (member, _, _, id) in ValueObjectMembers) + foreach (var (accessibility, _) in Accessibilities.Where(accessibility => accessibility.Name != "Private")) + rows.Add(accessibility, id, ValueObject(member, accessibility), $"{ValueObject(member, accessibility)}Record<{ValueObject(member, accessibility)}>."); + + foreach (var (member, _) in SetMembers) + foreach (var (accessibility, _) in Accessibilities.Where(accessibility => accessibility.Name != "Private")) + rows.Add(accessibility, "CMTK1011", Set(member, accessibility), $"{Set(member, accessibility)}Class<{Set(member, accessibility)}>."); + + rows.Add("private, seen from a nested type", "CMTK1011", "EnclosingBase.NestedInItsBase", "EnclosingBase.MaxValue in its base type EnclosingBase"); + return rows; + } + + /// Each value object or set over a member it cannot see. + public static TheoryData Generated() => + [ + .. ValueObjectMembers.Select(member => ValueObject(member.Name, "Private")), + .. SetMembers.Select(member => Set(member.Name, "Private")), + "OverALibrarysHiddenMembers", + "SetOverALibrarysHiddenMembers", + ]; +} + +public sealed partial class BuildOutcomeTests +{ + public static TheoryData SeenBaseMembers => BaseMemberAccessibility.Refused(); + + public static TheoryData HiddenBaseMembers => BaseMemberAccessibility.Generated(); + + /// + /// A base's member the value object or set can see, protected, internal or public, stands in for the generated + /// one, and is refused naming the base, as for a public one (#197, #205). + /// + [Theory] + [MemberData(nameof(SeenBaseMembers))] + public void A_base_member_the_type_can_see_is_refused(string accessibility, string id, string type, string member) => + consumer.Errors.ShouldContain(error => error.Id == id && error.Message.StartsWith($"'{type}' declares ") && error.Message.Contains(member) && error.Message.Contains(" in its base type "), + $"{id} on {type}, {accessibility}. The build reported:{Environment.NewLine}{consumer.Output}"); + + /// + /// A base's private member is hidden from the value object or set, so it stands in for nothing: the type builds, + /// as it did on 2.3.0, where 2.3.1's base-record checks refused it (#230). So does a library's internal one. + /// + [Theory] + [MemberData(nameof(HiddenBaseMembers))] + public void A_base_member_the_type_cannot_see_is_not_refused(string type) => + consumer.Errors.ShouldNotContain(error => error.Message.Contains($"'{type}'"), consumer.Output); +} diff --git a/test/CodoMetis.TypeKit.Generators.Tests/BuildOutcomeTests.cs b/test/CodoMetis.TypeKit.Generators.Tests/BuildOutcomeTests.cs index a726059..da9acf4 100644 --- a/test/CodoMetis.TypeKit.Generators.Tests/BuildOutcomeTests.cs +++ b/test/CodoMetis.TypeKit.Generators.Tests/BuildOutcomeTests.cs @@ -2252,9 +2252,9 @@ public readonly partial record struct SplitAcrossFiles public async ValueTask InitializeAsync() { - var library = await BuildLibrary(_libraryDirectory, Library); - var declarations = Build(_directory, new Dictionary { ["Declarations.cs"] = Declarations, ["OtherFile.cs"] = OtherFile }, library); - var sets = Build(_setsDirectory, new Dictionary { ["RefusedSets.cs"] = RefusedSets }, library); + var library = await BuildLibrary(_libraryDirectory, Library + BaseMemberAccessibility.LibrarySource); + var declarations = Build(_directory, new Dictionary { ["Declarations.cs"] = Declarations, ["OtherFile.cs"] = OtherFile, ["BaseMemberAccessibility.cs"] = BaseMemberAccessibility.ValueObjectSource() }, library); + var sets = Build(_setsDirectory, new Dictionary { ["RefusedSets.cs"] = RefusedSets, ["BaseMemberAccessibility.cs"] = BaseMemberAccessibility.SetSource() }, library); var (declarationOutput, declarationErrors) = await declarations; var (setOutput, setErrors) = await sets; diff --git a/test/CodoMetis.TypeKit.Generators.Tests/GeneratedSurface.verified.txt b/test/CodoMetis.TypeKit.Generators.Tests/GeneratedSurface.verified.txt index ec5946d..23b0b8c 100644 --- a/test/CodoMetis.TypeKit.Generators.Tests/GeneratedSurface.verified.txt +++ b/test/CodoMetis.TypeKit.Generators.Tests/GeneratedSurface.verified.txt @@ -3676,6 +3676,61 @@ OverParsesVirtually static op_Inequality(OverParsesVirtually, OverParsesVirtually) : Boolean extension static OverParsesVirtuallyExtensions.GetValue(OverParsesVirtually) : IParsesVirtually extension static OverParsesVirtuallyExtensions.ValueOrNull(OverParsesVirtually?) : IParsesVirtually +OverPrivateNames + $() : OverPrivateNames + CompareTo(Object) : Int32 + CompareTo(OverPrivateNames) : Int32 + Equals(Object) : Boolean + Equals(OverPrivateNames) : Boolean + Equals(PrivateNamesBase) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int32 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IEquatable> + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements IReimplementedEquatable> + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverPrivateNamesJsonConverter : JsonConverter, IStoredJsonConverterSource + nested OverPrivateNamesTypeConverter : TypeConverter + static From(Int32) : OverPrivateNames + static MaxValue : OverPrivateNames + static MinValue : OverPrivateNames + static Parse(ReadOnlySpan, IFormatProvider) : OverPrivateNames + static Parse(ReadOnlySpan, IFormatProvider) : OverPrivateNames + static Parse(String, IFormatProvider) : OverPrivateNames + static TryParse(ReadOnlySpan, IFormatProvider, out OverPrivateNames) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out OverPrivateNames) : Boolean + static TryParse(String, IFormatProvider, out OverPrivateNames) : Boolean + static op_Equality(OverPrivateNames, OverPrivateNames) : Boolean + static op_GreaterThan(OverPrivateNames, OverPrivateNames) : Boolean + static op_GreaterThanOrEqual(OverPrivateNames, OverPrivateNames) : Boolean + static op_Inequality(OverPrivateNames, OverPrivateNames) : Boolean + static op_LessThan(OverPrivateNames, OverPrivateNames) : Boolean + static op_LessThanOrEqual(OverPrivateNames, OverPrivateNames) : Boolean + extension static OverPrivateNamesExtensions.GetValue(OverPrivateNames) : Int32 + extension static OverPrivateNamesExtensions.ValueOrNull(OverPrivateNames) : Int32? OverProbeStatus Equals(Object) : Boolean Equals(OverProbeStatus) : Boolean @@ -4746,6 +4801,24 @@ SetOverAnOverloadingBase static Parse(String, Boolean) : SetOverAnOverloadingBase static TryParse(String, Boolean, out SetOverAnOverloadingBase) : Boolean static TryParse(String, out SetOverAnOverloadingBase) : Boolean +SetOverPrivateNames + Match(Func, Func) : TResult + Name : String + QualifiedName : String + ToString() : String + attribute CompilerGeneratedAttribute + attribute GeneratedClosedSetAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IClosedSet + implements IParsable + nested SetOverPrivateNamesJsonConverter : JsonConverter + nested SetOverPrivateNamesTypeConverter : TypeConverter + static All : IReadOnlyList + static Parse(String) : SetOverPrivateNames + static Parse(String, Boolean) : SetOverPrivateNames + static TryParse(String, Boolean, out SetOverPrivateNames) : Boolean + static TryParse(String, out SetOverPrivateNames) : Boolean SetWithAGenericToString Match(Func) : TResult Name : String @@ -5556,3 +5629,57 @@ ValidatedOverASeamOverloadingBase static op_LessThanOrEqual(ValidatedOverASeamOverloadingBase, ValidatedOverASeamOverloadingBase) : Boolean extension static ValidatedOverASeamOverloadingBaseExtensions.GetValue(ValidatedOverASeamOverloadingBase) : Int32 extension static ValidatedOverASeamOverloadingBaseExtensions.ValueOrNull(ValidatedOverASeamOverloadingBase) : Int32? +ValidatedOverPrivateNames + $() : ValidatedOverPrivateNames + CompareTo(Object) : Int32 + CompareTo(ValidatedOverPrivateNames) : Int32 + Equals(Object) : Boolean + Equals(PrivateNamesBase) : Boolean + Equals(ValidatedOverPrivateNames) : Boolean + GetHashCode() : Int32 + Revalidate() : Result + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int32 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable> + implements IEquatable + implements IFormattable + implements IParsable + implements IReimplementedEquatable> + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValidatedValue + implements IValueObject + implements IValueObjectMaterializer + nested ValidatedOverPrivateNamesJsonConverter : JsonConverter, IStoredJsonConverterSource + nested ValidatedOverPrivateNamesTypeConverter : TypeConverter + static Create(Int32) : Result + static FromKnownGood(Int32, String) : ValidatedOverPrivateNames + static Parse(ReadOnlySpan, IFormatProvider) : ValidatedOverPrivateNames + static Parse(ReadOnlySpan, IFormatProvider) : ValidatedOverPrivateNames + static Parse(String, IFormatProvider) : ValidatedOverPrivateNames + static TryFrom(Int32) : Option + static TryParse(ReadOnlySpan, IFormatProvider, out ValidatedOverPrivateNames) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out ValidatedOverPrivateNames) : Boolean + static TryParse(String, IFormatProvider, out ValidatedOverPrivateNames) : Boolean + static op_Equality(ValidatedOverPrivateNames, ValidatedOverPrivateNames) : Boolean + static op_GreaterThan(ValidatedOverPrivateNames, ValidatedOverPrivateNames) : Boolean + static op_GreaterThanOrEqual(ValidatedOverPrivateNames, ValidatedOverPrivateNames) : Boolean + static op_Inequality(ValidatedOverPrivateNames, ValidatedOverPrivateNames) : Boolean + static op_LessThan(ValidatedOverPrivateNames, ValidatedOverPrivateNames) : Boolean + static op_LessThanOrEqual(ValidatedOverPrivateNames, ValidatedOverPrivateNames) : Boolean + extension static ValidatedOverPrivateNamesExtensions.GetValue(ValidatedOverPrivateNames) : Int32 + extension static ValidatedOverPrivateNamesExtensions.ValueOrNull(ValidatedOverPrivateNames) : Int32? diff --git a/test/CodoMetis.TypeKit.Generators.Tests/InheritedMembersTests.cs b/test/CodoMetis.TypeKit.Generators.Tests/InheritedMembersTests.cs index 3ab3237..a193896 100644 --- a/test/CodoMetis.TypeKit.Generators.Tests/InheritedMembersTests.cs +++ b/test/CodoMetis.TypeKit.Generators.Tests/InheritedMembersTests.cs @@ -88,6 +88,70 @@ public sealed partial class MaskedSet : LocalMaskingSetBase, CodoMetis.TypeKit.C public static readonly MaskedSet Closed = new(); } +/// +/// A base record whose private members have the generated members' names and signatures. C# hides them from a +/// derived record, so they stand in for nothing: 2.3.0 generated the value object beside them, and 2.3.1's +/// base-record checks refused it (CMTK1011, CMTK1008, #230). +/// +public abstract record PrivateNamesBase where TSelf : notnull +{ + private static int MaxValue => 100; + private static int MinValue => 0; + private int Value => -1; + private static TSelf From(int value) => throw new NotSupportedException("hidden"); + private static TSelf Parse(string text, IFormatProvider? provider) => throw new NotSupportedException("hidden"); + private static bool TryParse(string? text, IFormatProvider? provider, out TSelf result) => throw new NotSupportedException("hidden"); + private static TSelf Parse(ReadOnlySpan text, IFormatProvider? provider) => throw new NotSupportedException("hidden"); + private static bool TryParse(ReadOnlySpan text, IFormatProvider? provider, out TSelf result) => throw new NotSupportedException("hidden"); + private string ToString(string? format, IFormatProvider? provider) => "hidden"; + private bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) => throw new NotSupportedException("hidden"); + private int CompareTo(object? other) => 0; + private int CompareTo(TSelf? other) => 0; + private static Option TryFrom(int value) => throw new NotSupportedException("hidden"); + private Result Revalidate() => throw new NotSupportedException("hidden"); + + /// What the base uses its private members for. + protected bool InRange(int value) => value >= MinValue && value <= MaxValue && Value < 0 && CompareTo(null) == 0 && ToString(null, null) == "hidden"; +} + +/// Why a value object over is refused. +public enum PrivateNamesFault +{ + /// Below zero. + Negative +} + +/// A plain value object over a base record's private members of generated names. +public sealed partial record OverPrivateNames : PrivateNamesBase, IValue; + +/// A validated value object over a base record's private TryFrom and Revalidate. +public sealed partial record ValidatedOverPrivateNames : PrivateNamesBase, IValidatedValue +{ + /// Zero or more. + public static Result Create(int value) => value < 0 ? Result.Error(PrivateNamesFault.Negative) : new ValidatedOverPrivateNames(value); +} + +/// A base class whose private members have a closed set's generated names and signatures (#230). +public abstract class PrivateSetNamesBase +{ + private static TSelf Parse(string name) => throw new NotSupportedException("hidden"); + private static bool TryParse(string? name, out TSelf member) => throw new NotSupportedException("hidden"); + private TResult Match(Func first, Func second) => throw new NotSupportedException("hidden"); + + /// What the base uses its private members for. + protected static bool Parses(string name) => TryParse(name, out _) || Parse(name) is not null || ((PrivateSetNamesBase)null!).Match(() => 0, () => 1) == 0; +} + +/// A closed set over a base class's private members of generated names. +public sealed partial class SetOverPrivateNames : PrivateSetNamesBase, CodoMetis.TypeKit.ClosedSets.IClosedSet +{ + /// The first member. + public static readonly SetOverPrivateNames First = new(); + + /// The second member. + public static readonly SetOverPrivateNames Second = new(); +} + /// /// What a value object inherits from a base record. Refusals are build errors and live in /// ; this is what is generated. @@ -203,6 +267,53 @@ public void A_sealed_ToString_in_a_base_record_is_the_seam(object value) TypeDescriptor.GetConverter(value.GetType()).ConvertToInvariantString(value).ShouldBe("SECRET"); } + /// + /// A base record's private members stand in for nothing: every generated member is there, and is what a call + /// on the value object reaches (#230). + /// + [Fact] + public void A_base_record_s_private_members_of_generated_names_are_hidden() + { + OverPrivateNames.MaxValue.Value.ShouldBe(int.MaxValue); + OverPrivateNames.MinValue.Value.ShouldBe(int.MinValue); + OverPrivateNames.From(7).Value.ShouldBe(7); + OverPrivateNames.Parse("7", null).ShouldBe(OverPrivateNames.From(7)); + OverPrivateNames.TryParse("7", null, out var parsed).ShouldBeTrue(); + parsed.ShouldBe(OverPrivateNames.From(7)); + OverPrivateNames.Parse("7".AsSpan(), null).ShouldBe(OverPrivateNames.From(7)); + OverPrivateNames.TryParse("7"u8, null, out var utf8).ShouldBeTrue(); + utf8.ShouldBe(OverPrivateNames.From(7)); + OverPrivateNames.From(7).ToString("D3", null).ShouldBe("007"); + + Span buffer = stackalloc char[8]; + OverPrivateNames.From(7).TryFormat(buffer, out var written, "D3", null).ShouldBeTrue(); + buffer[..written].ToString().ShouldBe("007"); + + (OverPrivateNames.From(1) < OverPrivateNames.From(2)).ShouldBeTrue(); + OverPrivateNames.From(1).CompareTo((object)OverPrivateNames.From(2)).ShouldBeLessThan(0); + typeof(OverPrivateNames).IsDefined(typeof(CodoMetis.TypeKit.CompilerServices.HandWrittenOrderingAttribute), inherit: false).ShouldBeFalse(); + } + + /// A base record's private TryFrom and Revalidate are no seams: the generated ones run Create (#230). + [Fact] + public void A_base_record_s_private_TryFrom_and_Revalidate_are_hidden() + { + ValidatedOverPrivateNames.TryFrom(50).IsSome().ShouldBeTrue(); + ValidatedOverPrivateNames.TryFrom(-1).IsNone().ShouldBeTrue(); + ValidatedOverPrivateNames.FromKnownGood(50).Revalidate().TryGetValue(out var revalidated, out _).ShouldBeTrue(); + revalidated.ShouldBe(ValidatedOverPrivateNames.FromKnownGood(50)); + } + + /// A base class's private Parse, TryParse and Match are hidden from a closed set too (#230). + [Fact] + public void A_closed_set_s_base_class_s_private_members_of_generated_names_are_hidden() + { + SetOverPrivateNames.Parse("Second").ShouldBeSameAs(SetOverPrivateNames.Second); + SetOverPrivateNames.TryParse("First", out var member).ShouldBeTrue(); + member.ShouldBeSameAs(SetOverPrivateNames.First); + SetOverPrivateNames.Second.Match(first: () => 1, second: () => 2).ShouldBe(2); + } + /// /// A base class's sealed ToString() is a closed set's seam too (#205): it is what the set prints, while /// its name, parsing and JSON are generated as usual. diff --git a/test/oracles/claims.json b/test/oracles/claims.json index 41773fc..b0be1f3 100644 --- a/test/oracles/claims.json +++ b/test/oracles/claims.json @@ -1209,6 +1209,21 @@ "BuildOutcomeTests" ] }, + { + "id": "C22-a-base-s-private-member", + "source": "src/CodoMetis.TypeKit.Generators/README.md#write-your-own-members", + "quote": "A base's `private` member is fine: the value object can't see it, so it stands in for nothing.", + "claim": "A base record's private member of a generated member's name or signature does not fail the build: the value object is generated beside it, and every generated member is what a call reaches; a protected, internal or public one is refused.", + "oracle": "claim", + "surfaces": [ + "IPlainValueObject.*", + "IValidatedValue.*" + ], + "heldBy": [ + "BuildOutcomeTests", + "InheritedMembersTests" + ] + }, { "id": "C22-a-contract-through-an-interface", "source": "src/CodoMetis.TypeKit.Generators/README.md#write-your-own-members", @@ -1251,6 +1266,20 @@ "BuildOutcomeTests" ] }, + { + "id": "C22-a-set-s-base-class-private-member", + "source": "src/CodoMetis.TypeKit.Generators/README.md#closed-sets", + "quote": "A `private` one in that class is fine, since the set can't see it.", + "claim": "A closed set's base class's private Parse, TryParse or Match does not fail the build, and the set's generated members are what a call reaches; a protected, internal or public one is refused.", + "oracle": "claim", + "surfaces": [ + "IClosedSet.*" + ], + "heldBy": [ + "BuildOutcomeTests", + "InheritedMembersTests" + ] + }, { "id": "C22-a-set-s-base-class-tostring", "source": "src/CodoMetis.TypeKit.Generators/README.md#closed-sets",