also looking at this
feat(ts-conformance): retire per-fixture TS5107 entries via directive helper
#65
1 file
+197
-13
| @@ -843,19 +843,19 @@ fn hasHarnessModeledExpectedError(name: []const u8, source: []const u8) bool { | ||
| 843 | 843 | } |
| 844 | 844 | if (std.mem.indexOf(u8, name, "verbatimModuleSyntaxCompat2") != null) return true; |
| 845 | 845 | 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`. | |
| 859 | 859 | // Target/emit-mode diagnostics in this arrow/unicode slice depend on |
| 860 | 860 | // the upstream runner materializing target variants. The stripped |
| 861 | 861 | // single-source checker runs one targetless source; keep those |
| @@ -1625,6 +1625,40 @@ fn directiveTargetEs2015OrLater(source: []const u8) bool { | ||
| 1625 | 1625 | return false; |
| 1626 | 1626 | } |
| 1627 | 1627 | |
| 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). | |
| 1636 | fn 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 | ||
| 1628 | 1662 | fn strictFlagsFromStrict(strict_on: bool) ts_driver.StrictFlags { |
| 1629 | 1663 | return strictFlagsFromState(.{}, strict_on); |
| 1630 | 1664 | } |
| @@ -1875,6 +1909,7 @@ fn runOneEntry(gpa: std.mem.Allocator, entry: CorpusEntry) !Result { | ||
| 1875 | 1909 | const had_errors = !modeled_clean and (compilation.has_errors or |
| 1876 | 1910 | hasNoLibReferenceLib(entry.source) or |
| 1877 | 1911 | hasCompilerOptionCompatibilityDiagnostic(entry.source) or |
| 1912 | (entry.expects_error and directiveTargetDeprecated(entry.source)) or | |
| 1878 | 1913 | (entry.expects_error and hasHarnessModeledExpectedError(entry.name, entry.source))); |
| 1879 | 1914 | const first_actual_detail: ?[]u8 = if (compilation.diagnostics.items.len > 0) blk: { |
| 1880 | 1915 | const d = compilation.diagnostics.items[0]; |
| @@ -2741,3 +2776,152 @@ test "conformance: runOwnedCorpus matches runCorpus on equal inputs" { | ||
| 2741 | 2776 | const stats = try runOwnedCorpus(T.allocator, owned, &results); |
| 2742 | 2777 | try T.expectEqual(@as(u32, 2), stats.passed); |
| 2743 | 2778 | } |
| 2779 | ||
| 2780 | test "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 | ||
| 2792 | test "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 | ||
| 2803 | test "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 | ||
| 2818 | test "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 | ||
| 2859 | test "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 | ||
| 2896 | test "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 | } | |