Skip to content
Merged
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
13 changes: 13 additions & 0 deletions crates/codegraph-core/src/extractors/javascript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5684,6 +5684,19 @@ mod tests {
assert_eq!(dyn_imports[0].names, vec!["a".to_string(), "rest".to_string()]);
}

/// Regression test for #2037: the native `require()` destructuring path
/// reuses `collect_object_pattern_names` (already fixed for #1920), so a
/// rest binding must come through correctly here — this locks in parity
/// with the WASM/TS `extractCjsRequireBinding` fix for the same issue.
#[test]
fn finds_cjs_require_with_object_rest_destructuring() {
let s = parse_js("const { a, ...rest } = require('./mod');");
let cjs_imports: Vec<_> = s.imports.iter().filter(|i| i.cjs_require == Some(true)).collect();
assert_eq!(cjs_imports.len(), 1);
assert_eq!(cjs_imports[0].source, "./mod");
assert_eq!(cjs_imports[0].names, vec!["a".to_string(), "rest".to_string()]);
}

#[test]
fn finds_dynamic_import_with_shorthand_default_destructuring() {
let s = parse_js("const { a = 1 } = await import('./mod.js');");
Expand Down
7 changes: 7 additions & 0 deletions src/extractors/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1093,6 +1093,13 @@ function extractCjsRequireBinding(
if (val?.type === 'identifier' || val?.type === 'shorthand_property_identifier_pattern') {
names.push(val.text);
}
} else if (prop.type === 'rest_pattern' || prop.type === 'rest_element') {
// { a, ...rest } = require(...) — without this branch the rest binding
// was silently dropped from the CJS-require import-artifact classification
// (issue #2037), a parity gap with Rust's collect_object_pattern_names
// (which the native require() path already reuses correctly).
const inner = extractRestPatternIdentifier(prop);
if (inner) names.push(inner);
}
}
if (names.length === 0) return null;
Expand Down
19 changes: 19 additions & 0 deletions tests/parsers/javascript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,25 @@ describe('JavaScript parser', () => {
});
});

describe('CJS require() destructuring rest binding (#2037)', () => {
// `extractCjsRequireBinding`'s object-pattern loop only recognized
// shorthand_property_identifier_pattern and pair_pattern children, so a
// rest element (`...rest`) was silently dropped from the CJS-require
// import-artifact classification (#1661) — a parity gap with Rust's
// collect_object_pattern_names, which the native require() path already
// reuses correctly (#2037).

it('includes the rest binding in cjsRequireBindings alongside plain names', () => {
const symbols: any = parseJS(`const { a, ...rest } = require('./mod');`);
expect(symbols.cjsRequireBindings).toEqual([{ names: ['a', 'rest'], source: './mod' }]);
});

it('includes a rest binding mixed with a renamed pair', () => {
const symbols: any = parseJS(`const { a: b, ...rest } = require('./mod');`);
expect(symbols.cjsRequireBindings).toEqual([{ names: ['b', 'rest'], source: './mod' }]);
});
});

it('extracts call expressions', () => {
const symbols = parseJS(`import { foo } from './bar'; foo(); baz();`);
expect(symbols.calls).toContainEqual(expect.objectContaining({ name: 'foo' }));
Expand Down
Loading