-
Notifications
You must be signed in to change notification settings - Fork 15
feat(resolver): resolve this-dispatch inside Object.defineProperty accessor functions (JS) #1346
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0c83401
cb58c4c
e3840cb
ca17f13
1095e3c
63a95ca
8690fdf
b2dd6cc
5f9b9e1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -709,6 +709,94 @@ function buildFnRefBindingsPtsPostPass( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Object.defineProperty accessor post-pass for the native call-edge path. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * When a function is registered as a getter/setter via | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * `Object.defineProperty(obj, "bar", { get: getter })`, calls to `this.X()` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * inside `getter` need to resolve against `obj` (because `this === obj` when | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * the accessor is invoked). The native Rust engine has no knowledge of | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * `definePropertyReceivers`, so this JS post-pass adds the missing edges. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function buildDefinePropertyPostPass( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ctx: PipelineContext, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getNodeIdStmt: NodeIdStmt, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| allEdgeRows: EdgeRowTuple[], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sharedLookup?: CallNodeLookup, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const filesWithReceivers = [...ctx.fileSymbols].filter( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ([, symbols]) => symbols.definePropertyReceivers && symbols.definePropertyReceivers.size > 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (filesWithReceivers.length === 0) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const seenByPair = new Set<string>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const [srcId, tgtId] of allEdgeRows) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| seenByPair.add(`${srcId}|${tgtId}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { barrelOnlyFiles, rootDir } = ctx; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const lookup = sharedLookup ?? makeContextLookup(ctx, getNodeIdStmt); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const [relPath, symbols] of filesWithReceivers) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (barrelOnlyFiles.has(relPath)) continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!fileNodeRow) continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const typeMap: Map<string, TypeMapEntry | string> = symbols.typeMap || new Map(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const definePropertyReceivers = symbols.definePropertyReceivers!; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const call of symbols.calls) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (call.receiver !== 'this') continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!caller.callerName) continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const receiverVarName = definePropertyReceivers.get(caller.callerName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!receiverVarName) continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Only add edges the native engine missed (no direct target already). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { targets: directTargets } = resolveCallTargets( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lookup, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| call, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| relPath, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| importedNames, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| typeMap as Map<string, unknown>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| caller.callerName, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (directTargets.length > 0) continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Resolve via receiver type | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let targets: ReadonlyArray<{ id: number; file: string }> = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const typeEntry = typeMap.get(receiverVarName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const typeName = typeEntry | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? typeof typeEntry === 'string' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? typeEntry | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : (typeEntry as { type?: string }).type | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeName) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const qualifiedName = `${typeName}.${call.name}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| targets = lookup.byNameAndFile(qualifiedName, relPath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Same-file fallback for plain object-literal methods | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (targets.length === 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| targets = lookup.byNameAndFile(call.name, relPath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const t of targets) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const edgeKey = `${caller.id}|${t.id}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (t.id !== caller.id && !seenByPair.has(edgeKey)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const conf = computeConfidence(relPath, t.file, null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (conf > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| seenByPair.add(edgeKey); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'ts-native']); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Phase 8.5: CHA + RTA post-pass for the native call-edge path. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1020,6 +1108,50 @@ function buildFileCallEdges( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Object.defineProperty accessor fallback: when a function is registered as | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // a getter/setter via `Object.defineProperty(obj, "bar", { get: getter })`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // calls to `this.X()` inside `getter` resolve against `obj` (this === obj | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // when the accessor is invoked). If the same-class fallback above found | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // nothing, try treating `obj` as the receiver and look up `obj.X` in the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // typeMap, or fall back to a same-file lookup of any definition named X | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // that belongs to the object literal or its type. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| targets.length === 0 && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| call.receiver === 'this' && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| caller.callerName != null && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| symbols.definePropertyReceivers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const receiverVarName = symbols.definePropertyReceivers.get(caller.callerName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (receiverVarName) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Try typeMap lookup for receiver.methodName | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const typeEntry = typeMap.get(receiverVarName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const typeName = typeEntry | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? typeof typeEntry === 'string' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? typeEntry | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : (typeEntry as { type?: string }).type | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeName) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const qualifiedName = `${typeName}.${call.name}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const qualified = lookup.byNameAndFile(qualifiedName, relPath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (qualified.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| targets = [...qualified]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // If still no targets, search for any definition named `call.name` in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // the same file — handles plain object literals where the method isn't | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // qualified (e.g. `const obj = { baz() {} }` defines `baz` directly). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Note: this is intentionally broad — it matches any same-file definition | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // with the called name, not just members of the receiver object. This is | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // the same behaviour used by the native post-pass path (buildDefinePropertyPostPass). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (targets.length === 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const sameFile = lookup.byNameAndFile(call.name, relPath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (sameFile.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| targets = [...sameFile]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1133
to
+1151
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed — removed both
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed — but used spread syntax ([...qualified] and [...sameFile]) rather than direct assignment. The original no-op .filter() was inadvertently serving as a ReadonlyArray-to-mutable-Array conversion: .filter() on a ReadonlyArray returns a new mutable Array. Removing it caused TS4104 errors at both lines (ReadonlyArray cannot be assigned to the mutable targets). Spreading into a new array is the clean fix. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const t of targets) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const edgeKey = `${caller.id}|${t.id}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (t.id !== caller.id) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1482,6 +1614,9 @@ export async function buildEdges(ctx: PipelineContext): Promise<void> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // (e.g. `const f = fn.bind(ctx)`), so calls to bind-created aliases are | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // not resolved to their original function on the native path. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| buildFnRefBindingsPtsPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Object.defineProperty accessor post-pass: resolve this-dispatch inside | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // getter/setter functions registered via Object.defineProperty. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| buildDefinePropertyPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Phase 8.5 post-pass: augment native call edges with CHA-resolved dispatch. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // The native Rust engine has no knowledge of the CHA context, so this/self | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // calls and interface dispatch are not expanded to concrete implementations. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -359,6 +359,10 @@ function extractSymbolsQuery(tree: TreeSitterTree, query: TreeSitterQuery): Extr | |
| const newExpressions: string[] = []; | ||
| extractNewExpressionsWalk(tree.rootNode, newExpressions); | ||
|
|
||
| // Object.defineProperty accessor receiver bindings | ||
| const definePropertyReceivers: Map<string, string> = new Map(); | ||
| extractDefinePropertyReceiversWalk(tree.rootNode, definePropertyReceivers); | ||
|
|
||
| return { | ||
| definitions, | ||
| calls, | ||
|
|
@@ -371,6 +375,7 @@ function extractSymbolsQuery(tree: TreeSitterTree, query: TreeSitterQuery): Extr | |
| fnRefBindings, | ||
| paramBindings, | ||
| newExpressions, | ||
| ...(definePropertyReceivers.size > 0 ? { definePropertyReceivers } : {}), | ||
| }; | ||
| } | ||
|
|
||
|
|
@@ -629,6 +634,10 @@ function extractSymbolsWalk(tree: TreeSitterTree): ExtractorOutput { | |
| const newExpressions: string[] = []; | ||
| extractNewExpressionsWalk(tree.rootNode, newExpressions); | ||
| ctx.newExpressions = newExpressions; | ||
| // Object.defineProperty accessor receiver bindings | ||
| const definePropertyReceivers: Map<string, string> = new Map(); | ||
| extractDefinePropertyReceiversWalk(tree.rootNode, definePropertyReceivers); | ||
| if (definePropertyReceivers.size > 0) ctx.definePropertyReceivers = definePropertyReceivers; | ||
| return ctx; | ||
| } | ||
|
|
||
|
|
@@ -1415,6 +1424,79 @@ function extractNewExpressionsWalk(rootNode: TreeSitterNode, newExpressions: str | |
| walk(rootNode, 0); | ||
| } | ||
|
|
||
| /** | ||
| * Walk the AST to find `Object.defineProperty(obj, "bar", { get: getter })` patterns | ||
| * and record which functions are used as getter/setter accessors for which objects. | ||
| * | ||
| * Result is stored in the provided map as `funcName → receiverVarName`. | ||
| */ | ||
| function extractDefinePropertyReceiversWalk( | ||
| rootNode: TreeSitterNode, | ||
| out: Map<string, string>, | ||
| ): void { | ||
| function walk(node: TreeSitterNode, depth: number): void { | ||
| if (depth >= MAX_WALK_DEPTH) return; | ||
| if (node.type === 'call_expression') { | ||
| const fn = node.childForFieldName('function'); | ||
| // Match `Object.defineProperty` | ||
| if (fn?.type === 'member_expression') { | ||
| const obj = fn.childForFieldName('object'); | ||
| const prop = fn.childForFieldName('property'); | ||
| if ( | ||
| obj?.type === 'identifier' && | ||
| obj.text === 'Object' && | ||
| prop?.text === 'defineProperty' | ||
| ) { | ||
| const argsNode = node.childForFieldName('arguments') ?? findChild(node, 'arguments'); | ||
| if (argsNode) { | ||
| // Collect non-punctuation children: arg0 (target obj), arg1 (prop name string), arg2 (descriptor) | ||
| const argChildren: TreeSitterNode[] = []; | ||
| for (let i = 0; i < argsNode.childCount; i++) { | ||
| const c = argsNode.child(i); | ||
| if (!c) continue; | ||
| if (c.type === ',' || c.type === '(' || c.type === ')') continue; | ||
| argChildren.push(c); | ||
| } | ||
| if (argChildren.length >= 3) { | ||
| const targetObj = argChildren[0]; | ||
| const descriptor = argChildren[2]; | ||
| if (targetObj?.type === 'identifier' && descriptor?.type === 'object') { | ||
| const targetName = targetObj.text; | ||
| // Walk the descriptor object's pair children looking for get/set | ||
| for (let i = 0; i < descriptor.childCount; i++) { | ||
| const pair = descriptor.child(i); | ||
| if (pair?.type !== 'pair') continue; | ||
| const key = pair.childForFieldName('key'); | ||
| const val = pair.childForFieldName('value'); | ||
| if ( | ||
| key && | ||
| (key.text === 'get' || key.text === 'set') && | ||
| val?.type === 'identifier' && | ||
| !BUILTIN_GLOBALS.has(val.text) | ||
| ) { | ||
| // Known limitation: if the same function is registered as an | ||
| // accessor on multiple objects, last-write-wins — only the | ||
| // last target object is retained. This is an unusual pattern | ||
| // (sharing one function across multiple defineProperty calls) | ||
| // and covering it would require Map<string, string[]> which | ||
| // changes the consumer API. Tracked as a known edge case. | ||
| out.set(val.text, targetName); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Acknowledged with a comment. A proper fix (allowing multiple receiver bindings per accessor function) would require changing |
||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| for (let i = 0; i < node.childCount; i++) { | ||
| const child = node.child(i); | ||
| if (child) walk(child, depth + 1); | ||
| } | ||
| } | ||
| walk(rootNode, 0); | ||
| } | ||
|
|
||
| /** | ||
| * Extract variable-to-type assignments into a per-file type map. | ||
| * | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the typeMap holds no type annotation for the receiver variable, both the WASM path (
buildFileCallEdges) and the native post-pass (buildDefinePropertyPostPass) fall back tolookup.byNameAndFile(call.name, relPath), which returns every definition namedcall.namein the file — not just members of the receiver object. In a file that has a top-level function sharing a name with a method on the accessor target, both would be emitted as call targets. The test fixture avoids this collision, but the JavaScript benchmark precision floor is held at 1.0, so any future fixture that triggers this path would immediately break CI.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an intentional design decision consistent with the existing WASM path. Added a comment to
buildFileCallEdgesexplicitly documenting that the same-file fallback is intentionally broad and mirrors thebuildDefinePropertyPostPassbehaviour. The JS benchmark precision floor at 1.0 will catch any future fixture regression caused by name collisions.