@@ -541,19 +541,62 @@ function opensClassScope(node) {
541541}
542542
543543/**
544- * Where a CALLABLE'S OWN NAME is declared: the lexical scopes, plus the class.
544+ * Where a CALLABLE'S OWN NAME is declared -- a RECORDING predicate, ⛔ not a
545+ * lookup one: the lexical scopes, plus the class.
545546 *
546547 * A method's name belongs to its class body, a function declaration's and a
547548 * `const fn = () => …`'s to the block or file that declares it -- so this is
548549 * {@link opensScope} with the class added, and not either one alone. ⛔ Not the
549550 * same predicate as {@link opensClassScope}: a method named `getEngine` is
550551 * class-scoped, while a `function getEngine()` two lines above the class is not,
551552 * and a chain that saw only classes would lose the second one.
553+ *
554+ * ⭐ That reasoning answers WHERE A NAME IS DECLARED, and it is the whole
555+ * question only while a declaration is being recorded. A CALL SITE asks a
556+ * different question -- which declaration THIS call reaches -- and the answer
557+ * there is keyed on how the call is WRITTEN. ⛔ Reading a bare `getEngine()`
558+ * through this union is how a method came to shadow a file-level function that
559+ * the language would never let it shadow. {@link calleeScopeChain} is the
560+ * lookup-side predicate; the two are deliberately not the same function.
552561 */
553562function opensCallableScope ( node ) {
554563 return opensScope ( node ) || opensClassScope ( node ) ;
555564}
556565
566+ /**
567+ * ⭐ The scope chain a CALL SITE resolves its callee through -- by how the call
568+ * is WRITTEN, which is not the question {@link opensCallableScope} answers.
569+ *
570+ * One map records every callable's name at the scope that declares it, and that
571+ * map is read from three different call shapes, each of which the language
572+ * resolves differently:
573+ *
574+ * • `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
577+ * `class C { getEngine() {…} }` does not shadow a
578+ * `function getEngine()` for a call written `getEngine()`
579+ * inside `C`. Reading it through the class subtracted a real
580+ * engine write under the DEFENDED `platform-type` arm, which
581+ * prints nothing and is counted nowhere.
582+ * • `this.m()` -- a method name, which belongs to the enclosing class exactly
583+ * as a property does, and to no lexical scope.
584+ * • `x.m()` -- a member of whatever `x` is, and this module has no index of
585+ * class members to read that off (interfaces and type-literal
586+ * aliases only, via {@link memberTypeOfShapes}, tried before
587+ * this chain). `null` ⇒ the file-wide FLOOR alone, which is
588+ * exactly what this receiver resolved through before there
589+ * were any scopes at all. ⛔ Not a placement this module can
590+ * justify -- it is the one it can defend as unchanged.
591+ */
592+ function calleeScopeChain ( callee ) {
593+ if ( ts . isIdentifier ( callee ) ) return opensScope ;
594+ if ( ts . isPropertyAccessExpression ( callee ) && callee . expression . kind === ts . SyntaxKind . ThisKeyword ) {
595+ return opensClassScope ;
596+ }
597+ return null ;
598+ }
599+
557600/**
558601 * The scopes enclosing a node, innermost first, under `opens`.
559602 *
@@ -617,10 +660,18 @@ function scopedNames(opens = opensScope) {
617660 scopeMap ( declNode ) . set ( name , entry ) ;
618661 flat . set ( name , entry ) ;
619662 } ,
620- /** The declaration `name` refers to AT `node` -- lexical tier, then the floor. */
621- lookup ( name , node ) {
622- if ( node ) {
623- for ( const scope of scopeChainOf ( node , opens ) ) {
663+ /**
664+ * The declaration `name` refers to AT `node` -- lexical tier, then the floor.
665+ *
666+ * `chain` defaults to the predicate this map RECORDS under, which is the
667+ * right answer wherever the name is looked up the same way it is declared.
668+ * A callable's name is not: it is recorded where it is declared and read by
669+ * how the call is written, so {@link resolveReceiver} passes the chain from
670+ * {@link calleeScopeChain} instead. `null` asks for the FLOOR alone.
671+ */
672+ lookup ( name , node , chain = opens ) {
673+ if ( node && chain ) {
674+ for ( const scope of scopeChainOf ( node , chain ) ) {
624675 const hit = byScope . get ( scope ) ?. get ( name ) ;
625676 if ( hit ) return hit ;
626677 }
@@ -851,7 +902,7 @@ export function resolveReceiver(recvNode, sf, decls, index, depth = 0) {
851902 return inlineEngineDoorOrOther ( mt , `${ fname } () return` , sf ) ;
852903 }
853904 }
854- const entry = fname ? decls . fnReturns . lookup ( fname , callee ) : null ;
905+ const entry = fname ? decls . fnReturns . lookup ( fname , callee , calleeScopeChain ( callee ) ) : null ;
855906 if ( entry ) return fromEntry ( entry , `${ fname } ()` ) ;
856907 return { kind : 'unresolved' , how : 'call' , detail : receiverKey ( r , sf ) } ;
857908 }
@@ -2481,6 +2532,39 @@ export function selfTest() {
24812532 + `export function w(d: Deps) {\n d.getEngine().${ WRITE } ;\n}\n` , [ 'IProbeEngine' ] ) ,
24822533 'engine/IProbeEngine' ) ;
24832534
2535+ // ── ⭐⭐ ONE MAP, THREE CALL SHAPES ────────────────────────────────
2536+ // The cases above all read the callable map the way its name was DECLARED.
2537+ // These four read it the way the call is WRITTEN, which is the other question
2538+ // and the one the lookup site actually asks. A single chain answering both is
2539+ // wrong for one of them: reading a BARE `getEngine()` through the enclosing
2540+ // class body lets a method shadow a file-level function that the language
2541+ // would never let it shadow -- and in this instrument that is the quiet
2542+ // direction again, a real engine write subtracted under `platform-type`, an
2543+ // arm that DEFENDS the subtraction and so prints nothing and is counted
2544+ // nowhere. ⛔ Pinned in BOTH declaration orders, because the floor decides the
2545+ // one the lexical tier does not reach and the two orders disagree there.
2546+ t ( '⭐⭐ a BARE call resolves lexically -- a method never shadows a file-level function of the same name' ,
2547+ verdictsIn ( 'function getEngine(): IProbeEngine { return null as never; }\n'
2548+ + 'class C {\n getEngine(): Map<string, number> { return new Map(); }\n'
2549+ + ` w() { getEngine().${ WRITE } ; }\n}\n` , [ 'IProbeEngine' ] ) ,
2550+ 'engine/IProbeEngine' ) ;
2551+ t ( '⭐⭐ …and in the other declaration order, where the file-wide floor would have answered the method' ,
2552+ verdictsIn ( 'class C {\n getEngine(): Map<string, number> { return new Map(); }\n'
2553+ + ` w() { getEngine().${ WRITE } ; }\n}\n`
2554+ + 'function getEngine(): IProbeEngine { return null as never; }\n' , [ 'IProbeEngine' ] ) ,
2555+ 'engine/IProbeEngine' ) ;
2556+ t ( '⭐ a `this.<method>()` call DOES read the enclosing class, so its own method wins over a same-named function' ,
2557+ verdictsIn ( 'function getEngine(): IProbeEngine { return null as never; }\n'
2558+ + 'class C {\n getEngine(): Map<string, number> { return new Map(); }\n'
2559+ + " w() { this.getEngine().delete('k'); }\n}\n" , [ 'IProbeEngine' ] ) ,
2560+ 'other/platform-type' ) ;
2561+ t ( '⛔ FLOOR: an `x.<method>()` call is not the enclosing class\'s method -- the site\'s own class does not capture it' ,
2562+ verdictsIn ( 'class A {\n getEngine(): IProbeEngine { return null as never; }\n}\n'
2563+ + 'class B {\n constructor(private readonly x: A) {}\n'
2564+ + ' getEngine(): Map<string, number> { return new Map(); }\n'
2565+ + ` w() { this.x.getEngine().${ WRITE } ; }\n}\n` , [ 'IProbeEngine' ] ) ,
2566+ 'engine/IProbeEngine' ) ;
2567+
24842568 // The same conflation decided two OTHER questions, and both are verdicts the
24852569 // artefacts carry: WHICH object a site writes, and whether it is elevated.
24862570 /** Every write call's object-name verdict, in source order. */
@@ -2554,7 +2638,10 @@ export function selfTest() {
25542638 + 'two places a name is NOT lexical: two classes sharing a property name are two properties at both '
25552639 + 'storage sites and through a `this.<base>.<member>` base, two same-named local functions are two '
25562640 + 'callables, each in both declaration orders, while a `this.<prop>` no enclosing class declares and '
2557- + 'a method called from outside its class both still resolve file-wide).' ,
2641+ + 'a method called from outside its class both still resolve file-wide -- and the callable map read '
2642+ + '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).' ,
25582645 ) ;
25592646 return 0 ;
25602647}
0 commit comments