diff --git a/tools/probe/corpus/claims.json b/tools/probe/corpus/claims.json index 5ca52131..d7d6d4ae 100644 --- a/tools/probe/corpus/claims.json +++ b/tools/probe/corpus/claims.json @@ -3648,6 +3648,49 @@ "abstract-string-ctor" ] } + }, + { + "id": "parsing-where-it-can-be-parsed", + "sentence": "`IParsable`/`ISpanParsable`/`IUtf8SpanParsable`, formatting in the invariant culture", + "axes": { + "parse-shape": [ + "ordinary" + ] + }, + "inapplicable": [ + { + "axes": { + "shape.wrapped": [ + "Uri", + "enum", + "flags-enum", + "NodaTime.Instant" + ], + "text.entry": [ + "ISpanParsable" + ] + }, + "cite": "not-promised:no-span-parsing-without-the-underlying-types" + } + ] + } + ], + "not-promised": [ + { + "id": "no-span-parsing-without-the-underlying-types", + "source": "src/CodoMetis.TypeKit.Generators/README.md#what-is-generated", + "sentence": "A `Uri`, an enum and a NodaTime type have neither.", + "axes": { + "shape.wrapped": [ + "Uri", + "enum", + "flags-enum", + "NodaTime.Instant" + ], + "text.entry": [ + "ISpanParsable" + ] + } } ], "exclude": [ @@ -6323,6 +6366,27 @@ "text.entry=type-converter" ] ] + }, + { + "reason": "A `Uri`, an enum and a NodaTime type have neither (Generators README, What is generated: no ISpanParsable or IUtf8SpanParsable where the underlying type has none, not-promised no-span-parsing-without-the-underlying-types), so parsing-where-it-can-be-parsed is inapplicable there and no check that reads shape.wrapped can credit them.", + "pairs": [ + [ + "shape.wrapped=NodaTime.Instant", + "text.entry=ISpanParsable" + ], + [ + "shape.wrapped=Uri", + "text.entry=ISpanParsable" + ], + [ + "shape.wrapped=enum", + "text.entry=ISpanParsable" + ], + [ + "shape.wrapped=flags-enum", + "text.entry=ISpanParsable" + ] + ] } ] }, @@ -6776,6 +6840,15 @@ "default" ] } + }, + { + "id": "a-member-is-silent", + "sentence": "public static readonly ImportFault Duplicate = new(FaultKind.Conflict);", + "axes": { + "value": [ + "valid" + ] + } } ], "exclude": [ diff --git a/tools/probe/corpus/harness/BuildRoslyn.cs b/tools/probe/corpus/harness/BuildRoslyn.cs index f5c7e6f0..2f3584d1 100644 --- a/tools/probe/corpus/harness/BuildRoslyn.cs +++ b/tools/probe/corpus/harness/BuildRoslyn.cs @@ -90,14 +90,22 @@ public static async Task Analyze(string id, IEnumerable<(string File, /// Compares the reports with the marks: every mark in reported exactly once at its span, nothing /// reported anywhere else. Null when they agree, else what differs. /// - public static string? Agree(Analysis a, IReadOnlySet expected, Func? message = null) + public static string? Agree(Analysis a, IReadOnlySet expected, Func? message = null) => + Agree(a, expected, _ => true, outside: true, message); + + /// + /// The same over the marks selects, one check's share of a compilation: each of them in + /// reported exactly once at its span, the others silent, and with , nothing + /// reported outside every mark. A report at a mark the check does not judge is another check's. + /// + public static string? Agree(Analysis a, IReadOnlySet expected, Func judged, bool outside, Func? message = null) { var marks = a.Marks.ToList(); foreach (var label in expected) if (!marks.Any(m => m.Label == label)) throw new InvalidOperationException($"no mark {label}"); var problems = new List(); - foreach (var m in marks) + foreach (var m in marks.Where(judged)) { - var at = a.Reported.Where(d => d.Location.SourceTree?.FilePath == m.File && d.Location.SourceSpan == m.Span).ToList(); + var at = At(a, m); if (expected.Contains(m.Label)) { if (at.Count != 1) problems.Add($"{m.Label} '{m.Text}': reported {at.Count} times, expected once"); @@ -105,9 +113,55 @@ public static async Task Analyze(string id, IEnumerable<(string File, } else if (at.Count != 0) problems.Add($"{m.Label} '{m.Text}': reported, expected silent ({at[0].GetMessage()})"); } - foreach (var d in a.Reported) - if (!marks.Any(m => d.Location.SourceTree?.FilePath == m.File && d.Location.SourceSpan == m.Span)) - problems.Add($"reported outside every mark at {Analysis.Where(d)}: {d.GetMessage()}"); + if (outside) problems.AddRange(Outside(a).Select(d => $"reported outside every mark at {Analysis.Where(d)}: {d.GetMessage()}")); + return problems.Count == 0 ? null : string.Join("; ", problems); + } + + /// The reports exactly at a mark's span. + public static List At(Analysis a, Mark m) => + [.. a.Reported.Where(d => d.Location.SourceTree?.FilePath == m.File && d.Location.SourceSpan == m.Span)]; + + /// The reports whose span holds a mark's: at it, or at the syntax around it. + public static List Around(Analysis a, Mark m) => + [.. a.Reported.Where(d => d.Location.SourceTree?.FilePath == m.File && d.Location.SourceSpan.Contains(m.Span))]; + + /// The reports whose span holds no mark. + public static List Outside(Analysis a) => + [.. a.Reported.Where(d => !a.Marks.Any(m => d.Location.SourceTree?.FilePath == m.File && d.Location.SourceSpan.Contains(m.Span)))]; + + /// + /// Whether a value is reported, wherever around it: each mark names that is in + /// has exactly one report at or around it, the others none. Where the report sits is 's question, so a + /// report moved to the syntax around its value still counts here. Null when they agree, else what differs. + /// + public static string? Reported(Analysis a, IEnumerable judged, IReadOnlySet expected, Func? message = null) + { + var problems = new List(); + foreach (var label in judged) + { + var m = a.Marks.SingleOrDefault(x => x.Label == label) ?? throw new InvalidOperationException($"no mark {label}"); + var around = Around(a, m); + if (expected.Contains(label)) + { + if (around.Count != 1) problems.Add($"{m.Label} '{m.Text}': reported {around.Count} times, expected once"); + else if (message?.Invoke(around[0], m) is { } bad) problems.Add($"{m.Label} '{m.Text}': {bad}"); + } + else if (around.Count != 0) problems.Add($"{m.Label} '{m.Text}': reported, expected silent ({around[0].GetMessage()})"); + } + return problems.Count == 0 ? null : string.Join("; ", problems); + } + + /// + /// Where the reports sit: every report at or around a mark in is at the mark's own span, the value as + /// written, and none is outside every mark. Null when so, else what differs. + /// + public static string? WhereWritten(Analysis a, IReadOnlySet expected) + { + var problems = new List(); + foreach (var m in a.Marks.Where(m => expected.Contains(m.Label))) + foreach (var d in Around(a, m).Where(d => d.Location.SourceSpan != m.Span)) + problems.Add($"{m.Label} '{m.Text}': reported at {Analysis.Where(d)}, not at the value"); + problems.AddRange(Outside(a).Select(d => $"reported outside every mark at {Analysis.Where(d)}: {d.GetMessage()}")); return problems.Count == 0 ? null : string.Join("; ", problems); } } diff --git a/tools/probe/corpus/harness/C11Shapes.g.cs b/tools/probe/corpus/harness/C11Shapes.g.cs index 49ab4f20..992834fa 100644 --- a/tools/probe/corpus/harness/C11Shapes.g.cs +++ b/tools/probe/corpus/harness/C11Shapes.g.cs @@ -2282,6 +2282,65 @@ public sealed partial record V239 : B239_1, IValue { } public abstract record B240_1 { } public sealed partial record V240 : B240_1, IValue { } +// row 241 +public static partial class O241 +{ +public sealed partial class P241 +{ +public interface IMark241 { int Mark(); } +public sealed partial record V241 : IValidatedValue, IMark241 +{ + public static Result Create(global::Probes.Shapes.PEnum value) => new V241(value); int IMark241.Mark() => 1; +} +} +} + +// row 242 +public partial struct O242 +{ +public abstract record B242_3 { } +public abstract record B242_2 : B242_3 { } +public abstract record B242_1 : B242_2 { } +public sealed partial record V242 : B242_1, IValue { } +} + +// row 243 +public readonly partial record struct V243 : IValue { } + +// row 244 +public static partial class O244 +{ +public sealed partial class P244 +{ +public readonly partial record struct V244 : IValidatedValue +{ + public static Result Create(global::NodaTime.Instant value) => new V244(value); +} +} +} + +// row 245 +public partial struct O245 +{ +public interface IMark245 { int Mark(); } +public readonly partial record struct V245 : IValue, IMark245 { int IMark245.Mark() => 1; } +} + +// row 246 +public static partial class O246 +{ +public sealed partial class P246 +{ +public abstract record B246_3 { } +public abstract record B246_2 : B246_3 { } +public abstract record B246_1 : B246_2 { } +public sealed partial record V246 : B246_1, IValidatedValue +{ + public static Result Create(global::System.Uri value) => new V246(value); +} +} +} + public static partial class C11Table { public static readonly C11Case[] Cases = @@ -2527,5 +2586,11 @@ public static partial class C11Table new(238, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-two-levels;shape.wrapped=ulong;text.culture=de-DE;text.entry=TryFormat-short-span", "sealed-record-class", "nested-two-levels", "base-depth-1", typeof(O238.P238.V238), typeof(ulong), null, null, []), new(239, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=double;text.culture=de-DE;text.entry=IParsable", "sealed-record-class", "top-level", "base-depth-1", typeof(V239), typeof(double), null, null, []), new(240, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=byte;text.culture=ar-SA;text.entry=TryFrom", "sealed-record-class", "top-level", "base-depth-1", typeof(V240), typeof(byte), null, null, []), + new(241, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-two-levels;shape.wrapped=enum;text.culture=de-DE;text.entry=IParsable", "validated-class", "nested-two-levels", "explicit-interface-member", typeof(O241.P241.V241), typeof(global::Probes.Shapes.PEnum), null, null, []), + new(242, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=flags-enum;text.culture=ar-SA;text.entry=TryFrom", "sealed-record-class", "nested-in-struct", "base-depth-3", typeof(O242.V242), typeof(global::Probes.Shapes.PFlags), null, null, []), + new(243, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-3;shape.kind=record-struct;shape.refused-kind=closed-set-named-as-generated-member (CMTK1021);shape.site=top-level;shape.wrapped=Uri;text.culture=ar-SA;text.entry=IParsable", "record-struct", "top-level", "none", typeof(V243), typeof(global::System.Uri), null, null, [("shape.inheritance", "none")]), + new(244, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=none;shape.kind=validated-struct;shape.refused-kind=equality-operator (CMTK1011);shape.site=nested-two-levels;shape.wrapped=NodaTime.Instant;text.culture=ar-SA;text.entry=TryFormat-short-span", "validated-struct", "nested-two-levels", "none", typeof(O244.P244.V244), typeof(global::NodaTime.Instant), null, null, []), + new(245, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=record-struct;shape.refused-kind=over-object (CMTK1005);shape.site=nested-in-struct;shape.wrapped=Uri;text.culture=invariant;text.entry=TryFormat-short-span", "record-struct", "nested-in-struct", "explicit-interface-member", typeof(O245.V245), typeof(global::System.Uri), null, null, []), + new(246, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=over-System.Enum (CMTK1005);shape.site=nested-two-levels;shape.wrapped=Uri;text.culture=ar-SA;text.entry=TryParse", "validated-class", "nested-two-levels", "base-depth-3", typeof(O246.P246.V246), typeof(global::System.Uri), null, null, []), ]; } diff --git a/tools/probe/corpus/probes/C11ParseShapesBuildProbe.cs b/tools/probe/corpus/probes/C11ParseShapesBuildProbe.cs index 169a27cd..7a05c281 100644 --- a/tools/probe/corpus/probes/C11ParseShapesBuildProbe.cs +++ b/tools/probe/corpus/probes/C11ParseShapesBuildProbe.cs @@ -2,13 +2,23 @@ // takes its text by `ref`, or an abstract class's string constructor, no longer breaks the build; the value object has no parsing." // With Generators README "What you can wrap" ("Parsing where it can be parsed") and "What is generated" (TryFrom for IValidatedValue, // the [TypeConverter] when parsing is generated, ToString() the underlying type's own text under the invariant culture). -// Oracle: claim; for the ordinary shape, differential against the catalogue's top-level value object of the same kind and wrapped type -// (Probes.Shapes.Registry). Each row's value object (harness/C11Shapes.g.cs, generated from this claim's rows) is of shape.kind at -// shape.site with shape.inheritance, over the parse shape's own class (implicit-result: Parse returns an int; ref-text: Parse or the -// string constructor takes the text by ref; abstract-string-ctor) or, for ordinary, over shape.wrapped. That it is in this assembly is -// the build half. Under text.culture and process.converter-lookup, the row's text.entry is then checked: over a parse shape, no -// Parse, TryParse, IParsable, ISpanParsable or string type converter, while TryFrom (validated) and formatting work; over the ordinary -// shape, the entry exists exactly where the catalogue's does, parses where the wrapped type parses, and gives the catalogue's result. +// Obligations (claims.json), each with a check below and a seed of its own in the registry entry (decision 104), the row's parse-shape +// selecting one: +// implicit-result: "A wrapped type's `Parse` whose result can't be assigned to it" +// ref-text: "or that takes its text by `ref`" +// abstract-string-constructor: "or an abstract class's string constructor, no longer breaks the build; the value object has no parsing." +// parsing-where-it-can-be-parsed (ordinary): "`IParsable`/`ISpanParsable`/`IUtf8SpanParsable`, formatting in the invariant culture" +// (the 2.0.0 entry of the same file: what the fix must leave to a type that parses) +// Each row's value object (harness/C11Shapes.g.cs, generated from this claim's rows) is of shape.kind at shape.site with +// shape.inheritance, over the parse shape's own class (implicit-result: Parse returns an int; ref-text: Parse or the string constructor +// takes the text by ref; abstract-string-ctor) or, for ordinary, over shape.wrapped. That it is in this assembly is the build half. +// Under text.culture and process.converter-lookup, the row's text.entry is then checked: +// a parse shape's obligation, against the CHANGELOG's sentence (Baseline.Docs): no Parse, TryParse, IParsable, ISpanParsable or +// string type converter, while TryFrom (validated) and formatting work, and the same declaration over the nearest type that parses +// (its control) parses; +// parsing-where-it-can-be-parsed, against the catalogue's top-level value object of the same kind over the same wrapped type +// (Probes.Shapes.Registry, Baseline.Twin): the entry exists exactly where the catalogue's does and wherever the wrapped type parses, +// ISpanParsable included, and gives the catalogue's result. using System.ComponentModel; using System.Globalization; using System.Reflection; @@ -49,10 +59,17 @@ public sealed class C11ParseShapesBuildProbe if (!c.Wrapped.IsInstanceOfType(sample)) { r.Outcome("harness-error", $"the sample is no {c.Wrapped.Name}"); return null; } var e = new Entry(c.Kind, wrapped ?? shape, c.Vo, c.Wrapped, () => sample, () => null, typeof(object), typeof(object), typeof(object), typeof(object)); TextOps.Prepare(r, e); - var vo = Make.Vo(e, sample); - if (!Equals(Make.Unwrap(e, vo), sample)) return $"the value object does not hold the value it was made from"; - if (ordinary) return Ordinary(r, e, entry, wrapped!, sample, vo); - return NoParsing(r, e, entry, sample, vo) ?? Control(c); + string? Holds(object vo) => Equals(Make.Unwrap(e, vo), sample) ? null : "the value object does not hold the value it was made from"; + if (ordinary) + return r.Check("parsing-where-it-can-be-parsed", Baseline.Twin, _ => { var vo = Make.Vo(e, sample); return Holds(vo) ?? Ordinary(r, e, entry, wrapped!, sample, vo); }); + var obligation = shape switch + { + "implicit-result" => "implicit-result", + "ref-text" => "ref-text", + "abstract-string-ctor" => "abstract-string-constructor", + _ => throw new ArgumentException(shape), + }; + return r.Check(obligation, Baseline.Docs("CHANGELOG.md#2.1.0-fixed"), _ => { var vo = Make.Vo(e, sample); return Holds(vo) ?? NoParsing(r, e, entry, sample, vo) ?? Control(c); }); } /// @@ -153,7 +170,7 @@ public sealed class C11ParseShapesBuildProbe { var text = TextOps.PlainText(reference, sample); var mine = TextOps.Read(e, entry, text); - if (mine.NotApplicable && wrapped != "own-record-class" && entry is not "ISpanParsable") + if (mine.NotApplicable && wrapped != "own-record-class") return $"{entry}: the value object over {wrapped}, which parses, has no {entry}"; fail = Same(mine, TextOps.Read(reference, entry, text), e, reference, entry); } diff --git a/tools/probe/corpus/probes/C17Cmtk0014Probe.cs b/tools/probe/corpus/probes/C17Cmtk0014Probe.cs index c9547454..629484f6 100644 --- a/tools/probe/corpus/probes/C17Cmtk0014Probe.cs +++ b/tools/probe/corpus/probes/C17Cmtk0014Probe.cs @@ -1,13 +1,26 @@ // Claim C17-cmtk0014 (src/CodoMetis.TypeKit.Analyzers/README.md#cmtk0014): "A FaultKind or a ConstructorAccessibility that names // no member: a cast, default(FaultKind), a zero, or a folded constant" is reported (Warning), and "Both are reported where the value // is written". The documented cases: `[RequireParameterlessConstructor((ConstructorAccessibility)7)]` and a fault set's member -// `new(default)`; "a default that is a member is silent" (ConstructorAccessibility.Public is 0). -// Oracle: claim. Each row writes the value (value axis) in the written form (site axis) at every place its shape axes describe, each -// place marked: in the declaration of shape.kind over shape.wrapped at shape.site (a fault set's member for closed-set, a static member -// of a value object otherwise), in the topmost base of its shape.inheritance chain and in an explicit interface member, in a member of a -// shape.containing type at shape.position, and in a declaration of shape.refused-kind (which compiles here, with no generators; the two -// kinds that do not are excluded). An attribute-argument row also writes the documented RequireParameterlessConstructor case. Every -// place of an undefined value is reported exactly once at the value, naming its enum; a valid one nowhere; nothing else is reported. +// `new(default)`, beside `new(FaultKind.Conflict)`, a member, which is silent; "a default that is a member is silent" +// (ConstructorAccessibility.Public is 0). +// Obligations (claims.json), each with a check below and a seed of its own in the registry entry (decision 104), the row's value +// and site selecting them: +// a-cast-naming-no-member (value (FaultKind)9): "A `FaultKind` or a `ConstructorAccessibility` that names no member: a cast" +// default-and-zero (value default): "`default(FaultKind)`, a zero" +// constructor-accessibility-too (value (FaultKind)9, site attribute-argument): "[RequireParameterlessConstructor((ConstructorAccessibility)7)] // CMTK0014: no member, read as public" +// reported-where-written (value (FaultKind)9 or default): "Both are reported where the value is written:" +// a-member-is-silent (value valid): "public static readonly ImportFault Duplicate = new(FaultKind.Conflict);" +// Oracle: the README's section (Baseline.Docs). Each row writes the value (value axis) in the written form (site axis) at every place +// its shape axes describe, each place marked: in the declaration of shape.kind over shape.wrapped at shape.site (a fault set's member +// for closed-set, a static member of a value object otherwise), in the topmost base of its shape.inheritance chain and in an explicit +// interface member, in a member of a shape.containing type at shape.position, and in a declaration of shape.refused-kind (which +// compiles here, with no generators; the two kinds that do not are excluded). An attribute-argument row also writes the documented +// RequireParameterlessConstructor case. One compilation per row, and every check judges its share of it: +// a-cast-naming-no-member, default-and-zero: each FaultKind place of an undefined value is reported once, at or around the value, +// naming FaultKind (default-and-zero also judges RequireParameterlessConstructor's default, Public, silent); +// constructor-accessibility-too: RequireParameterlessConstructor's (ConstructorAccessibility)9 is reported, naming its enum; +// reported-where-written: every report sits at the value's own span, and none outside every place; +// a-member-is-silent: no place of a value that names a member is reported, and nothing is reported anywhere. using Probes.Build; using Probes.Shapes; namespace Probes; @@ -161,12 +174,43 @@ public sealed partial class D{{id}} : {{baseList}}IFaultSet{{mark}} var run = await Rz.Analyze("CMTK0014", [("Row" + id + ".cs", source)]); if (run.Errors.Count > 0) { r.Outcome("harness-error", "the probe's source does not compile: " + run.ErrorText()); return null; } if (run.AnalyzerFailures.Count > 0) return "the analyzer failed: " + string.Join("; ", run.AnalyzerFailures.Select(f => f.GetMessage())); - r.Note(NoteKeys.Compared, $"{p.Labels.Count} places, {p.Expected.Count} expected reported"); - return Rz.Agree(run, p.Expected, (d, m) => + + // Every check the row's value selects runs, so a seed that breaks one obligation is seen by its own check whatever the others find. + var readme = Baseline.Docs("src/CodoMetis.TypeKit.Analyzers/README.md#cmtk0014"); + var kinds = p.Labels.Where(l => l.StartsWith('p')).ToList(); + var accessibilities = p.Labels.Where(l => l.StartsWith("ca", StringComparison.Ordinal)).ToList(); + var failures = new List(); + switch (value) { - var type = m.Label.StartsWith("ca") ? "ConstructorAccessibility" : "FaultKind"; - return d.GetMessage().Contains($"names no member of {type}") ? null : $"the report does not name {type}: {d.GetMessage()}"; - }); + case "(FaultKind)9": + failures.Add(r.Check("a-cast-naming-no-member", readme, c => Judged(c, Rz.Reported(run, kinds, p.Expected, Names), $"{kinds.Count} FaultKind places reported"))); + if (site == "attribute-argument") + failures.Add(r.Check("constructor-accessibility-too", readme, c => Judged(c, Rz.Reported(run, accessibilities, p.Expected, Names), "RequireParameterlessConstructor's (ConstructorAccessibility)9 reported"))); + failures.Add(r.Check("reported-where-written", readme, c => Judged(c, Rz.WhereWritten(run, p.Expected), $"{p.Expected.Count} reports at the value"))); + break; + case "default": + failures.Add(r.Check("default-and-zero", readme, c => Judged(c, Rz.Reported(run, p.Labels, p.Expected, Names), $"{kinds.Count} FaultKind places reported, {accessibilities.Count} default ConstructorAccessibility silent"))); + failures.Add(r.Check("reported-where-written", readme, c => Judged(c, Rz.WhereWritten(run, p.Expected), $"{p.Expected.Count} reports at the value"))); + break; + case "valid": + failures.Add(r.Check("a-member-is-silent", readme, c => Judged(c, Rz.Reported(run, p.Labels, p.Expected) ?? Rz.WhereWritten(run, p.Expected), $"{p.Labels.Count} places silent"))); + break; + default: throw new ArgumentException("value " + value); + } + return failures.FirstOrDefault(f => f is not null); + } + + static string? Judged(Probes.Shapes.Check c, string? failure, string compared) + { + if (failure is null) c.Note(NoteKeys.Compared, compared); + return failure; + } + + /// A report names the enum of the place it is at: FaultKind, or ConstructorAccessibility for RequireParameterlessConstructor's. + static string? Names(Microsoft.CodeAnalysis.Diagnostic d, Mark m) + { + var type = m.Label.StartsWith("ca", StringComparison.Ordinal) ? "ConstructorAccessibility" : "FaultKind"; + return d.GetMessage().Contains($"names no member of {type}") ? null : $"the report does not name {type}: {d.GetMessage()}"; } static string Member((string E, string Attr) place, string name) => $"{place.Attr}public static FaultKind {name} => {place.E};"; diff --git a/tools/probe/corpus/probes/C18Cmtk1023Probe.cs b/tools/probe/corpus/probes/C18Cmtk1023Probe.cs index ae1d86fc..ba45b2fb 100644 --- a/tools/probe/corpus/probes/C18Cmtk1023Probe.cs +++ b/tools/probe/corpus/probes/C18Cmtk1023Probe.cs @@ -2,15 +2,25 @@ // generated. A base record declares `Equals(TBase)`, `GetHashCode()` or `IEquatable.Equals`, which the generated equality, the // underlying value's, doesn't call." With README#equality: "a hand-written `Equals(TBase)`, `GetHashCode()` or `IEquatable.Equals` // on a base record isn't used, and a warning names it (CMTK1023)". -// Oracle: claim. One consumer build (Probes.Build.Consumer) holds every row's declaration in a file of its own: a value object of -// shape.kind over shape.wrapped at shape.site, over a chain of base records (shape.inheritance: the base-member on the topmost of 1, 2 or 3 -// bases, or on the direct base beside an explicit IEquatable of its own or an explicit member of another interface), used at -// shape.position of a shape.containing type. Each file reports CMTK1023 once, as a warning, for that value object, naming each equality -// member its bases declare and nothing else; it reports no error; and the value object is generated (its From or TryFrom is in the -// built assembly). -// In this process too, the half of the sentence a run can show: over a base that declares the base-member by reference, the generated -// equality is the underlying value's (one value object per base-member, built by this project's own weaving). +// Obligations (claims.json), each with a check below and a seed of its own in the registry entry (decision 104); every row runs all three: +// names-each-base-member: "So a hand-written `Equals(TBase)`, `GetHashCode()` or `IEquatable.Equals` on a base record isn't used, and a warning names it (CMTK1023)." +// a-warning-and-generated: "A warning, and the value object is generated." +// generated-equality-never-calls-the-base: "Over a base record, the generated `Equals(TSelf)` and `GetHashCode()` compare the underlying value alone, and never call the base's." +// Oracle: the README's sentences (Baseline.Docs) for what the build reports, the wrapped values (Baseline.Twin) for equality. One +// consumer build (Probes.Build.Consumer) holds every row's declaration in a file of its own: a value object of shape.kind over +// shape.wrapped at shape.site, over a chain of base records (shape.inheritance: the base-member on the topmost of 1, 2 or 3 bases, or on +// the direct base beside an explicit IEquatable of its own or an explicit member of another interface), used at shape.position of a +// shape.containing type. +// names-each-base-member: the file reports CMTK1023 once, for that value object, naming each equality member its bases declare and +// nothing else; +// a-warning-and-generated: the file reports no error, CMTK1023 is a warning, and the value object is generated (its From or TryFrom +// is in the built assembly); +// generated-equality-never-calls-the-base: in this process, the half of the sentence a run can show: over a base that declares the +// base-member by reference, the generated equality is the underlying value's (one value object per base-member, built by this +// project's own weaving). It reads base-member alone, since the row's shape is not what it runs. +// The row's settings are read inside the checks that run them (the consumer's two), from the rows file's copy of the row. using Probes.Build; +using Baseline = Probes.Shapes.Baseline; using NoteKeys = Probes.Shapes.NoteKeys; namespace Probes; @@ -143,7 +153,10 @@ static string Container(string containing, string position, string name, string "top-level" => "", "nested-in-class" or "nested-in-struct" => $"O{id}+", "nested-two-levels" => $"O{id}+P{id}+", _ => throw new ArgumentException(s.Site), } + $"V{id}"; - /// Two value objects holding 42 over a base declaring the member by reference: equal, of one hash, and one holding 43 not. + /// + /// Two value objects holding 42 over a base declaring the member by reference, and one holding 43: equal, of one hash, and unequal, + /// as the wrapped values 42, 42 and 43 are. + /// static string? Twin(string member) { (object A, object B, object C, object BaseA, object BaseB) v = member switch @@ -153,9 +166,11 @@ static string Container(string containing, string position, string name, string "IEquatable" => (C18EquatableTwin.FromKnownGood(42), C18EquatableTwin.FromKnownGood(42), C18EquatableTwin.FromKnownGood(43), C18EquatableTwin.FromKnownGood(42), C18EquatableTwin.FromKnownGood(42)), _ => throw new ArgumentException(member), }; - if (!v.A.Equals(v.B) || v.A.GetHashCode() != v.B.GetHashCode()) return $"over a base declaring {member} by reference, two value objects holding 42 are unequal or hash apart: the generated equality called the base's"; - if (v.A.Equals(v.C)) return $"over a base declaring {member}, value objects holding 42 and 43 are equal"; - if (member == "IEquatable" && !((IEquatable)v.BaseA).Equals((C18EquatableBase)v.BaseB)) return "the base's explicit IEquatable compares two value objects holding 42 by reference"; + int a = 42, b = 42, c = 43; + if (v.A.Equals(v.B) != a.Equals(b) || (v.A.GetHashCode() == v.B.GetHashCode()) != (a.GetHashCode() == b.GetHashCode())) + return $"over a base declaring {member} by reference, two value objects holding 42 are unequal or hash apart: the generated equality called the base's"; + if (v.A.Equals(v.C) != a.Equals(c)) return $"over a base declaring {member}, value objects holding 42 and 43 are equal"; + if (member == "IEquatable" && ((IEquatable)v.BaseA).Equals((C18EquatableBase)v.BaseB) != a.Equals(b)) return "the base's explicit IEquatable compares two value objects holding 42 by reference"; return null; } @@ -164,8 +179,8 @@ static Task Built() => Consumer.Build(RowsFile, () => static async Task Check(Probes.Shapes.Row r) { - var s = Of(r); - foreach (var (axis, value, why) in s.Substituted) r.Substituted(axis, value, why); + // The row as the rows file holds it, read without marking the run's row: each check marks what it ran. + var s = Of(Probes.Shapes.Rows.Get(RowsFile, r.Index)); var id = r.Index.ToString(); var build = await Built(); var file = $"R{id}.cs"; @@ -173,11 +188,26 @@ static Task Built() => Consumer.Build(RowsFile, () => var errors = mine.Where(d => d.Severity == "error").ToList(); if (errors.Any(e => e.Id.StartsWith("CS"))) { r.Outcome("harness-error", "the probe's declaration does not compile: " + string.Join("; ", errors.Select(e => $"{e.Id} {e.Message}"))); return null; } - if (errors.Count > 0) return "the value object is not generated: " + string.Join("; ", errors.Select(e => $"{e.Id} {e.Message}")); var reported = mine.Where(d => d.Id == "CMTK1023").ToList(); + + // Every check runs, so a seed that breaks one obligation is seen by its own check whatever the others find. + var named = r.Check("names-each-base-member", Baseline.Docs("src/CodoMetis.TypeKit.Generators/README.md#equality"), c => { Ran(c, s); return NamesEach(c, s, id, file, reported); }); + var generated = r.Check("a-warning-and-generated", Baseline.Docs("src/CodoMetis.TypeKit.Generators/README.md#build-errors"), c => { Ran(c, s); return WarningAndGenerated(c, s, id, build, errors, reported); }); + var equality = r.Check("generated-equality-never-calls-the-base", Baseline.Twin, c => Equality(c)); + return named ?? generated ?? equality; + } + + /// Marks the axes the consumer's declaration was written from read for , at the value that ran. + static void Ran(Probes.Shapes.Check c, Shape s) + { + foreach (var axis in (string[])["shape.kind", "shape.site", "shape.inheritance", "shape.wrapped", "base-member", "shape.containing", "shape.position"]) _ = c[axis]; + foreach (var (axis, value, why) in s.Substituted) c.Substituted(axis, value, why); + } + + static string? NamesEach(Probes.Shapes.Check c, Shape s, string id, string file, List reported) + { if (reported.Count != 1) return $"CMTK1023 reported {reported.Count} times in {file}, expected once"; var d = reported[0]; - if (d.Severity != "warning") return $"CMTK1023 is reported as {d.Severity}, documented a warning"; if (!d.Message.StartsWith($"'{Nesting(s, id)}V{id}' inherits ", StringComparison.Ordinal)) return "CMTK1023 does not name the value object: " + d.Message; var named = Named(s, id); @@ -186,14 +216,30 @@ static Task Built() => Consumer.Build(RowsFile, () => var count = System.Text.RegularExpressions.Regex.Matches(d.Message, " in its base type ").Count; if (count != named.Count) return $"CMTK1023 names {count} members, the bases declare {named.Count} ({string.Join(", ", named)}): {d.Message}"; if (d.Message.Contains("Mark", StringComparison.Ordinal)) return "CMTK1023 names the explicit member of another interface: " + d.Message; + c.Note(NoteKeys.Compared, $"{named.Count} members named"); + return null; + } + + static string? WarningAndGenerated(Probes.Shapes.Check c, Shape s, string id, ConsumerOutcome build, List errors, List reported) + { + if (errors.Count > 0) return "the value object is not generated: " + string.Join("; ", errors.Select(e => $"{e.Id} {e.Message}")); + if (reported.Count == 0) return "no CMTK1023 warning: the build reports none for the value object"; + if (reported.FirstOrDefault(d => d.Severity != "warning") is { } other) return $"CMTK1023 is reported as {other.Severity}, documented a warning"; if (build.Methods.Count == 0) - { r.Outcome("harness-error", $"the consumer built no assembly (exit {build.ExitCode}): " + string.Join("; ", build.Diagnostics.Where(x => x.Severity == "error").Take(3).Select(x => $"{Path.GetFileName(x.File)} {x.Id} {x.Message}"))); return null; } + { c.Outcome("harness-error", $"the consumer built no assembly (exit {build.ExitCode}): " + string.Join("; ", build.Diagnostics.Where(x => x.Severity == "error").Take(3).Select(x => $"{Path.GetFileName(x.File)} {x.Id} {x.Message}"))); return null; } var type = MetadataName(s, id); - if (!build.Methods.TryGetValue(type, out var methods)) { r.Outcome("harness-error", $"no type {type} in the consumer"); return null; } + if (!build.Methods.TryGetValue(type, out var methods)) { c.Outcome("harness-error", $"no type {type} in the consumer"); return null; } var entry = s.Kind == "validated-class" ? "TryFrom" : "From"; if (!methods.Contains(entry)) return $"the value object is not generated: {type} has no {entry} ({string.Join(", ", methods.Order())})"; - if (Twin(s.Member) is { } twin) return twin; - r.Note(NoteKeys.Compared, $"{named.Count} members named"); + c.Note(NoteKeys.Compared, $"a warning, and {type} has {entry}"); + return null; + } + + static string? Equality(Probes.Shapes.Check c) + { + var member = c["base-member"]; + if (Twin(member) is { } twin) return twin; + c.Note(NoteKeys.Compared, $"over a base declaring {member}, equality is the wrapped value's"); return null; } } diff --git a/tools/probe/corpus/probes/C19Cmtk0010KindsProbe.cs b/tools/probe/corpus/probes/C19Cmtk0010KindsProbe.cs index a834afc3..349e1ea1 100644 --- a/tools/probe/corpus/probes/C19Cmtk0010KindsProbe.cs +++ b/tools/probe/corpus/probes/C19Cmtk0010KindsProbe.cs @@ -4,23 +4,36 @@ // await's GetAwaiter, a Deconstruct (in var (a, b) = x, a foreach, or a pattern like x is (1, 2)), and a collection expression's Add or // builder method ... every other attribute is [checked]", and at a call; "An inferred one is reported at the call ... the foreach's // collection, the await, the deconstruction, the pattern or the collection expression"; a pair's breach "once, at the later argument". -// Oracle: the page's table ("Accepts") over what each argument is. Each row compiles, with the CMTK0010 analyzer: -// - the catalogue (test/CodoMetis.TypeKit.Analyzers.Tests/Variants: TypeArgumentSites' subjects and its uses of the row's site, -// TypeArgumentKinds' candidates), with one argument that breaks the attribute and one that meets it (a pair attribute: subjects of -// two parameters at the same sites), plus a collection expression's builder method, which the catalogue has no use of; -// - the row's shapes as arguments at the same sites: the value object or closed set of shape.kind over shape.wrapped at shape.site, -// its base and its explicit interface (shape.inheritance), the wrapped type itself, and a declaration of shape.refused-kind (one -// that compiles without the generators; the two that do not are excluded); each use hosted in a member of shape.containing at -// shape.position. -// Every breaking use is reported exactly once at its span, no meeting one anywhere. +// Obligations (claims.json), each with a check below and a seed of its own in the registry entry (decision 104), the row's attribute +// selecting one of the first four and a site other than a call the fifth: +// interface-and-unsealed, value-object-and-closed-set, flags-enum-and-read-only-struct (attribute): the page's sentences on the pairs +// of kinds; a-pair-once-at-the-later-argument (Distinct, Unrelated): "A breach is reported once, at the later argument."; +// methods-the-compiler-calls (site foreach, await, Deconstruct, collection-expression): "**Methods the compiler calls for you:** ..." +// Oracle: the page (Baseline.Docs): its table ("Accepts") over what each argument is, and its list of what the rule checks. Each row +// compiles, with the CMTK0010 analyzer: +// - the catalogue (test/CodoMetis.TypeKit.Analyzers.Tests/Variants: TypeArgumentSites' subjects, its calls with written and inferred +// arguments and its uses of the row's site, TypeArgumentKinds' candidates), with one argument that breaks the attribute and one that +// meets it (a pair attribute: subjects of two parameters at the same sites), plus a collection expression's builder method, which +// the catalogue has no use of; +// - the row's shapes as arguments at the same calls and sites: the value object or closed set of shape.kind over shape.wrapped at +// shape.site, its base and its explicit interface (shape.inheritance), the wrapped type itself, and a declaration of +// shape.refused-kind (one that compiles without the generators; the two that do not are excluded); each use hosted in a member of +// shape.containing at shape.position. +// The attribute's check judges the calls: every breaking use reported exactly once at its span, no meeting one, nothing anywhere else. +// The methods' check judges the uses at the row's site against each argument's call with inferred arguments in the same compilation: +// reported once at its span where that call is reported, silent where it is silent; so a kind the rule misjudges fails the attribute's +// check alone, and a site the rule does not check fails the methods' check alone. using Probes.Build; +using Baseline = Probes.Shapes.Baseline; using NoteKeys = Probes.Shapes.NoteKeys; using CodoMetis.TypeKit.Analyzers.Tests.Variants; namespace Probes; public sealed class C19Cmtk0010KindsProbe { - public static TheoryData Data() => Probes.Shapes.Rows.Data("C19Cmtk0010KindsProbe"); + const string RowsFile = "C19Cmtk0010KindsProbe"; // the probe's own rows + + public static TheoryData Data() => Probes.Shapes.Rows.Data(RowsFile); [Theory, MemberData(nameof(Data))] public Task Probe(string probe, int idx) => Probes.Shapes.Rec.Run(probe, idx, Check); @@ -160,13 +173,35 @@ sealed class Uses int n; public readonly HashSet Expected = new(); public readonly List All = new(); + /// The uses at a call, written or inferred: what the attribute's check judges. + public readonly HashSet Calls = new(); + /// Each argument's call with inferred arguments, the baseline of its uses at the row's site. + public readonly Dictionary InferredCall = new(); + /// The uses at the row's site when it is not a call, each with its argument: what the methods' check judges. + public readonly Dictionary AtSite = new(); public readonly System.Text.StringBuilder Core = new(); /// Relabels a statement's {|#0:…|} with a label of its own; its breaking or meeting is recorded. - public string Label(string statements, bool breaking) + public string Label(string statements, bool breaking) => Label(statements, breaking, out _); + string Label(string statements, bool breaking, out string l) { - var l = "u" + n++; All.Add(l); if (breaking) Expected.Add(l); + l = "u" + n++; All.Add(l); if (breaking) Expected.Add(l); return statements.Replace("{|#0:", "{|#" + l + ":"); } + /// A use at a call of , the inferred one its uses at the site are compared with. + public string Call(string statements, bool breaking, string argument, bool inferred) + { + var s = Label(statements, breaking, out var l); + Calls.Add(l); + if (inferred) InferredCall[argument] = l; + return s; + } + /// A use of at the row's site, a method the compiler calls. + public string Site(string statements, bool breaking, string argument) + { + var s = Label(statements, breaking, out var l); + AtSite[l] = argument; + return s; + } public int Next => n++; } @@ -186,15 +221,35 @@ static string StatementsOf(string siteName) return use[prefix.Length..^suffix.Length]; } + const string Inferred = "a call with inferred arguments"; + + /// The obligation of each attribute's row (claims.json). + static readonly Dictionary ObligationOf = new() + { + ["Interface"] = "interface-and-unsealed", ["Unsealed"] = "interface-and-unsealed", + ["ValueObject"] = "value-object-and-closed-set", ["ClosedSet"] = "value-object-and-closed-set", + ["FlagsEnum"] = "flags-enum-and-read-only-struct", ["ReadOnlyStruct"] = "flags-enum-and-read-only-struct", + ["Distinct"] = "a-pair-once-at-the-later-argument", ["Unrelated"] = "a-pair-once-at-the-later-argument", + }; + static async Task Check(Probes.Shapes.Row r) { - var attribute = r["attribute"]; var site = r["site"]; + // The row as the rows file holds it, read without marking the run's row: each check marks the axes it ran, so the attribute's + // check, judged at the calls, is credited with the row's site only where the site is a call. + var row = Probes.Shapes.Rows.Get(RowsFile, r.Index); + var read = new List(); + var substituted = new List<(string Axis, string Value, string Why)>(); + string Read(string axis) { read.Add(axis); return row[axis]; } + + var attribute = Read("attribute"); var site = Read("site"); var pair = Pair.Contains(attribute); var constraint = $"[{attribute}]"; var uses = new Uses(); var id = r.Index.ToString(); + var atCall = site == "call"; - // The catalogue, with an argument that breaks the attribute and one that meets it. + // The catalogue, with an argument that breaks the attribute and one that meets it, each used at a call, written and inferred, + // and, where the row's site is not a call, at the site. if (!pair) { var (breaking, meeting) = attribute switch @@ -207,18 +262,19 @@ static string StatementsOf(string siteName) "ReadOnlyStruct" => ("Mutable", "Frozen"), _ => throw new ArgumentException(attribute), }; - var names = CatalogueSites(site); var k = 0; - foreach (var name in names) - foreach (var (arg, breaks) in new[] { (breaking, true), (meeting, false) }) - { - var use = TypeArgumentSites.All.Single(s => s.Name == name).Use.Replace("{A}", arg) - .Replace("public static class Use", $"public static class Use{k++}"); - uses.Core.AppendLine(uses.Label(use, breaks)); - } - if (site == "collection-expression") - foreach (var (arg, breaks) in new[] { (breaking, true), (meeting, false) }) - uses.Core.AppendLine(uses.Label(TypeArgumentSites.Body("Built<{A}> built = {|#0:[a]|};").Replace("{A}", arg).Replace("public static class Use", $"public static class Use{k++}"), breaks)); + foreach (var (arg, breaks) in new[] { (breaking, true), (meeting, false) }) + { + var argument = "catalogue:" + arg; + string Use(string use) => use.Replace("{A}", arg).Replace("public static class Use", $"public static class Use{k++}"); + foreach (var name in CatalogueSites("call")) + uses.Core.AppendLine(uses.Call(Use(TypeArgumentSites.All.Single(s => s.Name == name).Use), breaks, argument, name == Inferred)); + if (atCall) continue; + foreach (var name in CatalogueSites(site)) + uses.Core.AppendLine(uses.Site(Use(TypeArgumentSites.All.Single(s => s.Name == name).Use), breaks, argument)); + if (site == "collection-expression") + uses.Core.AppendLine(uses.Site(Use(TypeArgumentSites.Body("Built<{A}> built = {|#0:[a]|};")), breaks, argument)); + } } else { @@ -226,15 +282,22 @@ static string StatementsOf(string siteName) var cases = new List<(string F, string S, bool Breaks)> { ("int", "string", false), ("Placed", "Placed", true), ("Handler", "OrderHandler", attribute == "Unrelated") }; var k = 0; foreach (var (f, s, breaks) in cases) + { + var argument = $"catalogue:{f},{s}"; + var calls = PairStatements("call"); + for (var i = 0; i < calls.Length; i++) + uses.Core.AppendLine(uses.Call(PairHost("root", "class", $"Pair{k++}", f, s, calls[i]), breaks, argument, inferred: i == 1)); + if (atCall) continue; foreach (var statement in PairStatements(site)) - uses.Core.AppendLine(uses.Label(PairHost("root", "class", $"Pair{k++}", f, s, statement), breaks)); + uses.Core.AppendLine(uses.Site(PairHost("root", "class", $"Pair{k++}", f, s, statement), breaks, argument)); + } } // The row's shapes. - var kind = r["shape.kind"]; var inheritance = r["shape.inheritance"]; + var kind = Read("shape.kind"); var inheritance = Read("shape.inheritance"); if (Src.IsStructKind(kind) && inheritance is not ("none" or "explicit-interface-member")) { - r.Substituted("shape.inheritance", "none", "a record struct has no base type (excluded pair)"); + substituted.Add(("shape.inheritance", "none", "a record struct has no base type (excluded pair)")); inheritance = "none"; } var depth = inheritance switch { "base-depth-1" or "base-explicit-IEquatable" => 1, "base-depth-2" => 2, "base-depth-3" => 3, _ => 0 }; @@ -266,7 +329,7 @@ public sealed partial class D{{id}} : {{baseList}}IClosedSet{{mark}} """); else { - wrapped = r["shape.wrapped"]; + wrapped = Read("shape.wrapped"); var w = Src.Wrapped(wrapped, id); decl.AppendLine($"public enum DF{id} {{ No }}"); decl.AppendLine(kind switch @@ -281,10 +344,10 @@ public sealed partial class D{{id}} : {{baseList}}IClosedSet{{mark}} // shape.refused-kind: refused by the generators, an ordinary declaration to the compiler. The two kinds that are not (a closed // set named as a generated member, CS0535; a hand-written operator ==, CS0111) are excluded in claims.json, and a row the // covering array fills with one runs over object instead. - var refused = r["shape.refused-kind"]; + var refused = Read("shape.refused-kind"); if (refused is "closed-set-named-as-generated-member (CMTK1021)" or "equality-operator (CMTK1011)") { - r.Substituted("shape.refused-kind", "over-object (CMTK1005)", "it does not compile without the generators (excluded pair)"); + substituted.Add(("shape.refused-kind", "over-object (CMTK1005)", "it does not compile without the generators (excluded pair)")); refused = "over-object (CMTK1005)"; } var rName = refused == "named-as-generated-member (CMTK1005)" ? Refusals.ValueObjectNames[r.Index % Refusals.ValueObjectNames.Length] : $"R{id}"; @@ -300,7 +363,7 @@ public sealed partial class D{{id}} : {{baseList}}IClosedSet{{mark}} _ => throw new ArgumentException(refused), }); - var containing = r["shape.containing"]; var position = r["shape.position"]; + var containing = Read("shape.containing"); var position = Read("shape.position"); var dFacts = OfKind(kind); if (!pair) { @@ -309,11 +372,16 @@ public sealed partial class D{{id}} : {{baseList}}IClosedSet{{mark}} if (explicitMember) arguments.Add(($"IMark{id}", !Accepts(attribute, new Facts(Interface: true)))); if (wrapped is not null) arguments.Add((Src.Wrapped(wrapped, id), !Accepts(attribute, OfWrapped(wrapped)))); foreach (var (type, breaks) in arguments) + { + var argument = "shape:" + type; + foreach (var name in CatalogueSites("call")) + decl.AppendLine(uses.Call(Host(position, containing, $"C{id}_{uses.Next}", type, StatementsOf(name)), breaks, argument, name == Inferred)); + if (atCall) continue; foreach (var name in CatalogueSites(site)) - decl.AppendLine(uses.Label(Host(position, containing, $"C{id}_{uses.Next}", type, StatementsOf(name)), breaks)); - if (site == "collection-expression") - foreach (var (type, breaks) in arguments) - decl.AppendLine(uses.Label(Host(position, containing, $"C{id}_{uses.Next}", type, "Built<{A}> built = {|#0:[a]|};"), breaks)); + decl.AppendLine(uses.Site(Host(position, containing, $"C{id}_{uses.Next}", type, StatementsOf(name)), breaks, argument)); + if (site == "collection-expression") + decl.AppendLine(uses.Site(Host(position, containing, $"C{id}_{uses.Next}", type, "Built<{A}> built = {|#0:[a]|};"), breaks, argument)); + } } else { @@ -327,11 +395,18 @@ public sealed partial class D{{id}} : {{baseList}}IClosedSet{{mark}} (rName, $"D{id}", false), }; foreach (var (f, s, breaks) in cases) + { + var argument = $"shape:{f},{s}"; + var calls = PairStatements("call"); + for (var i = 0; i < calls.Length; i++) + decl.AppendLine(uses.Call(PairHost(position, containing, $"C{id}_{uses.Next}", f, s, calls[i]), breaks, argument, inferred: i == 1)); + if (atCall) continue; foreach (var statement in PairStatements(site)) - decl.AppendLine(uses.Label(PairHost(position, containing, $"C{id}_{uses.Next}", f, s, statement), breaks)); + decl.AppendLine(uses.Site(PairHost(position, containing, $"C{id}_{uses.Next}", f, s, statement), breaks, argument)); + } } - var shapes = Src.Usings + Src.WrappedDeclarations(id) + Src.AtSite(r["shape.site"], id, decl.ToString()); + var shapes = Src.Usings + Src.WrappedDeclarations(id) + Src.AtSite(Read("shape.site"), id, decl.ToString()); var subjects = TypeArgumentSites.Subjects.Replace("{C}", pair ? "" : constraint); var run = await Rz.Analyze("CMTK0010", [ @@ -341,10 +416,38 @@ public sealed partial class D{{id}} : {{baseList}}IClosedSet{{mark}} ]); if (run.Errors.Count > 0) { r.Outcome("harness-error", "the probe's source does not compile: " + run.ErrorText()); return null; } if (run.AnalyzerFailures.Count > 0) return "the analyzer failed: " + string.Join("; ", run.AnalyzerFailures.Select(f => f.GetMessage())); - r.Note(NoteKeys.Compared, $"{uses.All.Count} uses, {uses.Expected.Count} expected reported"); - return Rz.Agree(run, uses.Expected); - } + void Ran(Probes.Shapes.Check c, bool withSite) + { + foreach (var axis in read) if (withSite || axis != "site") _ = c[axis]; + foreach (var (axis, value, why) in substituted) c.Substituted(axis, value, why); + } + + // The attribute's obligation, judged at the calls: each breaking argument reported once at its span, each meeting one silent, and + // nothing reported outside every use. Where the row's site is a call, that is every use of the row. + var page = pair ? "docs/type-constraints.md#two-parameters-apart" : "docs/type-constraints.md#the-kind-of-a-type-argument"; + var kinds = r.Check(ObligationOf[attribute], Baseline.Docs(page), c => + { + Ran(c, withSite: atCall); + var fail = Rz.Agree(run, uses.Expected, m => uses.Calls.Contains(m.Label), outside: true); + if (fail is null) c.Note(NoteKeys.Compared, $"{uses.Calls.Count} uses at a call, {uses.Calls.Count(uses.Expected.Contains)} expected reported"); + return fail; + }); + if (atCall) return kinds; + + // The methods the compiler calls, judged against each argument's own call with inferred arguments in the same compilation: a use + // at the row's site is reported once at its span where that call is reported, and is silent where it is silent. + var methods = r.Check("methods-the-compiler-calls", Baseline.Docs("docs/type-constraints.md#constrain-a-type-parameter"), c => + { + Ran(c, withSite: true); + var marks = run.Marks.ToDictionary(m => m.Label); + var expected = uses.AtSite.Where(u => Rz.At(run, marks[uses.InferredCall[u.Value]]).Count > 0).Select(u => u.Key).ToHashSet(); + var fail = Rz.Agree(run, expected, m => uses.AtSite.ContainsKey(m.Label), outside: false); + if (fail is null) c.Note(NoteKeys.Compared, $"{uses.AtSite.Count} uses at a {site}, {expected.Count} reported as their inferred calls are"); + return fail; + }); + return kinds ?? methods; + } /// /// A member of shape.containing at shape.position holding in the catalogue's frame: a, holder, /// bag, crate and o of the argument {A}. A struct is given no instance initializer and no lambda over this. diff --git a/tools/probe/corpus/registry.json b/tools/probe/corpus/registry.json index 26931806..e85adb23 100644 --- a/tools/probe/corpus/registry.json +++ b/tools/probe/corpus/registry.json @@ -401,6 +401,11 @@ "obligation": "abstract-string-constructor", "patch": "seeds/C11-parse-shapes-build-abstract-string-constructor.patch", "defect": "An abstract wrapped class with a string constructor is parsed through its type converter, so the value object has parsing." + }, + { + "obligation": "parsing-where-it-can-be-parsed", + "patch": "seeds/C11-parse-shapes-build-parsing-where-it-can-be-parsed.patch", + "defect": "The generated [TypeConverter] is attached to a top-level value object only, so a nested one over a type that parses has no converter from text for MVC binding and the configuration binder." } ] }, @@ -592,6 +597,11 @@ "obligation": "reported-where-written", "patch": "seeds/C17-cmtk0014-reported-where-written.patch", "defect": "CMTK0014 is reported at the syntax around the value, not at the value." + }, + { + "obligation": "a-member-is-silent", + "patch": "seeds/C17-cmtk0014-a-member-is-silent.patch", + "defect": "CMTK0014 judges an explicit cast by its form, not by the value it names, so a cast that names a member, such as (FaultKind)2, is reported." } ] }, diff --git a/tools/probe/corpus/rows/C11ParseShapesBuildProbe.json.gz b/tools/probe/corpus/rows/C11ParseShapesBuildProbe.json.gz index d0ebe235..ffe8f639 100644 Binary files a/tools/probe/corpus/rows/C11ParseShapesBuildProbe.json.gz and b/tools/probe/corpus/rows/C11ParseShapesBuildProbe.json.gz differ diff --git a/tools/probe/corpus/seeds/C11-parse-shapes-build-parsing-where-it-can-be-parsed.patch b/tools/probe/corpus/seeds/C11-parse-shapes-build-parsing-where-it-can-be-parsed.patch new file mode 100644 index 00000000..232d8f1b --- /dev/null +++ b/tools/probe/corpus/seeds/C11-parse-shapes-build-parsing-where-it-can-be-parsed.patch @@ -0,0 +1,12 @@ +diff --git a/src/CodoMetis.TypeKit.Generators/ValueObjectTypeConverterAspect.cs b/src/CodoMetis.TypeKit.Generators/ValueObjectTypeConverterAspect.cs +index ab5020e..d8067bb 100644 +--- a/src/CodoMetis.TypeKit.Generators/ValueObjectTypeConverterAspect.cs ++++ b/src/CodoMetis.TypeKit.Generators/ValueObjectTypeConverterAspect.cs +@@ -80,6 +80,6 @@ internal sealed partial class ValueObjectTypeConverterAspect : TypeAspect + typeof(TypeConverterAttribute), + constructorArguments: [typeConverter.Target] + ); +- builder.IntroduceAttribute(typeConverterAttribute); ++ if (builder.Target.DeclaringType is null) builder.IntroduceAttribute(typeConverterAttribute); + } + } diff --git a/tools/probe/corpus/seeds/C17-cmtk0014-a-member-is-silent.patch b/tools/probe/corpus/seeds/C17-cmtk0014-a-member-is-silent.patch new file mode 100644 index 00000000..2cb967ad --- /dev/null +++ b/tools/probe/corpus/seeds/C17-cmtk0014-a-member-is-silent.patch @@ -0,0 +1,13 @@ +diff --git a/src/CodoMetis.TypeKit.Analyzers/UndefinedEnumValueAnalyzer.cs b/src/CodoMetis.TypeKit.Analyzers/UndefinedEnumValueAnalyzer.cs +index 7265ea3..1750073 100644 +--- a/src/CodoMetis.TypeKit.Analyzers/UndefinedEnumValueAnalyzer.cs ++++ b/src/CodoMetis.TypeKit.Analyzers/UndefinedEnumValueAnalyzer.cs +@@ -105,7 +105,7 @@ public sealed class UndefinedEnumValueAnalyzer : DiagnosticAnalyzer + if (operation.Type is not INamedTypeSymbol type || !enums.TryGetValue(type, out var known)) return null; + if (!operation.ConstantValue.HasValue || operation.ConstantValue.Value is not { } value) return null; + +- return known.Members.Contains(value) ? null : (type, known.Consequence); ++ return known.Members.Contains(value) && operation is not IConversionOperation { IsImplicit: false } ? null : (type, known.Consequence); + } + + /// Whether is an operand of a larger constant of an enum, which is judged in its place.