Skip to content

Commit 9a24005

Browse files
committed
fix(census): record an object-literal method under its literal, not its block
`ts.isMethodDeclaration` is true of `const o = { getEngine() {…} }` as well as of a class method, and an object literal opens no scope under `opensCallableScope` -- so such a method was recorded as a lexical callable of the enclosing BLOCK, and a bare `getEngine()` written in that block resolved to the object's method. The language never does that: a name declared only that way is `TS2304: Cannot find name`. Measured at the previous head: a file-level `function getEngine(): IProbeEngine` beside a block-local object literal declaring `getEngine(): Map<string, number>` answered `other/platform-type` for a bare `getEngine().insert(…)` in that block, in BOTH declaration orders, while `origin/main` answered the engine in one of them and `tsc --noEmit --strict` answers the engine in both. That is a real engine write subtracted under the DEFENDED `platform-type` arm, which prints nothing and is counted nowhere -- the direction this census's header names as the one it cannot survive. `opensCallableScope` now also opens an object literal, so a member name is recorded under ITS CONTAINER exactly as a class method's is. No lookup chain walks a container (`calleeScopeChain` reaches the class for `this.m()` and nothing for the rest), so the method stays reachable through the file-wide FLOOR exactly as before and no receiver that resolved stops resolving. Pinned in both declaration orders; the docblocks and the self-test success line no longer claim a property the shape falsified. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
1 parent a36551a commit 9a24005

1 file changed

Lines changed: 58 additions & 12 deletions

File tree

‎scripts/tenant-audit-census.mjs‎

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -542,25 +542,39 @@ function opensClassScope(node) {
542542

543543
/**
544544
* Where a CALLABLE'S OWN NAME is declared -- a RECORDING predicate, ⛔ not a
545-
* lookup one: the lexical scopes, plus the class.
545+
* lookup one: the lexical scopes, plus every container whose members are NOT
546+
* lexical names -- the class, and the object literal.
546547
*
547548
* A method's name belongs to its class body, a function declaration's and a
548549
* `const fn = () => …`'s to the block or file that declares it -- so this is
549-
* {@link opensScope} with the class added, and not either one alone. ⛔ Not the
550-
* same predicate as {@link opensClassScope}: a method named `getEngine` is
551-
* class-scoped, while a `function getEngine()` two lines above the class is not,
552-
* and a chain that saw only classes would lose the second one.
550+
* {@link opensScope} with those containers added, and not either one alone.
551+
* ⛔ Not the same predicate as {@link opensClassScope}: a method named
552+
* `getEngine` is class-scoped, while a `function getEngine()` two lines above
553+
* the class is not, and a chain that saw only classes would lose the second one.
554+
*
555+
* ⭐ The object literal is on this list for the same reason the class is, and
556+
* omitting it cost the same site. `ts.isMethodDeclaration` is true for
557+
* `const o = { getEngine() {…} }` as well as for a class method, and an object
558+
* literal opens no lexical scope -- so such a method was recorded at the
559+
* enclosing BLOCK, and a bare `getEngine()` written in that block resolved to
560+
* it, which the language never does (the compiler reports `TS2304` for a name
561+
* declared only that way). ⇒ A member name is recorded under ITS CONTAINER, and
562+
* no lookup chain walks a container: {@link calleeScopeChain} reaches the class
563+
* for `this.m()` and nothing for the rest, so an object-literal method stays
564+
* reachable through the file-wide FLOOR exactly as it was before.
553565
*
554566
* ⭐ That reasoning answers WHERE A NAME IS DECLARED, and it is the whole
555567
* question only while a declaration is being recorded. A CALL SITE asks a
556568
* different question -- which declaration THIS call reaches -- and the answer
557569
* there is keyed on how the call is WRITTEN. ⛔ Reading a bare `getEngine()`
558570
* through this union is how a method came to shadow a file-level function that
559571
* the language would never let it shadow. {@link calleeScopeChain} is the
560-
* lookup-side predicate; the two are deliberately not the same function.
572+
* lookup-side predicate; the two are deliberately not the same function --
573+
* and BOTH halves are needed, because a chain that excludes a container cannot
574+
* help when the member was recorded outside that container in the first place.
561575
*/
562576
function opensCallableScope(node) {
563-
return opensScope(node) || opensClassScope(node);
577+
return opensScope(node) || opensClassScope(node) || ts.isObjectLiteralExpression(node);
564578
}
565579

566580
/**
@@ -572,13 +586,17 @@ function opensCallableScope(node) {
572586
* resolves differently:
573587
*
574588
* • `f()` -- a BARE IDENTIFIER is a lexical name and nothing else. A
575-
* method lives on the prototype, ⛔ never in lexical scope,
576-
* so the enclosing class body is not on this chain: a
589+
* method lives on its prototype or its object, ⛔ never in
590+
* lexical scope, so no member container is on this chain: a
577591
* `class C { getEngine() {…} }` does not shadow a
578592
* `function getEngine()` for a call written `getEngine()`
579593
* inside `C`. Reading it through the class subtracted a real
580594
* engine write under the DEFENDED `platform-type` arm, which
581-
* prints nothing and is counted nowhere.
595+
* prints nothing and is counted nowhere. ⚠️ Excluding the
596+
* container here is only half of that property: it also
597+
* depends on {@link opensCallableScope} RECORDING every
598+
* member name under its container, which is why an object
599+
* literal is on that predicate.
582600
* • `this.m()` -- a method name, which belongs to the enclosing class exactly
583601
* as a property does, and to no lexical scope.
584602
* • `x.m()` -- a member of whatever `x` is, and this module has no index of
@@ -2565,6 +2583,32 @@ export function selfTest() {
25652583
+ ` w() { this.x.getEngine().${WRITE}; }\n}\n`, ['IProbeEngine']),
25662584
'engine/IProbeEngine');
25672585

2586+
// ── ⭐⭐ A MEMBER NAME IS NOT A LEXICAL ONE ON THE RECORDING SIDE EITHER ───
2587+
// Keeping the class body off a bare call's chain is only half of "a bare
2588+
// `f()` is never shadowed by a method". A name is reachable through whatever
2589+
// scope it was RECORDED at, so a member name recorded at a LEXICAL scope is
2590+
// found by a bare call however careful the chain is. `ts.isMethodDeclaration`
2591+
// is true of an OBJECT LITERAL's method too, and an object literal opens no
2592+
// lexical scope -- so one was recorded at the enclosing BLOCK, and a bare
2593+
// `getEngine()` in that block resolved to the object's method. The compiler
2594+
// never does that: declared ONLY that way, the name is `TS2304: Cannot find
2595+
// name`. Quiet direction again -- a real engine write subtracted under
2596+
// `platform-type`, an arm that DEFENDS the subtraction and so prints nothing
2597+
// and is counted nowhere. ⛔ Pinned in BOTH declaration orders: the floor
2598+
// decides the order the lexical tier does not reach, and the two disagree
2599+
// there, so one order alone can pass on the floor's answer by luck.
2600+
const objectLiteralMethod = 'export function w() {\n'
2601+
+ ' const o = { getEngine(): Map<string, number> { return new Map(); } };\n'
2602+
+ ' void o;\n'
2603+
+ ` getEngine().${WRITE};\n}\n`;
2604+
const fileLevelGetEngine = 'function getEngine(): IProbeEngine { return null as never; }\n';
2605+
t('⭐⭐ a BARE call is not shadowed by an OBJECT LITERAL method of the same name in the same block',
2606+
verdictsIn(fileLevelGetEngine + objectLiteralMethod, ['IProbeEngine']),
2607+
'engine/IProbeEngine');
2608+
t('⭐⭐ …and in the other declaration order, where the file-wide floor would have answered the method',
2609+
verdictsIn(objectLiteralMethod + fileLevelGetEngine, ['IProbeEngine']),
2610+
'engine/IProbeEngine');
2611+
25682612
// The same conflation decided two OTHER questions, and both are verdicts the
25692613
// artefacts carry: WHICH object a site writes, and whether it is elevated.
25702614
/** Every write call's object-name verdict, in source order. */
@@ -2640,8 +2684,10 @@ export function selfTest() {
26402684
+ 'callables, each in both declaration orders, while a `this.<prop>` no enclosing class declares and '
26412685
+ 'a method called from outside its class both still resolve file-wide -- and the callable map read '
26422686
+ 'by how the CALL is written rather than by where the name was declared: a bare `f()` resolves '
2643-
+ 'lexically and is never shadowed by a same-named method of the enclosing class, in both '
2644-
+ 'declaration orders, while `this.m()` does read that class and `x.m()` reads neither).',
2687+
+ 'lexically and is never shadowed by a same-named method -- neither one on the enclosing class, '
2688+
+ 'which the lookup chain excludes, nor one on an OBJECT LITERAL in the same block, which is kept '
2689+
+ 'off that chain by being recorded under the literal -- in both declaration orders each, while '
2690+
+ '`this.m()` does read that class and `x.m()` reads neither).',
26452691
);
26462692
return 0;
26472693
}

0 commit comments

Comments
 (0)