mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-15 07:15:37 +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
@@ -704,6 +704,17 @@ function tsJsCalleeName(func: SyntaxNode, source: string): { name: string; at: S
|
||||
return /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(chain) ? { name: chain, at: cur } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name a JS/TS string literal spells when it is a plain identifier: `'svc'`
|
||||
* or `"getItems"` → the text between the quotes. Null for anything else,
|
||||
* including a string with an escape or a character no identifier can hold.
|
||||
* Mirrored in the kernel's quoted_identifier.
|
||||
*/
|
||||
function quotedIdentifier(node: SyntaxNode | null, source: string): string | null {
|
||||
if (node?.type !== 'string') return null;
|
||||
return /^(['"])([A-Za-z_$][\w$]*)\1$/.exec(getNodeText(node, source))?.[2] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A TS/JS receiver that subscripts the enclosing class's object: an element
|
||||
* of a field, `this.<field>[k]` → `this.<field>[]`, or a field named by a
|
||||
@@ -716,8 +727,7 @@ function tsThisSubscriptReceiver(node: SyntaxNode, source: string): string | nul
|
||||
if (!objectNode) return null;
|
||||
const object = peelTsJsReceiver(objectNode);
|
||||
if (object.type === 'this') {
|
||||
const index = getChildByField(node, 'index');
|
||||
const key = index?.type === 'string' ? /^(['"])([A-Za-z_$][\w$]*)\1$/.exec(getNodeText(index, source))?.[2] : undefined;
|
||||
const key = quotedIdentifier(getChildByField(node, 'index'), source);
|
||||
return key ? `this.${key}` : null;
|
||||
}
|
||||
if (object.type !== 'member_expression' || getChildByField(object, 'object')?.type !== 'this') return null;
|
||||
@@ -7305,9 +7315,10 @@ export class TreeSitterExtractor {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* `exports.NAME = <node>` or `module.exports.NAME = <node>`, or the same
|
||||
* with the name in quotes, `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 (
|
||||
@@ -7321,12 +7332,14 @@ export class TreeSitterExtractor {
|
||||
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;
|
||||
if (!left || (left.type !== 'member_expression' && left.type !== 'subscript_expression')) return null;
|
||||
const object = getChildByField(left, 'object');
|
||||
const property = getChildByField(left, 'property');
|
||||
if (!object || !property || property.type !== 'property_identifier') return null;
|
||||
if (!object) return null;
|
||||
const objectText = getNodeText(object, this.source);
|
||||
if (objectText !== 'exports' && objectText !== 'module.exports') return null;
|
||||
if (left.type === 'subscript_expression') return quotedIdentifier(getChildByField(left, 'index'), this.source);
|
||||
const property = getChildByField(left, 'property');
|
||||
if (!property || property.type !== 'property_identifier') return null;
|
||||
return getNodeText(property, this.source);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user