Skip to content
73 changes: 73 additions & 0 deletions tools/probe/corpus/claims.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -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"
]
]
}
]
},
Expand Down Expand Up @@ -6776,6 +6840,15 @@
"default"
]
}
},
{
"id": "a-member-is-silent",
"sentence": "public static readonly ImportFault Duplicate = new(FaultKind.Conflict);",
"axes": {
"value": [
"valid"
]
}
}
],
"exclude": [
Expand Down
66 changes: 60 additions & 6 deletions tools/probe/corpus/harness/BuildRoslyn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,24 +90,78 @@ public static async Task<Analysis> Analyze(string id, IEnumerable<(string File,
/// Compares the reports with the marks: every mark in <paramref name="expected"/> reported exactly once at its span, nothing
/// reported anywhere else. Null when they agree, else what differs.
/// </summary>
public static string? Agree(Analysis a, IReadOnlySet<string> expected, Func<Diagnostic, Mark, string?>? message = null)
public static string? Agree(Analysis a, IReadOnlySet<string> expected, Func<Diagnostic, Mark, string?>? message = null) =>
Agree(a, expected, _ => true, outside: true, message);

/// <summary>
/// The same over the marks <paramref name="judged"/> selects, one check's share of a compilation: each of them in
/// <paramref name="expected"/> reported exactly once at its span, the others silent, and with <paramref name="outside"/>, nothing
/// reported outside every mark. A report at a mark the check does not judge is another check's.
/// </summary>
public static string? Agree(Analysis a, IReadOnlySet<string> expected, Func<Mark, bool> judged, bool outside, Func<Diagnostic, Mark, string?>? 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<string>();
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");
else if (message?.Invoke(at[0], m) is { } bad) problems.Add($"{m.Label} '{m.Text}': {bad}");
}
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);
}

/// <summary>The reports exactly at a mark's span.</summary>
public static List<Diagnostic> At(Analysis a, Mark m) =>
[.. a.Reported.Where(d => d.Location.SourceTree?.FilePath == m.File && d.Location.SourceSpan == m.Span)];

/// <summary>The reports whose span holds a mark's: at it, or at the syntax around it.</summary>
public static List<Diagnostic> Around(Analysis a, Mark m) =>
[.. a.Reported.Where(d => d.Location.SourceTree?.FilePath == m.File && d.Location.SourceSpan.Contains(m.Span))];

/// <summary>The reports whose span holds no mark.</summary>
public static List<Diagnostic> Outside(Analysis a) =>
[.. a.Reported.Where(d => !a.Marks.Any(m => d.Location.SourceTree?.FilePath == m.File && d.Location.SourceSpan.Contains(m.Span)))];

/// <summary>
/// Whether a value is reported, wherever around it: each mark <paramref name="judged"/> names that is in <paramref name="expected"/>
/// has exactly one report at or around it, the others none. Where the report sits is <see cref="WhereWritten"/>'s question, so a
/// report moved to the syntax around its value still counts here. Null when they agree, else what differs.
/// </summary>
public static string? Reported(Analysis a, IEnumerable<string> judged, IReadOnlySet<string> expected, Func<Diagnostic, Mark, string?>? message = null)
{
var problems = new List<string>();
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);
}

/// <summary>
/// Where the reports sit: every report at or around a mark in <paramref name="expected"/> is at the mark's own span, the value as
/// written, and none is outside every mark. Null when so, else what differs.
/// </summary>
public static string? WhereWritten(Analysis a, IReadOnlySet<string> expected)
{
var problems = new List<string>();
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);
}
}
65 changes: 65 additions & 0 deletions tools/probe/corpus/harness/C11Shapes.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2282,6 +2282,65 @@ public sealed partial record V239 : B239_1, IValue<double> { }
public abstract record B240_1 { }
public sealed partial record V240 : B240_1, IValue<byte> { }

// row 241
public static partial class O241
{
public sealed partial class P241
{
public interface IMark241 { int Mark(); }
public sealed partial record V241 : IValidatedValue<V241, global::Probes.Shapes.PEnum, global::Probes.Shapes.ShapeFault>, IMark241
{
public static Result<V241, global::Probes.Shapes.ShapeFault> 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<global::Probes.Shapes.PFlags> { }
}

// row 243
public readonly partial record struct V243 : IValue<global::System.Uri> { }

// row 244
public static partial class O244
{
public sealed partial class P244
{
public readonly partial record struct V244 : IValidatedValue<V244, global::NodaTime.Instant, global::Probes.Shapes.ShapeFault>
{
public static Result<V244, global::Probes.Shapes.ShapeFault> 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<global::System.Uri>, 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<V246, global::System.Uri, global::Probes.Shapes.ShapeFault>
{
public static Result<V246, global::Probes.Shapes.ShapeFault> Create(global::System.Uri value) => new V246(value);
}
}
}

public static partial class C11Table
{
public static readonly C11Case[] Cases =
Expand Down Expand Up @@ -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, []),
];
}
41 changes: 29 additions & 12 deletions tools/probe/corpus/probes/C11ParseShapesBuildProbe.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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); });
}

/// <summary>
Expand Down Expand Up @@ -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);
}
Expand Down
Loading
Loading