From c1c6e3d188be253498b9cd8ee1f6d98b498f933f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Gro=C3=9F?= Date: Sat, 31 Oct 2026 08:43:35 +0100 Subject: [PATCH] fix: a base's private member of a generated name is hidden from the value object, not refused 2.3.1's base checks read every member of every base at any accessibility: Declared over SelfAndBases in ValueObjectDeclaration.GeneratedMembersDeclaredByHand (#197), InheritedSeams (#195) and the base loop of ValueObjectComparableAspect.HandWrittenComparisonMembers (#194), and, from #218, the method lines of ClosedSetTypes.GeneratedMembersDeclaredByHand (#205). C# hides a base's private member from the derived type, so it stands in for no generated member: a base record's private MaxValue, Parse, TryFrom or CompareTo(object) failed the build with CMTK1011 or CMTK1008, where 2.3.0 generated the value object, and a set's base class's private Parse(string) with CMTK1011. Each base check now counts only a member the type can see (IsAccessibleFrom), on the value object and on the closed set, whose field and property line read Accessibility != Private and now reads the same. A value object nested in its base sees the base's private members, as C# does, and stays refused; a library's private and internal ones were never listed by Metalama and still build. Input class (GTS): a base member of a generated member's name or signature, by accessibility. BaseMemberAccessibility is the catalogue: private, private protected, protected, internal, protected internal and public, by each name the checks read (Value, From, MinValue, MaxValue, Parse and TryParse over string, span and UTF-8, ToString(string?, IFormatProvider?), TryFormat, CompareTo(object), CompareTo(TSelf), TryFrom, Revalidate) and a closed set's Parse, TryParse and Match: 108 types in the consumer of BuildOutcomeTests. The 19 private ones build, the 80 others and the nested one are refused naming the base, and a library's hidden members build. InheritedMembersTests holds what the private ones generate. Reverting the source fails the 19 private rows and refuses the three types of InheritedMembersTests in the test project's own build; counting only a base's public members fails the 76 protected and internal rows and the nested one. No sibling outside these checks: the other base reads (equality, PrintMembers, an unsealed ToString()) match members C# requires to be public or protected in a record. Closes #230 Defect-Class: GTS Found-By: CA Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + design/aspects.md | 4 +- .../ClosedSetTypes.cs | 8 +- src/CodoMetis.TypeKit.Generators/README.md | 8 +- .../ValueObjectComparableAspect.cs | 5 +- .../ValueObjectDeclaration.cs | 67 +++--- .../BaseMemberAccessibility.cs | 203 ++++++++++++++++++ .../BuildOutcomeTests.cs | 6 +- .../GeneratedSurface.verified.txt | 127 +++++++++++ .../InheritedMembersTests.cs | 111 ++++++++++ test/oracles/claims.json | 29 +++ 11 files changed, 527 insertions(+), 42 deletions(-) create mode 100644 test/CodoMetis.TypeKit.Generators.Tests/BaseMemberAccessibility.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index acdc62f..830847f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ section is the GitHub release's notes. - Generators: `IValueObject`, `IValueObjectMaterializer` or `IPlainValueObject` declared through an interface of yours, such as an `IEntityId : IValueObject`, or by a base record now fails with CMTK1011 naming that interface, where it failed with a Metalama error (LAMA0512) that named nothing you wrote. To constrain generic code, use `IValueObject` itself ([Write your own members](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.Generators/README.md#write-your-own-members)). - 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)). ## 2.3.0 — 2026-10-29 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 119cf03..3403541 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",