diff --git a/CHANGELOG.md b/CHANGELOG.md index 09fbad95a..39718c6fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes +- JavaScript and TypeScript callbacks passed anonymously to AMD or CommonJS loaders now keep their inner functions and calls in the graph, without adding noisy anonymous function symbols. (#528) - Indexing a project that contains only config-style files (YAML, Twig, or `.properties`) no longer misleadingly reports "No files found to index" — these files are tracked at the file level and are now counted as indexed. Thanks @luojiyin1987 (#357). ## [0.9.7] - 2026-05-28 diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index b497af6a9..7a8033db1 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -350,6 +350,51 @@ export const fetchData = async () => { isExported: true, }); }); + + it('should traverse anonymous AMD and CommonJS callback bodies without anonymous nodes', () => { + const code = ` +define(['dep'], function(dep) { + function amdHandler() { + return dep.load(); + } + + const amdCallback = () => dep.ready(); + register(amdHandler); +}); + +require(['legacy'], function(legacy) { + function commonHandler() { + return normalize(legacy.read()); + } + + var commonCallback = function() { + return legacy.ready(); + }; + boot(commonHandler); +}); +`; + const result = extractFromSource('legacy-module.js', code); + + const functionNames = result.nodes + .filter((n) => n.kind === 'function') + .map((n) => n.name); + expect(functionNames).toContain('amdHandler'); + expect(functionNames).toContain('amdCallback'); + expect(functionNames).toContain('commonHandler'); + expect(functionNames).toContain('commonCallback'); + expect(functionNames).not.toContain(''); + + const callNames = result.unresolvedReferences + .filter((r) => r.referenceKind === 'calls') + .map((r) => r.referenceName); + expect(callNames).toContain('register'); + expect(callNames).toContain('boot'); + expect(callNames).toContain('dep.load'); + expect(callNames).toContain('dep.ready'); + expect(callNames).toContain('normalize'); + expect(callNames).toContain('legacy.read'); + expect(callNames).toContain('legacy.ready'); + }); }); describe('Type Alias Extraction', () => { diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index f576839fa..d22f4ba41 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -630,7 +630,14 @@ export class TreeSitterExtractor { } } } - if (name === '') return; // Skip anonymous functions + if (name === '') { + const body = this.extractor.resolveBody?.(node, this.extractor.bodyField) + ?? getChildByField(node, this.extractor.bodyField); + if (body) { + this.visitFunctionBody(body, ''); + } + return; // Skip anonymous function nodes, but keep their body visible. + } // Check for misparse artifacts (e.g. C++ macros causing "namespace detail" functions) // Skip the node but still visit the body for calls and structural nodes @@ -2020,19 +2027,12 @@ export class TreeSitterExtractor { } } - // Nested NAMED functions inside a body — function declarations and named - // function expressions like `.on('mount', function onmount(){})` — become - // their own nodes so the graph can link to them (callback handlers, local - // helpers). Anonymous arrows/expressions fall through to the default - // recursion below, keeping their inner calls attributed to the enclosing - // function: this bounds the new nodes to NAMED functions only (no explosion, - // no lost edges). extractFunction walks the nested body itself, so we return. + // Nested functions are handled by extractFunction. It creates nodes for + // named functions and variable-assigned callbacks, while true anonymous + // functions are skipped after their bodies remain visible to the walker. if (this.extractor!.functionTypes.includes(nodeType)) { - const nestedName = extractName(node, this.source, this.extractor!); - if (nestedName && nestedName !== '') { - this.extractFunction(node); - return; - } + this.extractFunction(node); + return; } // Extract structural nodes found inside function bodies.