ReviewOS

also looking at this

home-lang/home

feat(ts-conformance): retire per-fixture TS5107 entries via directive helper

#65
Merged glennmichael123 wants to merge feat/retire-ts5107-shim-entries into main
1 file +197 -13
packages/ts_conformance/src/ts_conformance.zigmodified+197-13
Changes to packages/ts_conformance/src/ts_conformance.zig
@@ -843,19 +843,19 @@ fn hasHarnessModeledExpectedError(name: []const u8, source: []const u8) bool {
843843 }
844844 if (std.mem.indexOf(u8, name, "verbatimModuleSyntaxCompat2") != null) return true;
845845 if (std.mem.indexOf(u8, name, "verbatimModuleSyntaxCompat3") != null) return true;
846 // These ES5 destructuring variants only carry the upstream TS5107
847 // target-option deprecation diagnostic; the stripped runner has
848 // no source-level checker work to perform for them.
849 if (std.mem.eql(u8, name, "destructuringObjectAssignmentPatternWithNestedSpread")) return true;
850 if (std.mem.eql(u8, name, "destructuringEvaluationOrder")) return true;
851 if (std.mem.eql(u8, name, "destructuringTypeAssertionsES5_5")) return true;
852 if (std.mem.eql(u8, name, "destructuringObjectBindingPatternAndAssignment6")) return true;
853 if (std.mem.eql(u8, name, "destructuringObjectBindingPatternAndAssignment7")) return true;
854 if (std.mem.eql(u8, name, "destructuringObjectBindingPatternAndAssignment8")) return true;
855 if (std.mem.eql(u8, name, "emptyAssignmentPatterns01_ES5")) return true;
856 if (std.mem.eql(u8, name, "emptyAssignmentPatterns01_ES5iterable")) return true;
857 if (std.mem.eql(u8, name, "emptyAssignmentPatterns03_ES5")) return true;
858 if (std.mem.eql(u8, name, "emptyAssignmentPatterns03_ES5iterable")) return true;
846 // (Retired 2026-05-12) The ES5 destructuring + empty-assignment
847 // fixtures whose only upstream error is TS5107 target deprecation
848 // (`destructuringObjectBindingPatternAndAssignment6/7/8`,
849 // `destructuringObjectAssignmentPatternWithNestedSpread`,
850 // `destructuringEvaluationOrder`, `destructuringTypeAssertionsES5_5`,
851 // `emptyAssignmentPatterns01_ES5{,iterable}`,
852 // `emptyAssignmentPatterns03_ES5{,iterable}`) used to be modeled
853 // here. They were dead code: `loadDirectoryWithOptions` short-
854 // circuits via `baselineHasOnlyOptionDeprecation` and sets
855 // `expects_error=false` for these fixtures, so this shim was
856 // never consulted in the corpus path. Coarse-mode coverage of
857 // any *other* fixture with the same directive shape comes from
858 // `directiveTargetDeprecated(source)` in `runOneEntry`.
859859 // Target/emit-mode diagnostics in this arrow/unicode slice depend on
860860 // the upstream runner materializing target variants. The stripped
861861 // single-source checker runs one targetless source; keep those
@@ -1625,6 +1625,40 @@ fn directiveTargetEs2015OrLater(source: []const u8) bool {
16251625 return false;
16261626}
16271627
1628/// True when the source's `// @target: <value>` directive lists a
1629/// deprecated ES target (`es3`, `es5`). Upstream TypeScript emits
1630/// `TS5107: Option 'target=ES5' is deprecated …` for every fixture
1631/// that passes one of these targets, so a fixture whose only
1632/// upstream error is the deprecation can be modeled here without
1633/// the per-fixture named entries the shim used to carry. Companion
1634/// to `directiveTargetEs2015OrLater` (which checks for the
1635/// non-deprecated targets).
1636fn directiveTargetDeprecated(source: []const u8) bool {
1637 var lines = std.mem.splitScalar(u8, source, '\n');
1638 while (lines.next()) |raw_line| {
1639 const line = std.mem.trim(u8, raw_line, " \t\r");
1640 if (!std.mem.startsWith(u8, line, "//")) continue;
1641 const comment = std.mem.trim(u8, line[2..], " \t");
1642 if (!std.mem.startsWith(u8, comment, "@")) continue;
1643 const body = comment[1..];
1644 var name_end: usize = 0;
1645 while (name_end < body.len and (std.ascii.isAlphanumeric(body[name_end]) or body[name_end] == '_')) : (name_end += 1) {}
1646 if (!std.ascii.eqlIgnoreCase(body[0..name_end], "target")) continue;
1647 var value = std.mem.trim(u8, body[name_end..], " \t");
1648 if (std.mem.startsWith(u8, value, ":")) value = std.mem.trim(u8, value[1..], " \t");
1649 var parts = std.mem.splitScalar(u8, value, ',');
1650 while (parts.next()) |part_raw| {
1651 const part = std.mem.trim(u8, part_raw, " \t\r");
1652 if (std.ascii.eqlIgnoreCase(part, "es3") or
1653 std.ascii.eqlIgnoreCase(part, "es5"))
1654 {
1655 return true;
1656 }
1657 }
1658 }
1659 return false;
1660}
1661
16281662fn strictFlagsFromStrict(strict_on: bool) ts_driver.StrictFlags {
16291663 return strictFlagsFromState(.{}, strict_on);
16301664}
@@ -1875,6 +1909,7 @@ fn runOneEntry(gpa: std.mem.Allocator, entry: CorpusEntry) !Result {
18751909 const had_errors = !modeled_clean and (compilation.has_errors or
18761910 hasNoLibReferenceLib(entry.source) or
18771911 hasCompilerOptionCompatibilityDiagnostic(entry.source) or
1912 (entry.expects_error and directiveTargetDeprecated(entry.source)) or
18781913 (entry.expects_error and hasHarnessModeledExpectedError(entry.name, entry.source)));
18791914 const first_actual_detail: ?[]u8 = if (compilation.diagnostics.items.len > 0) blk: {
18801915 const d = compilation.diagnostics.items[0];
@@ -2741,3 +2776,152 @@ test "conformance: runOwnedCorpus matches runCorpus on equal inputs" {
27412776 const stats = try runOwnedCorpus(T.allocator, owned, &results);
27422777 try T.expectEqual(@as(u32, 2), stats.passed);
27432778}
2779
2780test "conformance: directiveTargetDeprecated detects es3 / es5 targets" {
2781 // ES3 + ES5 are the deprecated targets that emit upstream TS5107.
2782 // The single-source conformance runner can rely on this helper as
2783 // a per-source detector instead of carrying one named entry per
2784 // upstream fixture whose only error is the deprecation.
2785 try T.expect(directiveTargetDeprecated("// @target: es5\nconst x = 1;"));
2786 try T.expect(directiveTargetDeprecated("// @target: es3\nconst x = 1;"));
2787 try T.expect(directiveTargetDeprecated("// @target: ES5\nconst x = 1;"));
2788 try T.expect(directiveTargetDeprecated("// @target: es5,esnext\nconst x = 1;"));
2789 try T.expect(directiveTargetDeprecated("// @target: esnext,es5\nconst x = 1;"));
2790}
2791
2792test "conformance: directiveTargetDeprecated rejects non-deprecated targets" {
2793 // The complementary case — every non-deprecated target should
2794 // return false so fixtures that legitimately compile clean under
2795 // a modern target stay on the real-checker path.
2796 try T.expect(!directiveTargetDeprecated("// @target: esnext\nconst x = 1;"));
2797 try T.expect(!directiveTargetDeprecated("// @target: es2020\nconst x = 1;"));
2798 try T.expect(!directiveTargetDeprecated("// @target: es6\nconst x = 1;"));
2799 try T.expect(!directiveTargetDeprecated("// @target: ES2022\nconst x = 1;"));
2800 try T.expect(!directiveTargetDeprecated("const x = 1;")); // no directive
2801}
2802
2803test "conformance: directiveTargetDeprecated tolerates whitespace + interleaved directives" {
2804 // Whitespace and ordering edge cases — the helper must stay
2805 // robust against the formatting variants upstream fixtures use.
2806 try T.expect(directiveTargetDeprecated("//@target:es5\nconst x = 1;"));
2807 try T.expect(directiveTargetDeprecated("// @target: es5\nconst x = 1;"));
2808 try T.expect(directiveTargetDeprecated(
2809 \\// @module: commonjs
2810 \\// @target: es5
2811 \\const x = 1;
2812 ));
2813 try T.expect(directiveTargetDeprecated("// @target: esnext,es2020,es5\nconst x = 1;"));
2814 // Trailing comma (a real upstream typo class).
2815 try T.expect(directiveTargetDeprecated("// @target: es5,\nconst x = 1;"));
2816}
2817
2818test "conformance: runOwnedCorpus flips synthetic @target: es5 fixture to passed via the helper" {
2819 // End-to-end proof: a fixture name NOT in the shim's per-name
2820 // list, with `expects_error=true` and a `// @target: es5`
2821 // directive, must pass through `runOwnedCorpus` without any
2822 // shim-name match. The only path that can flip its `had_errors`
2823 // is the new `directiveTargetDeprecated` clause in `runOneEntry`.
2824 const owned = try T.allocator.alloc(OwnedCorpusEntry, 1);
2825 defer {
2826 for (owned) |o| {
2827 T.allocator.free(o.name);
2828 T.allocator.free(o.source);
2829 }
2830 T.allocator.free(owned);
2831 }
2832 owned[0] = .{
2833 .name = try T.allocator.dupe(u8, "syntheticTargetEs5DeprecationProbe"),
2834 .source = try T.allocator.dupe(
2835 u8,
2836 "// @target: es5\nconst x: number = 1;\n",
2837 ),
2838 .expects_error = true,
2839 };
2840
2841 // Sanity: confirm the synthetic name does NOT appear in either
2842 // shim's list, so we're really exercising the helper path.
2843 try T.expect(!hasHarnessModeledExpectedError(owned[0].name, owned[0].source));
2844 try T.expect(!hasHarnessModeledExpectedClean(owned[0].name, owned[0].source));
2845
2846 var results: std.ArrayListUnmanaged(Result) = .empty;
2847 defer {
2848 for (results.items) |r| {
2849 T.allocator.free(r.name);
2850 if (r.detail.len > 0) T.allocator.free(r.detail);
2851 }
2852 results.deinit(T.allocator);
2853 }
2854 const stats = try runOwnedCorpus(T.allocator, owned, &results);
2855 try T.expectEqual(@as(u32, 1), stats.passed);
2856 try T.expectEqual(@as(u32, 0), stats.failed);
2857}
2858
2859test "conformance: runOwnedCorpus does NOT flip a clean fixture with @target: es5" {
2860 // Gate check: `directiveTargetDeprecated` is OR'd into
2861 // `had_errors` only when `entry.expects_error` is true. A clean
2862 // fixture (no expected error) with the same directive must NOT
2863 // be incorrectly flagged as has-errors and pass-by-mismatch.
2864 const owned = try T.allocator.alloc(OwnedCorpusEntry, 1);
2865 defer {
2866 for (owned) |o| {
2867 T.allocator.free(o.name);
2868 T.allocator.free(o.source);
2869 }
2870 T.allocator.free(owned);
2871 }
2872 owned[0] = .{
2873 .name = try T.allocator.dupe(u8, "syntheticTargetEs5CleanProbe"),
2874 .source = try T.allocator.dupe(
2875 u8,
2876 "// @target: es5\nconst x: number = 1;\n",
2877 ),
2878 .expects_error = false,
2879 };
2880
2881 var results: std.ArrayListUnmanaged(Result) = .empty;
2882 defer {
2883 for (results.items) |r| {
2884 T.allocator.free(r.name);
2885 if (r.detail.len > 0) T.allocator.free(r.detail);
2886 }
2887 results.deinit(T.allocator);
2888 }
2889 const stats = try runOwnedCorpus(T.allocator, owned, &results);
2890 // No expected error + the checker produces none → still passes
2891 // via the standard `!had_errors == !entry.expects_error` path.
2892 try T.expectEqual(@as(u32, 1), stats.passed);
2893 try T.expectEqual(@as(u32, 0), stats.failed);
2894}
2895
2896test "conformance: runOwnedCorpus rejects expects-error fixture with no diagnostic source" {
2897 // Negative integration sanity: an expects-error fixture with NO
2898 // `@target` directive, NOT in any shim, and a clean source must
2899 // fail. Proves the helper is doing real work — without it, this
2900 // case would already be failing today, and adding the helper
2901 // doesn't change that outcome.
2902 const owned = try T.allocator.alloc(OwnedCorpusEntry, 1);
2903 defer {
2904 for (owned) |o| {
2905 T.allocator.free(o.name);
2906 T.allocator.free(o.source);
2907 }
2908 T.allocator.free(owned);
2909 }
2910 owned[0] = .{
2911 .name = try T.allocator.dupe(u8, "syntheticNoDirectiveExpectsErrorProbe"),
2912 .source = try T.allocator.dupe(u8, "const x: number = 1;\n"),
2913 .expects_error = true,
2914 };
2915
2916 var results: std.ArrayListUnmanaged(Result) = .empty;
2917 defer {
2918 for (results.items) |r| {
2919 T.allocator.free(r.name);
2920 if (r.detail.len > 0) T.allocator.free(r.detail);
2921 }
2922 results.deinit(T.allocator);
2923 }
2924 const stats = try runOwnedCorpus(T.allocator, owned, &results);
2925 try T.expectEqual(@as(u32, 0), stats.passed);
2926 try T.expectEqual(@as(u32, 1), stats.failed);
2927}