Skip to content

Commit 0eca1c8

Browse files
claude[bot]claude
andauthored
fix(tooling): make an unparseable source REFUSE instead of scoring clean (#10573)
`ts.createSourceFile` never throws. A syntax error — or the wrong `ScriptKind` — returns a `SourceFile` built by error recovery with the errors parked on `parseDiagnostics`, a property nothing under `scripts/` read. Fifteen gates walked TypeScript that way, so any of them could report a confident zero about a file it never managed to read. This was not latent. `check-engine-double-contract.mjs` forced `ts.ScriptKind.TSX` on all 2504 `*.{test,spec}.{ts,tsx,mts}` files under `packages/` and `examples/`; in TSX a `<` opens a JSX element, so an ordinary `new Map<string, X>()` made the rest of the file wreckage. 32 of those 2504 parsed with errors (up to 633 diagnostics in one file) and the gate printed `OK — 342 pinned, 133 in the DEBT ledger, 2 exempt`. Read under the ScriptKind their own names imply, three of those files turn out to pin six engine doubles the ledger had never recorded. - `scripts/ts-parse.mjs` — the one sanctioned parse. Reads the diagnostics and exits 3 (distinct from 1: "could not read the tree" is not "found violations"), naming the file, the line:column and TypeScript's own message. It exits rather than throws because `try { createSourceFile } catch { continue }` is already written in `packages/lint/src`, against a throw that cannot happen. - `scripts/check-parse-guard.mjs` — the half that makes it hold. A raw `ts.createSourceFile` anywhere in `scripts/**` outside the parser home is a failure, so there is no second spelling left to drift from. Same shape as `invoked-as.mjs` + `check-entry-guard.mjs`, and for the same measured reason. - All 32 call sites across the 15 gates converted; the three engine-double-contract sites that forced TSX over real files now let the file name decide. - `engine-double-contract.pinned.json` grows by the 6 rows that became visible: 342 -> 348 pinned, 0 lost. Fixes: #10133 Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8ba478d commit 0eca1c8

18 files changed

Lines changed: 711 additions & 39 deletions

scripts/check-driver-memory-census.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ import { join, dirname, relative, sep } from 'node:path';
108108
import { fileURLToPath } from 'node:url';
109109
import { execFileSync } from 'node:child_process';
110110
import ts from 'typescript';
111+
import { parseSourceFile } from './ts-parse.mjs';
111112

112113
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
113114
const LEDGER_PATH = join(ROOT, 'scripts', 'driver-memory-census.ledger.json');
@@ -229,7 +230,7 @@ function classify(node) {
229230

230231
/** Every occurrence of the specifier in one source text, classified. */
231232
export function scanSource(fileName, text) {
232-
const sf = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest, true);
233+
const sf = parseSourceFile(fileName, text);
233234
const found = [];
234235
const visit = (node) => {
235236
if ((ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) && namesPackage(node.text)) {

scripts/check-durability-degradation-log-level.mjs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
240240
import { join, relative, sep } from 'node:path';
241241
import { fileURLToPath } from 'node:url';
242242
import ts from 'typescript';
243+
import { parseSourceFile } from './ts-parse.mjs';
243244

244245
const ROOT = fileURLToPath(new URL('..', import.meta.url));
245246
const BASELINE_PATH = join(ROOT, 'scripts', 'durability-degradation.baseline.json');
@@ -2441,7 +2442,7 @@ function runReadSeamRule({ list = false } = {}) {
24412442
for (const file of collectSourceFiles(join(ROOT, root))) {
24422443
const text = readFileSync(file, 'utf8');
24432444
if (!text.includes('catch')) continue;
2444-
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
2445+
const sf = parseSourceFile(file, text, ts.ScriptKind.TS);
24452446
analyzeReadSeams(sf, relative(ROOT, file).split(sep).join('/'), findings, seams, {
24462447
usedDiscriminators,
24472448
});
@@ -2716,7 +2717,7 @@ function run({ list = false } = {}) {
27162717
for (const file of files) {
27172718
const text = readFileSync(file, 'utf8');
27182719
if (!text.includes('catch')) continue;
2719-
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
2720+
const sf = parseSourceFile(file, text, ts.ScriptKind.TS);
27202721
analyzeSourceFile(sf, relative(ROOT, file).split(sep).join('/'), findings, seams, {
27212722
usedPropagationSites,
27222723
summaryBranches,
@@ -3815,7 +3816,7 @@ function selfTest() {
38153816

38163817
let failures = 0;
38173818
for (const c of cases) {
3818-
const sf = ts.createSourceFile('t.ts', c.code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
3819+
const sf = parseSourceFile('t.ts', c.code, ts.ScriptKind.TS);
38193820
const findings = [];
38203821
const seams = [];
38213822
const summaryBranches = [];
@@ -4481,7 +4482,7 @@ function selfTestReadSeams() {
44814482

44824483
let failures = 0;
44834484
for (const c of cases) {
4484-
const sf = ts.createSourceFile('t.ts', c.code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
4485+
const sf = parseSourceFile('t.ts', c.code, ts.ScriptKind.TS);
44854486
const findings = [];
44864487
const seams = [];
44874488
const usedDiscriminators = new Set();

scripts/check-engine-double-contract.mjs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ import { readFileSync, readdirSync, existsSync, writeFileSync } from 'node:fs';
275275
import { join, dirname, relative, sep } from 'node:path';
276276
import { fileURLToPath } from 'node:url';
277277
import ts from 'typescript';
278+
import { parseSourceFile } from './ts-parse.mjs';
278279

279280
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
280281
const BASELINE_PATH = join(ROOT, 'scripts', 'engine-double-contract.baseline.json');
@@ -816,7 +817,7 @@ function localFunctions(sourceFile) {
816817
* #5393 hit and #5480 removed the excuse for.
817818
*/
818819
function scanSource(fileName, text, slice = SLICES[0], opts = {}) {
819-
const sf = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
820+
const sf = parseSourceFile(fileName, text);
820821
const pinnedNames = pinnedImportsOf(sf, slice);
821822
const locals = localFunctions(sf);
822823
const doubles = [];
@@ -960,7 +961,7 @@ function declaredBindings(sf) {
960961
* construct left the population without any verdict being recorded.
961962
*/
962963
function censusSource(fileName, text, slice) {
963-
const sf = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
964+
const sf = parseSourceFile(fileName, text);
964965
const declared = declaredBindings(sf);
965966
const unrecognised = [];
966967
const scopedOut = [];
@@ -1202,7 +1203,7 @@ function censusRecognizer() {
12021203
const rel = relative(ROOT, abs).split(sep).join('/');
12031204
const text = readFileSync(abs, 'utf8');
12041205
if (!/\b(delete|update)\s*[(:,}]/.test(text)) continue;
1205-
const sf = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1206+
const sf = parseSourceFile(abs, text);
12061207

12071208
const consider = (props) => {
12081209
for (const m of props) {
@@ -1802,7 +1803,7 @@ function envelopeImportsOf(sourceFile) {
18021803
* caller-supplied id, in a function that answers a receipt.
18031804
*/
18041805
function scanSeams(fileName, text) {
1805-
const sf = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
1806+
const sf = parseSourceFile(fileName, text, ts.ScriptKind.TS);
18061807
const envelopeNames = envelopeImportsOf(sf);
18071808
const localFns = localFunctions(sf);
18081809
const methodFns = classMethods(sf);
@@ -1949,7 +1950,7 @@ function declaredFunctionNames(sourceFile) {
19491950
function fileDeclaresFunction(file, fn) {
19501951
const abs = join(ROOT, file);
19511952
if (!existsSync(abs)) return false;
1952-
const sf = ts.createSourceFile(file, readFileSync(abs, 'utf8'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
1953+
const sf = parseSourceFile(file, readFileSync(abs, 'utf8'), ts.ScriptKind.TS);
19531954
return declaredFunctionNames(sf).has(fn);
19541955
}
19551956

@@ -3517,7 +3518,7 @@ ${body}
35173518
// (`scalarWhereIdOf` is where "scalar" means something) and this pins it
35183519
// where a mutation can reach it.
35193520
const whereIdOf = (src) => {
3520-
const f = ts.createSourceFile('t.ts', src, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
3521+
const f = parseSourceFile('t.ts', src, ts.ScriptKind.TS);
35213522
let lit = null;
35223523
const v = (n) => { if (!lit && ts.isObjectLiteralExpression(n) && propertyNamed(n, 'where')) lit = n; ts.forEachChild(n, v); };
35233524
v(f);
@@ -3929,7 +3930,7 @@ class Svc {
39293930
// a walker blind to one of them would classify that seam's loss as the
39303931
// quieter story — and the classifier would still look healthy.
39313932
const namesOf = (src) => declaredFunctionNames(
3932-
ts.createSourceFile('d.ts', src, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS));
3933+
parseSourceFile('d.ts', src, ts.ScriptKind.TS));
39333934
expect('declaredFunctionNames reads a top-level function declaration (`callData`s shape)',
39343935
namesOf('export async function callData(deps) { return 1; }').has('callData'));
39353936
expect('…an OBJECT LITERAL method (`protocol.updateData` / the MCP bridge’s shape)',
@@ -4053,7 +4054,7 @@ const engine: any = { registry: {}, insert: async (o: string, d: any) => d, find
40534054
// another would print a confident table and hide the same blind spot the
40544055
// constants did.
40554056
const kindsOf = (src) => {
4056-
const sf = ts.createSourceFile('k.test.ts', src, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
4057+
const sf = parseSourceFile('k.test.ts', src, ts.ScriptKind.TSX);
40574058
const out = [];
40584059
const visit = (n) => {
40594060
if (ts.isObjectLiteralExpression(n) || ts.isClassDeclaration(n)) {
@@ -4076,7 +4077,7 @@ const engine: any = { registry: {}, insert: async (o: string, d: any) => d, find
40764077
expect('#9943 — a method body is its own kind too', kindsOf('const e = { async update(o, d) {} };') === 'method body');
40774078

40784079
const assignSites = (src) => objectAssignSites(
4079-
ts.createSourceFile('a.test.ts', src, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX), SCANNED_VERBS,
4080+
parseSourceFile('a.test.ts', src, ts.ScriptKind.TSX), SCANNED_VERBS,
40804081
);
40814082
let sites = assignSites('const ql = Object.assign(makeQl(), { async find() { return []; }, async insert() { return null; } });');
40824083
expect('#8553 — an Object.assign override varying OTHER engine members reads as BASE-accounted '

scripts/check-filter-alias-parity.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ import { readFileSync } from 'node:fs';
8787
import { join } from 'node:path';
8888
import { fileURLToPath } from 'node:url';
8989
import ts from 'typescript';
90+
import { parseSourceFile } from './ts-parse.mjs';
9091

9192
const ROOT = join(fileURLToPath(new URL('.', import.meta.url)), '..');
9293

@@ -108,7 +109,7 @@ const FILTER_SLOT = 'where';
108109
class UnreadableShape extends Error {}
109110

110111
function parse(path, text) {
111-
return ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true);
112+
return parseSourceFile(path, text);
112113
}
113114

114115
/** Every node in a subtree, depth-first. */

scripts/check-init-service-contract.mjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ import { readFileSync, readdirSync, statSync } from 'node:fs';
8282
import { join, relative, sep } from 'node:path';
8383
import { fileURLToPath } from 'node:url';
8484
import ts from 'typescript';
85+
import { parseSourceFile } from './ts-parse.mjs';
8586

8687
const ROOT = join(fileURLToPath(new URL('.', import.meta.url)), '..');
8788

@@ -393,7 +394,7 @@ function scan(files = discoverFiles()) {
393394
// would be filtered out here before the AST ever saw it, which is the same
394395
// silent-hole failure this file's #4772 note is about.
395396
if (!PREFILTER_TOKENS.some((token) => text.includes(token))) continue;
396-
const src = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
397+
const src = parseSourceFile(file, text);
397398
for (const unit of collectPluginUnits(file, src)) {
398399
units.push({ ...unit, initCalls: initServiceCalls(unit, src) });
399400
}
@@ -501,7 +502,7 @@ function selfTest() {
501502
const assert = (cond, msg) => { if (!cond) { console.error('✗ self-test: ' + msg); process.exit(1); } };
502503

503504
const auditSource = (code) => {
504-
const src = ts.createSourceFile('fixture.ts', code, ts.ScriptTarget.Latest, true);
505+
const src = parseSourceFile('fixture.ts', code);
505506
const units = collectPluginUnits('fixture.ts', src).map((u) => ({ ...u, initCalls: initServiceCalls(u, src) }));
506507
return auditUnits(units);
507508
};

scripts/check-kernel-hook-pairs.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import { readFileSync, readdirSync, statSync } from 'node:fs';
7171
import { join, relative, sep } from 'node:path';
7272
import { fileURLToPath } from 'node:url';
7373
import ts from 'typescript';
74+
import { parseSourceFile } from './ts-parse.mjs';
7475
import { isEntrypoint } from './invoked-as.mjs';
7576

7677
const ROOT = join(fileURLToPath(new URL('.', import.meta.url)), '..');
@@ -104,7 +105,7 @@ const HOOK_NAME = /^kernel:[A-Za-z][A-Za-z0-9_:-]*$/;
104105
// ── Scanning ─────────────────────────────────────────────────────────────────
105106

106107
function parse(fileName, source) {
107-
return ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
108+
return parseSourceFile(fileName, source, ts.ScriptKind.TS);
108109
}
109110

110111
/** The identifier a call expression ends in: `a.b.c(x)` → `c`, `c(x)` → `c`. */

scripts/check-meta-type-normalized.mjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ import { readFileSync, readdirSync, statSync } from 'node:fs';
8282
import { join, relative, sep } from 'node:path';
8383
import { fileURLToPath } from 'node:url';
8484
import ts from 'typescript';
85+
import { parseSourceFile } from './ts-parse.mjs';
8586

8687
const ROOT = join(fileURLToPath(new URL('.', import.meta.url)), '..');
8788

@@ -145,7 +146,7 @@ function walkFiles(dir, out) {
145146
/** Every raw-param decision site in one file. */
146147
function findViolations(file) {
147148
const text = readFileSync(file, 'utf8');
148-
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
149+
const source = parseSourceFile(file, text);
149150
const found = [];
150151

151152
const record = (node, kind) => {
@@ -198,7 +199,7 @@ function selfTest() {
198199
const ok2 = RestServer.metaTypeSingular(req.params.type) === 'book';
199200
const ok3 = metaType === 'doc';
200201
`;
201-
const source = ts.createSourceFile('fixture.ts', fixture, ts.ScriptTarget.Latest, true);
202+
const source = parseSourceFile('fixture.ts', fixture);
202203
const hits = [];
203204
const visit = (node) => {
204205
if (ts.isBinaryExpression(node) && COMPARISON_OPS.has(node.operatorToken.kind)

scripts/check-org-identifier.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ import { execFileSync } from 'node:child_process';
269269
import { readFileSync } from 'node:fs';
270270
import { join } from 'node:path';
271271
import ts from 'typescript';
272+
import { parseSourceFile } from './ts-parse.mjs';
272273
import { maskComments } from './js-comment-mask.mjs';
273274

274275
const ROOTS = ['examples', 'apps', 'packages'];
@@ -499,7 +500,7 @@ function parse(text, file) {
499500
// `scriptKind` left to the parser so `.tsx` / `.jsx` / `.mjs` are inferred
500501
// from the name. The parser is error-tolerant: a file it cannot fully parse
501502
// still yields the nodes around the failure rather than throwing.
502-
return ts.createSourceFile(file, text, ts.ScriptTarget.Latest, /* setParentNodes */ true);
503+
return parseSourceFile(file, text);
503504
}
504505

505506
// ── the gate ──────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)