Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<OrderId>.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<TFormat>(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<TFormat>()`, 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

Expand Down
4 changes: 3 additions & 1 deletion design/aspects.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions src/CodoMetis.TypeKit.Generators/ClosedSetTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@ private static string WithoutComments(string text)
/// <para>
/// A base class's count as the set's own (#205): its <c>Parse</c>, <c>TryParse</c>, <c>Match</c> or <c>All</c>,
/// and an <c>IParsable&lt;TSelf&gt;</c> 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 <c>_name</c> 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 <c>_name</c> field or <c>Parse(string)</c>, is out of its sight and builds (#230).
/// </para>
/// </remarks>
public static IReadOnlyList<string> GeneratedMembersDeclaredByHand(INamedType set, IReadOnlyList<IField> members)
Expand All @@ -212,15 +212,15 @@ public static IReadOnlyList<string> GeneratedMembersDeclaredByHand(INamedType se
var bases = ValueObjectDeclaration.SelfAndBases(set).Skip(1).ToList();

IEnumerable<string> Declared(Func<INamedType, IEnumerable<IMethod>> 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()}"));

List<string> found =
[
.. ValueObjectDeclaration.MembersNamed(set, methods: false, [.. generatedNames]).Where(declared => !memberNames.Contains(declared)),
.. bases.SelectMany(type => type.Fields.Concat<IMember>(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)))),
Expand Down
8 changes: 5 additions & 3 deletions src/CodoMetis.TypeKit.Generators/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -471,8 +472,9 @@ public sealed partial class OrderStatus : IClosedSet<OrderStatus>, IParsable<Ord
(CMTK1021).
- **A `ToString()` you write is kept**, and the name is still what JSON, parsing and the column use.
- **An overload of a generated method with another signature**, such as `Parse(int rank)` or a
generic `Parse<TFormat>(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<TFormat>(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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,14 @@ private static IEnumerable<string> 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()}";
}
Expand Down
Loading
Loading