fix(extraction): index CommonJS export assignments as functions (#1675) (#1771)

`exports.getItems = async (req, res) => {…}` and `module.exports.x =
function () {…}` — the Express controller style — produced no symbol: the
arrow's parent is an assignment, not a declarator, so it stayed anonymous,
its calls attributed to the file, and `node`/`callers` answered "Symbol
not found" for a route-wired handler. Resolve the name from the export
property, mark it exported, in both the wasm walker and the kernel.

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
This commit is contained in:
Colby Mchenry
2026-09-08 09:37:20 -05:00
committed by GitHub
co-authored by danusha2345 Colby McHenry
parent 3adf06772b
commit 43271f3cd3
5 changed files with 153 additions and 3 deletions
+42 -1
View File
@@ -1590,6 +1590,8 @@ export class TreeSitterExtractor {
// — SvelteKit actions). Inline-object arrows reached by the general walker
// get no override, so they still fall through to the <anonymous> skip below.
let name = nameOverride ?? extractName(node, this.source, this.extractor);
// A CommonJS export assignment names the function it holds — see below.
let commonJsExport = false;
// For arrow functions and function expressions assigned to variables,
// resolve the name from the parent variable_declarator.
// e.g. `export const useAuth = () => { ... }` — the arrow_function node
@@ -1605,6 +1607,18 @@ export class TreeSitterExtractor {
if (varName) {
name = getNodeText(varName, this.source);
}
} else if (parent?.type === 'assignment_expression') {
// `exports.getItems = async (req, res) => {…}` / `module.exports.x =
// function () {…}` — the CommonJS controller style. The function is
// anonymous only syntactically: the export property is the name every
// `router.get('/items', getItems)` resolves. Without a node the handler
// is invisible to callers/impact and its calls attribute to the file
// (#1675). Same treatment `const X = () => {}` already gets.
const exportName = this.commonJsExportName(parent, node);
if (exportName) {
name = exportName;
commonJsExport = true;
}
}
}
if (name === '<anonymous>') {
@@ -1635,7 +1649,7 @@ export class TreeSitterExtractor {
const docstring = getPrecedingDocstring(node, this.source);
const signature = this.extractor.getSignature?.(node, this.source);
const visibility = this.extractor.getVisibility?.(node);
const isExported = this.extractor.isExported?.(node, this.source);
const isExported = commonJsExport || this.extractor.isExported?.(node, this.source);
const isAsync = this.extractor.isAsync?.(node);
const isStatic = this.extractor.isStatic?.(node);
const returnType = this.extractor.getReturnType?.(node, this.source);
@@ -5311,6 +5325,33 @@ export class TreeSitterExtractor {
targets.add(target);
}
/**
* The property a CommonJS export assignment binds a function to
* `exports.NAME = <node>` or `module.exports.NAME = <node>` or null for
* any other assignment. JS-family only; the node must be the assignment's
* whole right-hand side.
*/
private commonJsExportName(assignment: SyntaxNode, value: SyntaxNode): string | null {
if (
this.language !== 'typescript' &&
this.language !== 'javascript' &&
this.language !== 'tsx' &&
this.language !== 'jsx'
) {
return null;
}
const right = getChildByField(assignment, 'right');
if (!right || right.startIndex !== value.startIndex || right.endIndex !== value.endIndex) return null;
const left = getChildByField(assignment, 'left');
if (!left || left.type !== 'member_expression') return null;
const object = getChildByField(left, 'object');
const property = getChildByField(left, 'property');
if (!object || !property || property.type !== 'property_identifier') return null;
const objectText = getNodeText(object, this.source);
if (objectText !== 'exports' && objectText !== 'module.exports') return null;
return getNodeText(property, this.source);
}
/**
* The declarator name a React handler hook binds an anonymous function to
* `const NAME = useCallback(<node>, [...])` or null for any other shape.