Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions __tests__/extraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('<anonymous>');

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', () => {
Expand Down
26 changes: 13 additions & 13 deletions src/extraction/tree-sitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,14 @@ export class TreeSitterExtractor {
}
}
}
if (name === '<anonymous>') return; // Skip anonymous functions
if (name === '<anonymous>') {
const body = this.extractor.resolveBody?.(node, this.extractor.bodyField)
?? getChildByField(node, this.extractor.bodyField);
if (body) {
this.visitFunctionBody(body, '');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Visit variable-assigned callbacks in anonymous module bodies

For anonymous AMD/CommonJS wrappers that define handlers as const handler = () => {} or var handler = function () {}, this branch still drops the handler node: visitFunctionBody only calls extractFunction for nested functions whose extractName is already non-anonymous, and extractName intentionally returns <anonymous> for arrows/function expressions before the parent variable_declarator inference in extractFunction can run. As a result, a common module-scope callback style remains absent from the graph even though function declarations in the new test pass.

Useful? React with 👍 / 👎.

}
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
Expand Down Expand Up @@ -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 !== '<anonymous>') {
this.extractFunction(node);
return;
}
this.extractFunction(node);
return;
}

// Extract structural nodes found inside function bodies.
Expand Down