@@ -27,7 +27,13 @@ fn import_node_types(language: &str) -> &'static [&'static str] {
2727 "python" => & [ "import_statement" , "import_from_statement" ] ,
2828 "rust" => & [ "use_declaration" ] ,
2929 "go" => & [ "import_spec" ] ,
30- "javascript" | "typescript" => & [ "import_statement" ] ,
30+ // `variable_declarator` also catches CommonJS `require()` — see
31+ // `parse_js_require`. It's the same node kind `assignment_nodes()`
32+ // (resolver/lang_constants.rs) already walks for alias tracking; the
33+ // two extractions look for different shapes in the same nodes and
34+ // don't conflict (alias tracking wants a bare-identifier RHS,
35+ // `parse_js_require` wants a `require(...)` call RHS).
36+ "javascript" | "typescript" => & [ "import_statement" , "variable_declarator" ] ,
3137 "java" => & [ "import_declaration" ] ,
3238 _ => & [ ] ,
3339 }
@@ -199,6 +205,10 @@ fn parse_go_import(text: &str) -> Option<ParsedImport> {
199205}
200206
201207fn parse_js_import ( text : & str ) -> Option < ParsedImport > {
208+ parse_js_esm_import ( text) . or_else ( || parse_js_require ( text) )
209+ }
210+
211+ fn parse_js_esm_import ( text : & str ) -> Option < ParsedImport > {
202212 // import { a, b as c } from 'mod'; import x from 'mod'; import * as ns from 'mod';
203213 let ( clause, module) = text. split_once ( " from " ) ?;
204214 let module = module
@@ -227,6 +237,47 @@ fn parse_js_import(text: &str) -> Option<ParsedImport> {
227237 } )
228238}
229239
240+ /// CommonJS `require()`, still common in real Node.js code (older packages,
241+ /// TypeScript compiled to CommonJS) but structurally a call expression, not
242+ /// an `import_statement` — this is fed `variable_declarator` text instead
243+ /// (`NAME = require(...)` or `{ a, b as c } = require(...)`, no trailing
244+ /// `;`, no `const`/`let`/`var` keyword — that's the parent node).
245+ ///
246+ /// Only a literal string argument resolves to a module — `require(path)`
247+ /// with a computed argument can't be statically attributed, so it's left
248+ /// unresolved (`None`) rather than guessed at.
249+ fn parse_js_require ( text : & str ) -> Option < ParsedImport > {
250+ let ( lhs, rhs) = text. split_once ( '=' ) ?;
251+ let after_require = rhs. trim ( ) . strip_prefix ( "require(" ) ?. trim_start ( ) ;
252+ let quote = after_require. chars ( ) . next ( ) ?;
253+ if quote != '"' && quote != '\'' {
254+ return None ;
255+ }
256+ let rest = & after_require[ quote. len_utf8 ( ) ..] ;
257+ let end = rest. find ( quote) ?;
258+ let module = rest[ ..end] . to_string ( ) ;
259+ if module. is_empty ( ) {
260+ return None ;
261+ }
262+
263+ let lhs = lhs. trim ( ) ;
264+ let mut names = Vec :: new ( ) ;
265+ if let Some ( start) = lhs. find ( '{' )
266+ && let Some ( end) = lhs. find ( '}' )
267+ {
268+ for seg in lhs[ start + 1 ..end] . split ( ',' ) {
269+ names. extend ( bound_name ( seg) ) ;
270+ }
271+ } else {
272+ names. extend ( ident ( lhs) ) ;
273+ }
274+
275+ Some ( ParsedImport {
276+ module_name : module,
277+ imported_names : names,
278+ } )
279+ }
280+
230281fn parse_java_import ( text : & str ) -> Option < ParsedImport > {
231282 // import a.b.C; import static a.b.C.m; import a.b.*;
232283 let rest = text
@@ -299,6 +350,43 @@ mod tests {
299350 assert_eq ! ( i. imported_names, vec![ "a" , "c" ] ) ;
300351 }
301352
353+ #[ test]
354+ fn js_require_default ( ) {
355+ let i = one ( "const foo = require('./foo');\n " , "javascript" ) ;
356+ assert_eq ! ( i. module_name, "./foo" ) ;
357+ assert_eq ! ( i. imported_names, vec![ "foo" ] ) ;
358+ }
359+
360+ #[ test]
361+ fn ts_require_double_quoted ( ) {
362+ let i = one ( "const foo = require(\" ./foo\" );\n " , "typescript" ) ;
363+ assert_eq ! ( i. module_name, "./foo" ) ;
364+ assert_eq ! ( i. imported_names, vec![ "foo" ] ) ;
365+ }
366+
367+ #[ test]
368+ fn js_require_destructure ( ) {
369+ let i = one ( "const { a, b: c } = require('./mod');\n " , "javascript" ) ;
370+ assert_eq ! ( i. module_name, "./mod" ) ;
371+ assert_eq ! ( i. imported_names, vec![ "a" , "c" ] ) ;
372+ }
373+
374+ /// A computed argument can't be statically attributed to a module —
375+ /// must not guess (see `parse_js_require`'s literal-only contract).
376+ #[ test]
377+ fn js_require_with_computed_path_yields_no_import ( ) {
378+ let v = extract_imports ( "const x = require(somePath);\n " , "javascript" ) ;
379+ assert ! ( v. is_empty( ) , "expected no import, got {v:?}" ) ;
380+ }
381+
382+ /// A plain (non-`require`) assignment must not be mistaken for an
383+ /// import now that `variable_declarator` is walked for JS/TS.
384+ #[ test]
385+ fn js_plain_assignment_yields_no_import ( ) {
386+ let v = extract_imports ( "const x = 5;\n " , "javascript" ) ;
387+ assert ! ( v. is_empty( ) , "expected no import, got {v:?}" ) ;
388+ }
389+
302390 #[ test]
303391 fn java_import ( ) {
304392 let i = one ( "import a.b.C;\n " , "java" ) ;
0 commit comments