From c4cab963490bae2019001d028f178c811114659b Mon Sep 17 00:00:00 2001 From: Erik Seliger Date: Sat, 22 Aug 2026 21:33:06 +0000 Subject: [PATCH 1/2] Connect prototype members across files Amp-Thread-ID: https://ampcode.com/threads/T-01a029e2-5095-710a-9cce-0c6d8645c173 --- .../input/prototype-members/package.json | 6 ++++ .../prototype-members/src/connection.mjs | 7 +++++ snapshots/input/prototype-members/src/use.mjs | 6 ++++ .../input/prototype-members/tsconfig.json | 11 +++++++ .../prototype-members/src/connection.mjs | 16 ++++++++++ .../output/prototype-members/src/use.mjs | 14 +++++++++ src/FileIndexer.ts | 30 +++++++++++++++++++ 7 files changed, 90 insertions(+) create mode 100644 snapshots/input/prototype-members/package.json create mode 100644 snapshots/input/prototype-members/src/connection.mjs create mode 100644 snapshots/input/prototype-members/src/use.mjs create mode 100644 snapshots/input/prototype-members/tsconfig.json create mode 100644 snapshots/output/prototype-members/src/connection.mjs create mode 100644 snapshots/output/prototype-members/src/use.mjs diff --git a/snapshots/input/prototype-members/package.json b/snapshots/input/prototype-members/package.json new file mode 100644 index 00000000..f5a01517 --- /dev/null +++ b/snapshots/input/prototype-members/package.json @@ -0,0 +1,6 @@ +{ + "name": "prototype-members", + "version": "1.0.0", + "private": true, + "type": "module" +} diff --git a/snapshots/input/prototype-members/src/connection.mjs b/snapshots/input/prototype-members/src/connection.mjs new file mode 100644 index 00000000..d395822a --- /dev/null +++ b/snapshots/input/prototype-members/src/connection.mjs @@ -0,0 +1,7 @@ +export function Connection() {} + +Connection.prototype = { + getSchemaVersion() { + return 0 + }, +} diff --git a/snapshots/input/prototype-members/src/use.mjs b/snapshots/input/prototype-members/src/use.mjs new file mode 100644 index 00000000..b972de6a --- /dev/null +++ b/snapshots/input/prototype-members/src/use.mjs @@ -0,0 +1,6 @@ +/** @import { Connection } from './connection.mjs' */ + +/** @param {Connection} connection */ +export function schemaVersion(connection) { + return connection.getSchemaVersion() +} diff --git a/snapshots/input/prototype-members/tsconfig.json b/snapshots/input/prototype-members/tsconfig.json new file mode 100644 index 00000000..8a630ef1 --- /dev/null +++ b/snapshots/input/prototype-members/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "target": "ES2022" + }, + "include": ["src/**/*.mjs"] +} diff --git a/snapshots/output/prototype-members/src/connection.mjs b/snapshots/output/prototype-members/src/connection.mjs new file mode 100644 index 00000000..d1cf1ba8 --- /dev/null +++ b/snapshots/output/prototype-members/src/connection.mjs @@ -0,0 +1,16 @@ +// language JavaScript +// < definition prototype-members 1.0.0 src/`connection.mjs`/ + +export function Connection() {} +// ^^^^^^^^^^ definition prototype-members 1.0.0 src/`connection.mjs`/Connection(). + +Connection.prototype = { +//^^^^^^^^^ reference prototype-members 1.0.0 src/`connection.mjs`/Connection(). +//^^^^^^^^^ reference local 3 +// ^^^^^^^^^ reference local 2 + getSchemaVersion() { +//^^^^^^^^^^^^^^^^ definition prototype-members 1.0.0 src/`connection.mjs`/Connection().getSchemaVersion(). + return 0 + }, +} + diff --git a/snapshots/output/prototype-members/src/use.mjs b/snapshots/output/prototype-members/src/use.mjs new file mode 100644 index 00000000..b90a2a2e --- /dev/null +++ b/snapshots/output/prototype-members/src/use.mjs @@ -0,0 +1,14 @@ +// language JavaScript +// < definition prototype-members 1.0.0 src/`use.mjs`/ + +/** @import { Connection } from './connection.mjs' */ + +/** @param {Connection} connection */ +export function schemaVersion(connection) { +// ^^^^^^^^^^^^^ definition prototype-members 1.0.0 src/`use.mjs`/schemaVersion(). +// ^^^^^^^^^^ definition prototype-members 1.0.0 src/`use.mjs`/schemaVersion().(connection) + return connection.getSchemaVersion() +// ^^^^^^^^^^ reference prototype-members 1.0.0 src/`use.mjs`/schemaVersion().(connection) +// ^^^^^^^^^^^^^^^^ reference prototype-members 1.0.0 src/`connection.mjs`/Connection().getSchemaVersion(). +} + diff --git a/src/FileIndexer.ts b/src/FileIndexer.ts index bcae42ee..f932ff4c 100644 --- a/src/FileIndexer.ts +++ b/src/FileIndexer.ts @@ -424,6 +424,28 @@ export class FileIndexer { } return relationships } + + private prototypeAssignmentOwner(node: ts.Node): ts.Declaration | undefined { + if (!ts.isObjectLiteralExpression(node)) { + return + } + const assignment = node.parent + if ( + !ts.isBinaryExpression(assignment) || + assignment.operatorToken.kind !== ts.SyntaxKind.EqualsToken || + assignment.right !== node || + !ts.isPropertyAccessExpression(assignment.left) || + assignment.left.name.text !== 'prototype' + ) { + return + } + let symbol = this.checker.getSymbolAtLocation(assignment.left.expression) + if (symbol && (symbol.flags & ts.SymbolFlags.Alias) !== 0) { + symbol = this.checker.getAliasedSymbol(symbol) + } + return symbol?.valueDeclaration ?? symbol?.declarations?.[0] + } + private scipSymbol(node: ts.Node): ScipSymbol { const fromCache: ScipSymbol | undefined = this.globalSymbolTable.get(node) || this.localSymbolTable.get(node) @@ -490,6 +512,14 @@ export class FileIndexer { } } + const prototypeOwner = this.prototypeAssignmentOwner(node) + if (prototypeOwner) { + // Methods in `Constructor.prototype = { ... }` belong to the constructor, + // not to an anonymous object local to this file. Giving that object the + // constructor's identity makes its members stable across files. + return this.cached(node, this.scipSymbol(prototypeOwner)) + } + const owner = this.scipSymbol(node.parent) if (owner.isEmpty() || owner.isLocal()) { return this.newLocalSymbol(node) From ae8b70fbdaff479780112ae7527bc1f265aa2381 Mon Sep 17 00:00:00 2001 From: Erik Seliger Date: Sat, 22 Aug 2026 23:51:25 +0000 Subject: [PATCH 2/2] Cover prototype property assignments Amp-Thread-ID: https://ampcode.com/threads/T-01a029e2-5095-710a-9cce-0c6d8645c173 --- .../prototype-members/src/connection.mjs | 4 ++ snapshots/input/prototype-members/src/use.mjs | 6 ++- .../prototype-members/src/connection.mjs | 9 +++- .../output/prototype-members/src/use.mjs | 14 +++++-- src/FileIndexer.ts | 42 +++++++++++++------ 5 files changed, 56 insertions(+), 19 deletions(-) diff --git a/snapshots/input/prototype-members/src/connection.mjs b/snapshots/input/prototype-members/src/connection.mjs index d395822a..14352345 100644 --- a/snapshots/input/prototype-members/src/connection.mjs +++ b/snapshots/input/prototype-members/src/connection.mjs @@ -4,4 +4,8 @@ Connection.prototype = { getSchemaVersion() { return 0 }, + getVersion: function () { + return 1 + }, + version: 1, } diff --git a/snapshots/input/prototype-members/src/use.mjs b/snapshots/input/prototype-members/src/use.mjs index b972de6a..b7781e42 100644 --- a/snapshots/input/prototype-members/src/use.mjs +++ b/snapshots/input/prototype-members/src/use.mjs @@ -2,5 +2,9 @@ /** @param {Connection} connection */ export function schemaVersion(connection) { - return connection.getSchemaVersion() + return [ + connection.getSchemaVersion(), + connection.getVersion(), + connection.version, + ] } diff --git a/snapshots/output/prototype-members/src/connection.mjs b/snapshots/output/prototype-members/src/connection.mjs index d1cf1ba8..131f6eee 100644 --- a/snapshots/output/prototype-members/src/connection.mjs +++ b/snapshots/output/prototype-members/src/connection.mjs @@ -6,11 +6,16 @@ export function Connection() {} Connection.prototype = { //^^^^^^^^^ reference prototype-members 1.0.0 src/`connection.mjs`/Connection(). -//^^^^^^^^^ reference local 3 -// ^^^^^^^^^ reference local 2 +// ^^^^^^^^^ reference prototype-members 1.0.0 src/`connection.mjs`/Connection(). getSchemaVersion() { //^^^^^^^^^^^^^^^^ definition prototype-members 1.0.0 src/`connection.mjs`/Connection().getSchemaVersion(). return 0 }, + getVersion: function () { +//^^^^^^^^^^ definition prototype-members 1.0.0 src/`connection.mjs`/Connection().getVersion. + return 1 + }, + version: 1, +//^^^^^^^ definition prototype-members 1.0.0 src/`connection.mjs`/Connection().version. } diff --git a/snapshots/output/prototype-members/src/use.mjs b/snapshots/output/prototype-members/src/use.mjs index b90a2a2e..b5dc1c4a 100644 --- a/snapshots/output/prototype-members/src/use.mjs +++ b/snapshots/output/prototype-members/src/use.mjs @@ -7,8 +7,16 @@ export function schemaVersion(connection) { // ^^^^^^^^^^^^^ definition prototype-members 1.0.0 src/`use.mjs`/schemaVersion(). // ^^^^^^^^^^ definition prototype-members 1.0.0 src/`use.mjs`/schemaVersion().(connection) - return connection.getSchemaVersion() -// ^^^^^^^^^^ reference prototype-members 1.0.0 src/`use.mjs`/schemaVersion().(connection) -// ^^^^^^^^^^^^^^^^ reference prototype-members 1.0.0 src/`connection.mjs`/Connection().getSchemaVersion(). + return [ + connection.getSchemaVersion(), +// ^^^^^^^^^^ reference prototype-members 1.0.0 src/`use.mjs`/schemaVersion().(connection) +// ^^^^^^^^^^^^^^^^ reference prototype-members 1.0.0 src/`connection.mjs`/Connection().getSchemaVersion(). + connection.getVersion(), +// ^^^^^^^^^^ reference prototype-members 1.0.0 src/`use.mjs`/schemaVersion().(connection) +// ^^^^^^^^^^ reference prototype-members 1.0.0 src/`connection.mjs`/Connection().getVersion. + connection.version, +// ^^^^^^^^^^ reference prototype-members 1.0.0 src/`use.mjs`/schemaVersion().(connection) +// ^^^^^^^ reference prototype-members 1.0.0 src/`connection.mjs`/Connection().version. + ] } diff --git a/src/FileIndexer.ts b/src/FileIndexer.ts index f932ff4c..9b9f200c 100644 --- a/src/FileIndexer.ts +++ b/src/FileIndexer.ts @@ -426,14 +426,24 @@ export class FileIndexer { } private prototypeAssignmentOwner(node: ts.Node): ts.Declaration | undefined { - if (!ts.isObjectLiteralExpression(node)) { - return - } - const assignment = node.parent + const assignment = ts.isBinaryExpression(node) + ? node + : ts.isObjectLiteralExpression(node) + ? node.parent + : ts.isPropertyAccessExpression(node) + ? node.parent + : ts.isIdentifier(node) && ts.isPropertyAccessExpression(node.parent) + ? node.parent.parent + : (ts.isPropertyAssignment(node) || + ts.isShorthandPropertyAssignment(node)) && + ts.isObjectLiteralExpression(node.parent) + ? node.parent.parent + : undefined if ( + !assignment || !ts.isBinaryExpression(assignment) || assignment.operatorToken.kind !== ts.SyntaxKind.EqualsToken || - assignment.right !== node || + !ts.isObjectLiteralExpression(assignment.right) || !ts.isPropertyAccessExpression(assignment.left) || assignment.left.name.text !== 'prototype' ) { @@ -462,6 +472,20 @@ export class FileIndexer { } return this.cached(node, package_) } + + const prototypeOwner = this.prototypeAssignmentOwner(node) + if (prototypeOwner) { + // Declarations attached to the assignment and its object literal belong + // to the constructor. Property assignments need their own stable member + // descriptor instead of the counter-based object-property fallback. + const owner = this.scipSymbol(prototypeOwner) + const symbol = + ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node) + ? ScipSymbol.global(owner, termDescriptor(node.name.getText())) + : owner + return this.cached(node, symbol) + } + if ( ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node) @@ -512,14 +536,6 @@ export class FileIndexer { } } - const prototypeOwner = this.prototypeAssignmentOwner(node) - if (prototypeOwner) { - // Methods in `Constructor.prototype = { ... }` belong to the constructor, - // not to an anonymous object local to this file. Giving that object the - // constructor's identity makes its members stable across files. - return this.cached(node, this.scipSymbol(prototypeOwner)) - } - const owner = this.scipSymbol(node.parent) if (owner.isEmpty() || owner.isLocal()) { return this.newLocalSymbol(node)