Skip to content

Commit ad1396e

Browse files
committed
fix: handle satisfies-cast dynamic imports, add WASM timeout (#1921)
docs check acknowledged — per Greptile review, no new functionality.
1 parent 82caa41 commit ad1396e

4 files changed

Lines changed: 39 additions & 9 deletions

File tree

crates/codegraph-core/src/extractors/javascript.rs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3038,13 +3038,19 @@ fn find_parent_class_no_fn_boundary(node: &Node, source: &[u8]) -> Option<String
30383038

30393039
/// Wrapper node kinds that can sit between a dynamic `import()` call and its
30403040
/// enclosing `variable_declarator` without changing which value gets bound —
3041-
/// `await`, redundant parentheses, and TypeScript `as` casts. Real-world call
3042-
/// sites often combine several of these, e.g.
3041+
/// `await`, redundant parentheses, and TypeScript `as`/`satisfies` casts.
3042+
/// Real-world call sites often combine several of these, e.g.
30433043
/// `const { X } = (await import('./mod.js')) as { X: Fn }` nests
30443044
/// await_expression → parenthesized_expression → as_expression before
3045-
/// reaching the declarator (#1781).
3046-
const DYNAMIC_IMPORT_WRAPPER_KINDS: &[&str] =
3047-
&["await_expression", "parenthesized_expression", "as_expression"];
3045+
/// reaching the declarator (#1781). `satisfies_expression` (TS 4.9+
3046+
/// `... satisfies { X: Fn }`) is structurally identical to `as_expression`
3047+
/// here — Greptile follow-up, mirrors the TS extractor.
3048+
const DYNAMIC_IMPORT_WRAPPER_KINDS: &[&str] = &[
3049+
"await_expression",
3050+
"parenthesized_expression",
3051+
"as_expression",
3052+
"satisfies_expression",
3053+
];
30483054

30493055
/// Extract named bindings from a dynamic `import()` call expression.
30503056
/// Handles: `const { a, b } = await import(...)`, `const mod = await import(...)`,
@@ -4247,6 +4253,17 @@ mod tests {
42474253
assert!(dyn_imports[0].names.contains(&"b".to_string()));
42484254
}
42494255

4256+
#[test]
4257+
fn finds_dynamic_import_with_satisfies_cast_destructuring() {
4258+
// TS 4.9+ `satisfies` is structurally identical to `as` here (Greptile
4259+
// follow-up to #1781) — same walk-up gap would otherwise reproduce.
4260+
let s = parse_ts("const { a, b } = await import('./foo.js') satisfies { a: Fn; b: Fn };");
4261+
let dyn_imports: Vec<_> = s.imports.iter().filter(|i| i.dynamic_import == Some(true)).collect();
4262+
assert_eq!(dyn_imports.len(), 1);
4263+
assert!(dyn_imports[0].names.contains(&"a".to_string()));
4264+
assert!(dyn_imports[0].names.contains(&"b".to_string()));
4265+
}
4266+
42504267
#[test]
42514268
fn finds_dynamic_import_with_parenthesized_as_cast_destructuring() {
42524269
// Exact repro shape from #1781 (native-orchestrator.ts):

src/extractors/javascript.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4054,16 +4054,19 @@ function extractImportNames(
40544054
/**
40554055
* Wrapper node types that can sit between a dynamic `import()` call and its
40564056
* enclosing `variable_declarator` without changing which value gets bound —
4057-
* `await`, redundant parentheses, and TypeScript `as` casts. Real-world
4058-
* dynamic-import call sites often combine several of these, e.g.
4057+
* `await`, redundant parentheses, and TypeScript `as`/`satisfies` casts.
4058+
* Real-world dynamic-import call sites often combine several of these, e.g.
40594059
* `const { X } = (await import('./mod.js')) as { X: Fn }` nests
40604060
* await_expression → parenthesized_expression → as_expression before
4061-
* reaching the declarator (#1781).
4061+
* reaching the declarator (#1781). `satisfies_expression` (TS 4.9+
4062+
* `... satisfies { X: Fn }`) is structurally identical to `as_expression`
4063+
* here — same Greptile follow-up as the native mirror.
40624064
*/
40634065
const DYNAMIC_IMPORT_WRAPPER_TYPES = new Set([
40644066
'await_expression',
40654067
'parenthesized_expression',
40664068
'as_expression',
4069+
'satisfies_expression',
40674070
]);
40684071

40694072
/**

tests/integration/issue-1781-dynamic-import-destructure-consumer.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ describe('dynamic import() + destructure consumer crediting (#1781) — WASM', (
126126
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-1781-wasm-'));
127127
writeFixture(tmpDir);
128128
await buildGraph(tmpDir, { engine: 'wasm', incremental: false, skipRegistry: true });
129-
});
129+
}, 60_000);
130130

131131
afterAll(() => {
132132
fs.rmSync(tmpDir, { recursive: true, force: true });

tests/parsers/javascript.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,16 @@ describe('JavaScript parser', () => {
211211
expect(symbols.imports[0].names).toEqual(['a', 'b']);
212212
});
213213

214+
it('extracts destructured names through a TypeScript `satisfies {...}` assertion', () => {
215+
// TS 4.9+ `satisfies` is structurally identical to `as` here (Greptile
216+
// follow-up to #1781) — same walk-up gap would otherwise reproduce.
217+
const symbols = parseTS(
218+
`const { a, b } = await import('./foo.js') satisfies { a: Fn; b: Fn };`,
219+
);
220+
expect(symbols.imports).toHaveLength(1);
221+
expect(symbols.imports[0].names).toEqual(['a', 'b']);
222+
});
223+
214224
it('extracts destructured names through parens + `as`-cast combined (exact repro shape)', () => {
215225
// Matches native-orchestrator.ts's actual production pattern:
216226
// const { X, Y } = (await import('./mod.js')) as { X: Fn; Y: Fn };

0 commit comments

Comments
 (0)