-
Notifications
You must be signed in to change notification settings - Fork 15
feat(resolver): resolve prototype-based method calls, spread/iteration callbacks, func-prop this-dispatch, and object-rest param dispatch (JS) #1331
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 1 commit
11e70c8
a458aad
1f1b1c3
e4fa7c2
832f7fc
253bd71
4fe4f71
0ffb24e
1b84435
d926cf3
a6c5d2d
c484fd1
cdc84cf
4ed709e
66b899a
667866e
058f0f4
194507c
c036834
8407fcf
1f925bf
d68cf72
a20406b
0650ce9
07ce481
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 |
|---|---|---|
|
|
@@ -351,6 +351,9 @@ function extractSymbolsQuery(tree: TreeSitterTree, query: TreeSitterQuery): Extr | |
| // Extract definitions from destructured bindings (query patterns don't match object_pattern) | ||
| extractDestructuredBindingsWalk(tree.rootNode, definitions); | ||
|
|
||
| // Pre-ES6 prototype methods: `Foo.prototype.bar = fn` and `Foo.prototype = { bar: fn }` | ||
| extractPrototypeMethodsWalk(tree.rootNode, definitions, typeMap); | ||
|
|
||
| // Phase 8.5: collect all `new X()` constructor names for RTA instantiation tracking | ||
| const newExpressions: string[] = []; | ||
| extractNewExpressionsWalk(tree.rootNode, newExpressions); | ||
|
|
@@ -614,6 +617,8 @@ function extractSymbolsWalk(tree: TreeSitterTree): ExtractorOutput { | |
| ); | ||
| // Phase 8.3c: Extract call-site argument bindings for parameter-flow pts analysis | ||
| extractParamBindingsWalk(tree.rootNode, ctx.paramBindings!); | ||
| // Pre-ES6 prototype methods: `Foo.prototype.bar = fn` and `Foo.prototype = { bar: fn }` | ||
| extractPrototypeMethodsWalk(tree.rootNode, ctx.definitions, ctx.typeMap!); | ||
| // Phase 8.5: collect all `new X()` constructor names for RTA instantiation tracking | ||
| const newExpressions: string[] = []; | ||
| extractNewExpressionsWalk(tree.rootNode, newExpressions); | ||
|
|
@@ -1397,6 +1402,124 @@ function extractNewExpressionsWalk(rootNode: TreeSitterNode, newExpressions: str | |
| walk(rootNode, 0); | ||
| } | ||
|
|
||
| /** AST node types that represent a function body (used by prototype extraction). */ | ||
| const PROTO_FN_TYPES = new Set([ | ||
| 'function_expression', | ||
| 'arrow_function', | ||
| 'generator_function', | ||
| ]); | ||
|
|
||
| /** | ||
| * Walk the AST collecting pre-ES6 prototype assignments: | ||
| * Foo.prototype.bar = function() {} → definition Foo.bar (kind: method) | ||
| * Foo.prototype.bar = f → typeMap['Foo.bar'] = { type: 'f', confidence: 0.9 } | ||
| * Foo.prototype = { bar: fn, baz: f } → same rules per property | ||
| */ | ||
| function extractPrototypeMethodsWalk( | ||
| rootNode: TreeSitterNode, | ||
| definitions: Definition[], | ||
| typeMap: Map<string, TypeMapEntry>, | ||
| ): void { | ||
| function walk(node: TreeSitterNode, depth: number): void { | ||
| if (depth >= MAX_WALK_DEPTH) return; | ||
| if (node.type === 'expression_statement') { | ||
| const expr = node.child(0); | ||
| if (expr?.type === 'assignment_expression') handlePrototypeAssignment(expr, definitions, typeMap); | ||
| // Don't recurse inside expression_statement — no nested prototype assignments expected | ||
| return; | ||
|
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.
When an
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 — the IIFE concern was already addressed in the version merged from main. The |
||
| } | ||
| for (let i = 0; i < node.childCount; i++) { | ||
| walk(node.child(i)!, depth + 1); | ||
| } | ||
| } | ||
| walk(rootNode, 0); | ||
| } | ||
|
|
||
| function handlePrototypeAssignment( | ||
| assignNode: TreeSitterNode, | ||
| definitions: Definition[], | ||
| typeMap: Map<string, TypeMapEntry>, | ||
| ): void { | ||
| const lhs = assignNode.childForFieldName('left'); | ||
| const rhs = assignNode.childForFieldName('right'); | ||
| if (!lhs || !rhs || lhs.type !== 'member_expression') return; | ||
|
|
||
| const lhsProp = lhs.childForFieldName('property'); | ||
| const lhsObj = lhs.childForFieldName('object'); | ||
| if (!lhsProp || !lhsObj) return; | ||
|
|
||
| // Pattern 1: Foo.prototype.bar = rhs | ||
| if (lhsObj.type === 'member_expression') { | ||
| const innerProp = lhsObj.childForFieldName('property'); | ||
| const innerObj = lhsObj.childForFieldName('object'); | ||
| if ( | ||
| innerProp?.text === 'prototype' && | ||
| innerObj?.type === 'identifier' && | ||
| !BUILTIN_GLOBALS.has(innerObj.text) | ||
| ) { | ||
| emitPrototypeMethod(innerObj.text, lhsProp.text, rhs, definitions, typeMap); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| // Pattern 2: Foo.prototype = { ... } | ||
| if ( | ||
| lhsProp.text === 'prototype' && | ||
| lhsObj.type === 'identifier' && | ||
| !BUILTIN_GLOBALS.has(lhsObj.text) && | ||
| (rhs.type === 'object' || rhs.type === 'object_expression') | ||
| ) { | ||
| extractPrototypeObjectLiteral(lhsObj.text, rhs, definitions, typeMap); | ||
| } | ||
| } | ||
|
|
||
| function emitPrototypeMethod( | ||
| className: string, | ||
| methodName: string, | ||
| rhs: TreeSitterNode, | ||
| definitions: Definition[], | ||
| typeMap: Map<string, TypeMapEntry>, | ||
| ): void { | ||
| const qualifiedName = `${className}.${methodName}`; | ||
| if (PROTO_FN_TYPES.has(rhs.type)) { | ||
| definitions.push({ | ||
| name: qualifiedName, | ||
| kind: 'method', | ||
| line: nodeStartLine(rhs), | ||
| endLine: nodeEndLine(rhs), | ||
| }); | ||
| } else if (rhs.type === 'identifier' && !BUILTIN_GLOBALS.has(rhs.text)) { | ||
| setTypeMapEntry(typeMap, qualifiedName, rhs.text, 0.9); | ||
| } | ||
| } | ||
|
|
||
| function extractPrototypeObjectLiteral( | ||
| className: string, | ||
| objNode: TreeSitterNode, | ||
| definitions: Definition[], | ||
| typeMap: Map<string, TypeMapEntry>, | ||
| ): void { | ||
| for (let i = 0; i < objNode.childCount; i++) { | ||
| const child = objNode.child(i); | ||
| if (!child) continue; | ||
| if (child.type === 'pair') { | ||
| const key = child.childForFieldName('key'); | ||
| const value = child.childForFieldName('value'); | ||
| if (key && value) emitPrototypeMethod(className, key.text, value, definitions, typeMap); | ||
|
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.
For object-literal prototype assignments where a key is a quoted string — e.g.
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 — the |
||
| } else if (child.type === 'method_definition') { | ||
| const nameNode = child.childForFieldName('name'); | ||
| if (nameNode) { | ||
| definitions.push({ | ||
| name: `${className}.${nameNode.text}`, | ||
| kind: 'method', | ||
| line: nodeStartLine(child), | ||
| endLine: nodeEndLine(child), | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * 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.
newregex only matches uppercase-initial constructorsThe regex
[A-Z_$][A-Za-z0-9_$]*restricts matching to PascalCase (or_/$-prefixed) names, so a receiver like(new animal()).move()would not be resolved. Pre-ES6 code often uses lowercase constructor functions (e.g.function animal() {}). If this constraint is intentional (to avoid false positives on non-constructor calls), it is worth documenting explicitly; otherwise changing the first character class to[A-Za-z_$]would broaden coverage.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!
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.
Acknowledged and documented. Added a comment to the regex explaining the uppercase-initial constraint is intentional — it acts as a heuristic to distinguish constructors (PascalCase) from regular functions, avoiding false positives on patterns like
(new xmlParser()).parse(). Lowercase constructor functions are uncommon in practice and the trade-off favors precision over recall here.