Skip to content

Commit d510921

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-18306-fieldgroups-role-word
2 parents 7c01f9b + 42f8df1 commit d510921

3 files changed

Lines changed: 900 additions & 88 deletions

File tree

‎scripts/check-cross-package-test-inputs.mjs‎

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -412,16 +412,17 @@ const SELF_TEST_BATTERIES = Object.freeze({
412412
'the SPLIT test:repo task (#16466)': 16,
413413
'the node_modules REACH rule (#16555)': 18,
414414
'the TREE-SCOPED WALK (#15565)': 24,
415+
'the DOTTED NAMESPACE callee (#18643)': 15,
415416
'the WORKING-TREE blind spot (#18348)': 10,
416417
"the BARE SPECIFIER's PREMISE (#18236)": 20,
417418
});
418419

419420
// DELETING an entry silences that battery's floor exactly as effectively as
420-
// zeroing it, so the roster's own size is pinned too. Raised 10 -> 11 with the
421-
// #18236 battery, keeping the same one-row slack the roster has always carried:
421+
// zeroing it, so the roster's own size is pinned too. Raised 11 -> 12 with the
422+
// #18643 battery, keeping the same one-row slack the roster has always carried:
422423
// a floor left behind while the roster grows stops pinning the newest battery,
423424
// which is the one nothing else has learned to expect yet.
424-
const SELF_TEST_BATTERY_FLOOR = 11;
425+
const SELF_TEST_BATTERY_FLOOR = 12;
425426

426427
// The key an assertion is filed under when no battery is open. It is not a
427428
// declared battery, so it reds by the same set difference rather than silently
@@ -2100,6 +2101,10 @@ function expandLiteralSets(expr, sets) {
21002101
* ⚠️ An unlisted callee is TREATED AS A DESCENT, which is the safe direction
21012102
* here for the same reason the flat literal collector takes quoted paths without
21022103
* parsing: it can only force a declaration nobody needed, never withdraw one.
2104+
*
2105+
* ⚠️ This roster is matched against the callee's LAST dotted segment, so it holds
2106+
* METHOD names only. An OBJECT name needs the receiver roster below it — see
2107+
* `NON_DESCENT_NAMESPACES` for why the two questions are both asked.
21032108
*/
21042109
const NON_DESCENT_CALLEES = new Set([
21052110
...PATH_ARG_READS,
@@ -2112,6 +2117,41 @@ const NON_DESCENT_CALLEES = new Set([
21122117
'String', 'Number', 'Boolean', 'Set', 'Map', 'Array', 'Object', 'JSON', 'require', 'import',
21132118
]);
21142119

2120+
/**
2121+
* Receivers whose MEMBERS cannot be a descent entry either — the other half of
2122+
* the roster above, asked of the callee's FIRST dotted segment.
2123+
*
2124+
* One roster matched one way was blind in a shape it looked like it covered. The
2125+
* match takes the callee's LAST dotted segment, which is the right question for a
2126+
* method name — `path.join(...)` pops to `join`, `fs.readdirSync(...)` to
2127+
* `readdirSync` — and the wrong one for an object name: `JSON.stringify(<path>)`
2128+
* pops to `stringify`, so the `'JSON'` row could only ever match a bare `JSON(...)`
2129+
* call. Measured over the 3733 test files this gate scans (this repo at
2130+
* 8904880601): `JSON` was reached 8 times, every one of them the word inside a
2131+
* comment or a message, while 3220 `JSON.<member>(` call sites passed the guard and
2132+
* entered as descents; `Object` 4 (all prose) against 4451, `vi` 0 against 10835.
2133+
* Those rows were not narrow, they were DEAD, and the first argument of every
2134+
* missed call became a candidate walk root — which is how a `JSON.stringify(...)`
2135+
* of a path inside a generated shell script came to propose `packages/spec/dist`
2136+
* as a tree this gate walks.
2137+
*
2138+
* So a dotted callee is asked both questions: its last segment against
2139+
* NON_DESCENT_CALLEES, its receiver against this set. Every name here is already a
2140+
* row above, and the receiver leg is asked of DOTTED callees only, so a bare call
2141+
* keeps exactly the verdict it had.
2142+
*
2143+
* ⚠️ The safe direction is unchanged — an unlisted name is still TREATED AS A
2144+
* DESCENT — so a row here has to earn it: no member of these namespaces can hand a
2145+
* directory to a walker. The built-in globals' statics are data operations, and
2146+
* the vitest namespaces' members take mock factories, matchers and case tables.
2147+
* ⛔ A namespace any member of which reads the filesystem does not belong here;
2148+
* `fs` and `path` are deliberately absent, and their methods are rows above.
2149+
*/
2150+
const NON_DESCENT_NAMESPACES = new Set([
2151+
'String', 'Number', 'Boolean', 'Set', 'Map', 'Array', 'Object', 'JSON', 'URL', 'require', 'import',
2152+
'describe', 'it', 'test', 'expect', 'vi',
2153+
]);
2154+
21152155
/**
21162156
* The roots a directory descent in `src` is started from, as repo-relative
21172157
* names (`''` is the repo root).
@@ -2145,7 +2185,14 @@ export function descentRoots(src, hereDepth, fileSegs, known) {
21452185
};
21462186
const sets = literalStringSets(src);
21472187
for (const m of src.matchAll(/\b([A-Za-z_$][\w$.]*)\s*\(/g)) {
2148-
if (NON_DESCENT_CALLEES.has(m[1].split('.').pop())) continue;
2188+
const segs = m[1].split('.');
2189+
// Two questions, one per kind of name the roster holds: a METHOD is
2190+
// recognised wherever it sits, a NAMESPACE only in the one position it can
2191+
// occupy. The receiver leg reads `segs[0]` and not any middle segment, so the
2192+
// roster's reach is exactly one receiver deep and `x.JSON.stringify(dir)`
2193+
// stays a descent.
2194+
if (NON_DESCENT_CALLEES.has(segs[segs.length - 1])) continue;
2195+
if (segs.length > 1 && NON_DESCENT_NAMESPACES.has(segs[0])) continue;
21492196
const args = balancedArgs(src, m.index + m[0].length);
21502197
if (args === null) continue;
21512198
// The entry argument, by position: a walker takes the directory first.
@@ -3736,6 +3783,61 @@ function selfTest() {
37363783
);
37373784
}
37383785

3786+
// ── the DOTTED NAMESPACE callee (#18643) ─────────────────────────────────
3787+
//
3788+
// The roster is matched against the callee's LAST dotted segment, so an
3789+
// OBJECT-named row only ever matched a BARE call: `JSON.stringify(<path>)`
3790+
// popped to `stringify`, entered as a descent, and its first argument became a
3791+
// candidate walk root -- which is how a path inside a generated shell script
3792+
// came to name `packages/spec/dist` as a tree this gate walks.
3793+
//
3794+
// Both halves are pinned, because either alone is satisfiable by a scan that
3795+
// simply stopped answering: the LIT legs assert a namespace member is no longer
3796+
// a descent, and the two controls above them assert that an unrostered callee
3797+
// handed the SAME argument in the SAME position still is. The DARK legs hold
3798+
// the rows that were live -- a bare `String(...)` or `require(...)` -- to the
3799+
// verdict they already had.
3800+
battery('the DOTTED NAMESPACE callee (#18643)');
3801+
{
3802+
const SEGS = ['packages', 'pkg', 'src', 'x.test.ts'];
3803+
const SEED =
3804+
'const HERE = dirname(fileURLToPath(import.meta.url));\n' + "const REPO = resolve(HERE, '../../..');\n";
3805+
// The anonymous descent the walk limb needs before it engages at all: without
3806+
// it every sample below yields nothing and every leg is green for the wrong
3807+
// reason. The positive controls are what prove it is engaged.
3808+
const WALKER = 'const walk = (dir) => { for (const e of readdirSync(dir)) walk(join(dir, e)); };\n';
3809+
const ARG = "join(REPO, 'content')";
3810+
const isRoot = (src) => walkRootsOf(SEED + WALKER + src, 1, SEGS).includes('content');
3811+
3812+
ok(
3813+
'POSITIVE CONTROL: an unrostered BARE callee handed the argument is a descent',
3814+
isRoot(`const s = stringify(${ARG});`),
3815+
);
3816+
ok(
3817+
'POSITIVE CONTROL: an unrostered DOTTED callee is one too -- the receiver leg suppresses by ROSTER, not by the dot',
3818+
isRoot(`helper.crawl(${ARG});`),
3819+
);
3820+
ok('LIT: `JSON.stringify(<path>)` is not a descent -- the reported shape', !isRoot(`const s = JSON.stringify(${ARG});`));
3821+
ok('LIT: `Object.keys(<path>)` is not a descent', !isRoot(`const k = Object.keys(${ARG});`));
3822+
ok('LIT: `Array.from(<path>)` is not a descent', !isRoot(`const a = Array.from(${ARG});`));
3823+
ok('LIT: `vi.mock(<path>)` is not a descent -- the `vi` row reached nothing at all before', !isRoot(`vi.mock(${ARG});`));
3824+
ok('LIT: `expect.any(<path>)` is not a descent', !isRoot(`expect.any(${ARG});`));
3825+
ok('DARK: bare `String(<path>)` keeps its verdict', !isRoot(`const s = String(${ARG});`));
3826+
ok('DARK: bare `require(<path>)` keeps its verdict', !isRoot(`const m = require(${ARG});`));
3827+
ok('DARK: bare `Number(<path>)` keeps its verdict', !isRoot(`const n = Number(${ARG});`));
3828+
ok('DARK: `new Set(<path>)` keeps its verdict', !isRoot(`const s = new Set(${ARG});`));
3829+
ok('a rostered METHOD still suppresses when dotted: `fs.readdirSync(<path>)`', !isRoot(`fs.readdirSync(${ARG});`));
3830+
ok('and when bare: `readdirSync(<path>)` -- what the last-segment match buys', !isRoot(`readdirSync(${ARG});`));
3831+
ok(
3832+
'REACH: the receiver leg reads the FIRST segment only, so `x.JSON.stringify(<path>)` is still a descent',
3833+
isRoot(`x.JSON.stringify(${ARG});`),
3834+
);
3835+
ok(
3836+
'every namespace row is also a callee row, so no BARE call changed hands with this limb',
3837+
[...NON_DESCENT_NAMESPACES].every((n) => NON_DESCENT_CALLEES.has(n)),
3838+
);
3839+
}
3840+
37393841
// ── the WORKING-TREE blind spot (#18348) ──────────────────────────
37403842
//
37413843
// The three legs the card demands, driven on a REAL git tree rather than

0 commit comments

Comments
 (0)