|
| 1 | +import { debug } from '../logger.js'; |
1 | 2 | import { findChild, nodeEndLine } from './helpers.js'; |
2 | 3 |
|
3 | 4 | /** |
@@ -173,6 +174,9 @@ function extractSymbolsQuery(tree, query) { |
173 | 174 | // Extract top-level constants via targeted walk (query patterns don't cover these) |
174 | 175 | extractConstantsWalk(tree.rootNode, definitions); |
175 | 176 |
|
| 177 | + // Extract dynamic import() calls via targeted walk (query patterns don't match `import` function type) |
| 178 | + extractDynamicImportsWalk(tree.rootNode, imports); |
| 179 | + |
176 | 180 | return { definitions, calls, imports, classes, exports: exps }; |
177 | 181 | } |
178 | 182 |
|
@@ -224,6 +228,41 @@ function extractConstantsWalk(rootNode, definitions) { |
224 | 228 | } |
225 | 229 | } |
226 | 230 |
|
| 231 | +/** |
| 232 | + * Recursive walk to find dynamic import() calls. |
| 233 | + * Query patterns match call_expression with identifier/member_expression/subscript_expression |
| 234 | + * functions, but import() has function type `import` which none of those patterns cover. |
| 235 | + */ |
| 236 | +function extractDynamicImportsWalk(node, imports) { |
| 237 | + if (node.type === 'call_expression') { |
| 238 | + const fn = node.childForFieldName('function'); |
| 239 | + if (fn && fn.type === 'import') { |
| 240 | + const args = node.childForFieldName('arguments') || findChild(node, 'arguments'); |
| 241 | + if (args) { |
| 242 | + const strArg = findChild(args, 'string'); |
| 243 | + if (strArg) { |
| 244 | + const modPath = strArg.text.replace(/['"]/g, ''); |
| 245 | + const names = extractDynamicImportNames(node); |
| 246 | + imports.push({ |
| 247 | + source: modPath, |
| 248 | + names, |
| 249 | + line: node.startPosition.row + 1, |
| 250 | + dynamicImport: true, |
| 251 | + }); |
| 252 | + } else { |
| 253 | + debug( |
| 254 | + `Skipping non-static dynamic import() at line ${node.startPosition.row + 1} (template literal or variable)`, |
| 255 | + ); |
| 256 | + } |
| 257 | + } |
| 258 | + return; // no need to recurse into import() children |
| 259 | + } |
| 260 | + } |
| 261 | + for (let i = 0; i < node.childCount; i++) { |
| 262 | + extractDynamicImportsWalk(node.child(i), imports); |
| 263 | + } |
| 264 | +} |
| 265 | + |
227 | 266 | function handleCommonJSAssignment(left, right, node, imports) { |
228 | 267 | if (!left || !right) return; |
229 | 268 | const leftText = left.text; |
@@ -455,11 +494,36 @@ function extractSymbolsWalk(tree) { |
455 | 494 | case 'call_expression': { |
456 | 495 | const fn = node.childForFieldName('function'); |
457 | 496 | if (fn) { |
458 | | - const callInfo = extractCallInfo(fn, node); |
459 | | - if (callInfo) calls.push(callInfo); |
460 | | - if (fn.type === 'member_expression') { |
461 | | - const cbDef = extractCallbackDefinition(node, fn); |
462 | | - if (cbDef) definitions.push(cbDef); |
| 497 | + // Dynamic import(): import('./foo.js') → extract as an import entry |
| 498 | + if (fn.type === 'import') { |
| 499 | + const args = node.childForFieldName('arguments') || findChild(node, 'arguments'); |
| 500 | + if (args) { |
| 501 | + const strArg = findChild(args, 'string'); |
| 502 | + if (strArg) { |
| 503 | + const modPath = strArg.text.replace(/['"]/g, ''); |
| 504 | + // Extract destructured names from parent context: |
| 505 | + // const { a, b } = await import('./foo.js') |
| 506 | + // (standalone import('./foo.js').then(...) calls produce an edge with empty names) |
| 507 | + const names = extractDynamicImportNames(node); |
| 508 | + imports.push({ |
| 509 | + source: modPath, |
| 510 | + names, |
| 511 | + line: node.startPosition.row + 1, |
| 512 | + dynamicImport: true, |
| 513 | + }); |
| 514 | + } else { |
| 515 | + debug( |
| 516 | + `Skipping non-static dynamic import() at line ${node.startPosition.row + 1} (template literal or variable)`, |
| 517 | + ); |
| 518 | + } |
| 519 | + } |
| 520 | + } else { |
| 521 | + const callInfo = extractCallInfo(fn, node); |
| 522 | + if (callInfo) calls.push(callInfo); |
| 523 | + if (fn.type === 'member_expression') { |
| 524 | + const cbDef = extractCallbackDefinition(node, fn); |
| 525 | + if (cbDef) definitions.push(cbDef); |
| 526 | + } |
463 | 527 | } |
464 | 528 | } |
465 | 529 | break; |
@@ -941,3 +1005,64 @@ function extractImportNames(node) { |
941 | 1005 | scan(node); |
942 | 1006 | return names; |
943 | 1007 | } |
| 1008 | + |
| 1009 | +/** |
| 1010 | + * Extract destructured names from a dynamic import() call expression. |
| 1011 | + * |
| 1012 | + * Handles: |
| 1013 | + * const { a, b } = await import('./foo.js') → ['a', 'b'] |
| 1014 | + * const mod = await import('./foo.js') → ['mod'] |
| 1015 | + * import('./foo.js') → [] (no names extractable) |
| 1016 | + * |
| 1017 | + * Walks up the AST from the call_expression to find the enclosing |
| 1018 | + * variable_declarator and reads the name/object_pattern. |
| 1019 | + */ |
| 1020 | +function extractDynamicImportNames(callNode) { |
| 1021 | + // Walk up: call_expression → await_expression → variable_declarator |
| 1022 | + let current = callNode.parent; |
| 1023 | + // Skip await_expression wrapper if present |
| 1024 | + if (current && current.type === 'await_expression') current = current.parent; |
| 1025 | + // We should now be at a variable_declarator (or not, if standalone import()) |
| 1026 | + if (!current || current.type !== 'variable_declarator') return []; |
| 1027 | + |
| 1028 | + const nameNode = current.childForFieldName('name'); |
| 1029 | + if (!nameNode) return []; |
| 1030 | + |
| 1031 | + // const { a, b } = await import(...) → object_pattern |
| 1032 | + if (nameNode.type === 'object_pattern') { |
| 1033 | + const names = []; |
| 1034 | + for (let i = 0; i < nameNode.childCount; i++) { |
| 1035 | + const child = nameNode.child(i); |
| 1036 | + if (child.type === 'shorthand_property_identifier_pattern') { |
| 1037 | + names.push(child.text); |
| 1038 | + } else if (child.type === 'pair_pattern') { |
| 1039 | + // { a: localName } → use localName (the alias) for the local binding, |
| 1040 | + // but use the key (original name) for import resolution |
| 1041 | + const key = child.childForFieldName('key'); |
| 1042 | + if (key) names.push(key.text); |
| 1043 | + } |
| 1044 | + } |
| 1045 | + return names; |
| 1046 | + } |
| 1047 | + |
| 1048 | + // const mod = await import(...) → identifier (namespace-like import) |
| 1049 | + if (nameNode.type === 'identifier') { |
| 1050 | + return [nameNode.text]; |
| 1051 | + } |
| 1052 | + |
| 1053 | + // const [a, b] = await import(...) → array_pattern (rare but possible) |
| 1054 | + if (nameNode.type === 'array_pattern') { |
| 1055 | + const names = []; |
| 1056 | + for (let i = 0; i < nameNode.childCount; i++) { |
| 1057 | + const child = nameNode.child(i); |
| 1058 | + if (child.type === 'identifier') names.push(child.text); |
| 1059 | + else if (child.type === 'rest_pattern') { |
| 1060 | + const inner = child.child(0) || child.childForFieldName('name'); |
| 1061 | + if (inner && inner.type === 'identifier') names.push(inner.text); |
| 1062 | + } |
| 1063 | + } |
| 1064 | + return names; |
| 1065 | + } |
| 1066 | + |
| 1067 | + return []; |
| 1068 | +} |
0 commit comments