mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-14 16:52:09 +08:00
fix(js,ts): a function exported as exports["name"] is a symbol named after its export (#3032)
`exports.getItems = async (req, res) => {…}` names its function after the
export property, in both engines (since the CommonJS controller fix), but
the same export written with the name in quotes, `exports["getItems"] =
…` or `module.exports['deleteItem'] = function () {…}`, left the function
anonymous: no node, so nothing could find it or link to it, and its calls
counted as the file's. commonJsExportName (tree-sitter.ts) and its kernel
twin common_js_export_name (tsjs/extractors.rs) accepted only a
member_expression on the left of the assignment.
Both now also accept a subscript_expression on `exports` or
`module.exports` whose index is a string literal spelling a plain
identifier. The test for that string, which `this['svc']` receivers
already used in both engines, is now one helper per engine
(quotedIdentifier / quoted_identifier) shared by the two readers. A key
that no identifier can spell (`exports["not-a-name"]`), a computed key
and a template string still name nothing.
Validation: the new commonjs-exports cases fail on main and pass in both
engines, and a parity case compares the kernel's and wasm's nodes and
refs for the JS and TS forms.
Reimplements the bug part of PR #2896 by @johnhldiver-beep.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a38a1ec0c0
commit
8e854d11e5
@@ -138,27 +138,57 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// `exports.NAME = <node>` / `module.exports.NAME = <node>`, or the same
|
||||
/// with the name in quotes, `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" {
|
||||
if !matches!(left.kind(), "member_expression" | "subscript_expression") {
|
||||
return None;
|
||||
}
|
||||
let object = left.child_by_field_name("object")?;
|
||||
if !matches!(self.text(object), "exports" | "module.exports") {
|
||||
return None;
|
||||
}
|
||||
if left.kind() == "subscript_expression" {
|
||||
return self.quoted_identifier(left.child_by_field_name("index")?).map(str::to_string);
|
||||
}
|
||||
let property = left.child_by_field_name("property")?;
|
||||
if property.kind() != "property_identifier" {
|
||||
return None;
|
||||
}
|
||||
if !matches!(self.text(object), "exports" | "module.exports") {
|
||||
Some(self.text(property).to_string())
|
||||
}
|
||||
|
||||
/// The name a string literal spells when it is a plain identifier: `'svc'`
|
||||
/// or `"getItems"` → the text between the quotes. None for anything else,
|
||||
/// including a string with an escape or a character no identifier can
|
||||
/// hold. Mirrors quotedIdentifier in tree-sitter.ts.
|
||||
fn quoted_identifier(&self, node: Node<'t>) -> Option<&'t str> {
|
||||
if node.kind() != "string" {
|
||||
return None;
|
||||
}
|
||||
Some(self.text(property).to_string())
|
||||
let text = self.text(node);
|
||||
let quoted = text.len() >= 2
|
||||
&& (text.starts_with('\'') || text.starts_with('"'))
|
||||
&& text.ends_with(&text[..1]);
|
||||
if !quoted {
|
||||
return None;
|
||||
}
|
||||
let key = &text[1..text.len() - 1];
|
||||
let mut chars = key.chars();
|
||||
let first = chars.next()?;
|
||||
if !(first.is_ascii_alphabetic() || first == '_' || first == '$')
|
||||
|| !chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(key)
|
||||
}
|
||||
|
||||
// --- reactComponentHoc / extractReactComponentNode (#841) --------------------
|
||||
@@ -1775,22 +1805,7 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
let object = peel_receiver(receiver.child_by_field_name("object")?);
|
||||
if object.kind() == "this" {
|
||||
let index = receiver.child_by_field_name("index")?;
|
||||
if index.kind() != "string" {
|
||||
return None;
|
||||
}
|
||||
let text = self.text(index);
|
||||
let quoted = text.len() >= 2
|
||||
&& (text.starts_with('\'') || text.starts_with('"'))
|
||||
&& text.ends_with(&text[..1]);
|
||||
let key = if quoted { &text[1..text.len() - 1] } else { return None };
|
||||
let mut chars = key.chars();
|
||||
let first = chars.next()?;
|
||||
if !(first.is_ascii_alphabetic() || first == '_' || first == '$')
|
||||
|| !chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let key = self.quoted_identifier(receiver.child_by_field_name("index")?)?;
|
||||
return Some(format!("this.{key}"));
|
||||
}
|
||||
self.this_field_of(object).map(|field| format!("this.{field}[]"))
|
||||
|
||||
Reference in New Issue
Block a user