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
+34 -2
View File
@@ -20,7 +20,10 @@ impl<'t> Walker<'t> {
.unwrap_or_else(|| self.extract_name(node));
// Arrow/function-expression values: resolve the name from the parent
// variable_declarator (`export const useAuth = () => {}`).
// variable_declarator (`export const useAuth = () => {}`), or from a
// CommonJS export assignment (`exports.getItems = async () => {}`,
// #1675). Mirrors TreeSitterExtractor.extractFunction.
let mut common_js_export = false;
if name_override.is_none()
&& name == "<anonymous>"
&& matches!(node.kind(), "arrow_function" | "function_expression" | "generator_function")
@@ -30,6 +33,11 @@ impl<'t> Walker<'t> {
if let Some(var_name) = parent.child_by_field_name("name") {
name = self.text(var_name).to_string();
}
} else if parent.kind() == "assignment_expression" {
if let Some(export_name) = self.common_js_export_name(parent, node) {
name = export_name;
common_js_export = true;
}
}
}
}
@@ -46,7 +54,7 @@ impl<'t> Walker<'t> {
docstring: crate::docstring::preceding_docstring(node, self.src),
signature: self.signature_of(node),
visibility: self.visibility_of(node),
is_exported: Some(self.is_exported(node)),
is_exported: Some(common_js_export || self.is_exported(node)),
is_async: Some(self.is_async(node)),
is_static: self.is_static(node),
..Extra::default()
@@ -65,6 +73,30 @@ impl<'t> Walker<'t> {
self.stack.pop();
}
/// The property a CommonJS export assignment binds a function to —
/// `exports.NAME = <node>` / `module.exports.NAME = <node>` — or None for
/// any other assignment. The node must be the assignment's whole
/// right-hand side. Mirrors TreeSitterExtractor.commonJsExportName.
fn common_js_export_name(&self, assignment: Node<'t>, value: Node<'t>) -> Option<String> {
let right = assignment.child_by_field_name("right")?;
if right.start_byte() != value.start_byte() || right.end_byte() != value.end_byte() {
return None;
}
let left = assignment.child_by_field_name("left")?;
if left.kind() != "member_expression" {
return None;
}
let object = left.child_by_field_name("object")?;
let property = left.child_by_field_name("property")?;
if property.kind() != "property_identifier" {
return None;
}
if !matches!(self.text(object), "exports" | "module.exports") {
return None;
}
Some(self.text(property).to_string())
}
// --- reactComponentHoc / extractReactComponentNode (#841) --------------------
/// Some(inner) when the initializer is a recognized component wrapper —