From e12bd7ce91d5865a2bd69ea1c2f0d882b08a48a6 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Mon, 6 Apr 2026 20:31:37 -0500 Subject: [PATCH] feat: Include receiver names in method call extraction and improve built-in filtering Addresses method call resolution ambiguity where bare method names couldn't be distinguished from function calls. Modifies tree-sitter extraction to include receiver names (e.g., "console.log" instead of just "log") while skipping common instance references like self/this. Updates built-in filtering to be language-specific and adds Python built-in method detection based on receiver types and method names. --- src/extraction/tree-sitter.ts | 18 +++++++++++++++++- src/resolution/index.ts | 25 +++++++++++++++++++------ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index b37a34d..a4197ab 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -1139,7 +1139,23 @@ export class TreeSitterExtractor { // Go uses selector_expression with 'field', JS/TS uses member_expression with 'property' const property = getChildByField(func, 'property') || getChildByField(func, 'field') || func.namedChild(1); if (property) { - calleeName = getNodeText(property, this.source); + const methodName = getNodeText(property, this.source); + // Include receiver name for qualified resolution (e.g., console.print → "console.print") + // This helps the resolver distinguish method calls from bare function calls + // (e.g., Python's console.print() vs builtin print()) + // Skip self/this/cls as they don't aid resolution + const receiver = getChildByField(func, 'object') || getChildByField(func, 'operand') || func.namedChild(0); + const SKIP_RECEIVERS = new Set(['self', 'this', 'cls', 'super']); + if (receiver && receiver.type === 'identifier') { + const receiverName = getNodeText(receiver, this.source); + if (!SKIP_RECEIVERS.has(receiverName)) { + calleeName = `${receiverName}.${methodName}`; + } else { + calleeName = methodName; + } + } else { + calleeName = methodName; + } } } else if (func.type === 'scoped_identifier' || func.type === 'scoped_call_expression') { // Scoped call: Module::function() diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 025c477..8dd31dc 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -562,35 +562,48 @@ export class ReferenceResolver { */ private isBuiltInOrExternal(ref: UnresolvedRef): boolean { const name = ref.referenceName; + const isJsTs = ref.language === 'typescript' || ref.language === 'javascript' + || ref.language === 'tsx' || ref.language === 'jsx'; // JavaScript/TypeScript built-ins - if (JS_BUILT_INS.has(name)) { + if (isJsTs && JS_BUILT_INS.has(name)) { return true; } - // Common library calls - if (name.startsWith('console.') || name.startsWith('Math.') || name.startsWith('JSON.')) { + // Common JS/TS library calls (console.log, Math.floor, JSON.parse) + if (isJsTs && (name.startsWith('console.') || name.startsWith('Math.') || name.startsWith('JSON.'))) { return true; } // React hooks from React itself - if (REACT_HOOKS.has(name)) { + if (isJsTs && REACT_HOOKS.has(name)) { return true; } - // Python built-ins + // Python built-ins (bare calls only — dotted calls like console.print are method calls) if (ref.language === 'python' && PYTHON_BUILT_INS.has(name)) { return true; } - // Python built-in method calls (e.g., list.extend, dict.update, self.xxx) + // Python built-in method calls (e.g., list.extend, dict.update) if (ref.language === 'python') { const dotIdx = name.indexOf('.'); if (dotIdx > 0) { const receiver = name.substring(0, dotIdx); + const method = name.substring(dotIdx + 1); + // Filter calls on built-in types (list.append, dict.update, etc.) if (PYTHON_BUILT_IN_TYPES.has(receiver)) { return true; } + // Filter built-in methods on non-class receivers + // (e.g., items.append where items is a local list variable) + // But allow if the capitalized receiver matches a known codebase class + if (PYTHON_BUILT_IN_METHODS.has(method)) { + const capitalized = receiver.charAt(0).toUpperCase() + receiver.slice(1); + if (!this.knownNames?.has(capitalized)) { + return true; + } + } } if (PYTHON_BUILT_IN_METHODS.has(name)) { return true;