1use tree_sitter::{Language, Parser, Tree};
2
3use crate::core::code_graph::CodeGraph;
4use crate::core::moniker::Moniker;
5use crate::core::shape::Shape;
6
7use crate::lang::KindSpec;
8
9pub mod build;
10mod kinds;
11mod sdk_pipeline;
12
13pub fn parse(source: &str) -> Tree {
14 parse_with_uri(source, "")
15}
16
17pub fn parse_with_uri(source: &str, uri: &str) -> Tree {
18 let mut parser = Parser::new();
19 let language: Language = if uri_uses_jsx(uri) {
20 tree_sitter_typescript::LANGUAGE_TSX.into()
21 } else {
22 tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()
23 };
24 parser.set_language(&language).unwrap_or_else(|err| {
25 panic!("failed to load tree-sitter TypeScript grammar: {err}");
26 });
27 parser.parse(source, None).unwrap_or_else(|| {
28 panic!("tree-sitter parse returned None on a non-cancelled call");
29 })
30}
31
32fn uri_uses_jsx(uri: &str) -> bool {
33 uri.ends_with(".tsx") || uri.ends_with(".jsx")
34}
35
36#[derive(Clone, Debug, Default)]
37pub struct Presets {
38 pub di_register_callees: Vec<String>,
39 pub path_aliases: Vec<PathAlias>,
40}
41
42#[derive(Clone, Debug)]
43pub struct PathAlias {
44 pub pattern: String,
45 pub substitution: String,
46}
47
48pub fn extract(
49 uri: &str,
50 source: &str,
51 anchor: &Moniker,
52 deep: bool,
53 presets: &Presets,
54) -> CodeGraph {
55 sdk_pipeline::extract(uri, source, anchor, deep, presets)
56}
57
58pub struct Lang;
59
60const DEF_KINDS: &[&str] = &[
61 "class",
62 "interface",
63 "type",
64 "function",
65 "method",
66 "const",
67 "enum",
68 "constructor",
69 "field",
70 "enum_constant",
71 "namespace",
72];
73
74const DEF_KIND_SPECS: &[KindSpec] = &[
75 KindSpec::new("namespace", Shape::Namespace, 10, "namespace"),
76 KindSpec::new("class", Shape::Type, 20, "class"),
77 KindSpec::new("interface", Shape::Type, 21, "interface"),
78 KindSpec::new("enum", Shape::Type, 22, "enum"),
79 KindSpec::new("type", Shape::Type, 23, "type"),
80 KindSpec::new("constructor", Shape::Callable, 40, "constructor"),
81 KindSpec::new("method", Shape::Callable, 41, "method"),
82 KindSpec::new("function", Shape::Callable, 42, "function"),
83 KindSpec::new("field", Shape::Value, 60, "field"),
84 KindSpec::new("enum_constant", Shape::Value, 61, "enum_constant"),
85 KindSpec::new("const", Shape::Value, 62, "const"),
86];
87
88impl crate::lang::LangExtractor for Lang {
89 type Presets = Presets;
90 const LANG_TAG: &'static str = "ts";
91 const ALLOWED_KINDS: &'static [&'static str] = DEF_KINDS;
92 const KIND_SPECS: &'static [KindSpec] = DEF_KIND_SPECS;
93 const ALLOWED_VISIBILITIES: &'static [&'static str] =
94 &["public", "private", "protected", "module"];
95
96 fn extract(
97 uri: &str,
98 source: &str,
99 anchor: &Moniker,
100 deep: bool,
101 presets: &Self::Presets,
102 ) -> CodeGraph {
103 extract(uri, source, anchor, deep, presets)
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::core::moniker::MonikerBuilder;
111 use crate::lang::assert_conformance;
112
113 fn extract(uri: &str, source: &str, anchor: &Moniker, deep: bool) -> CodeGraph {
114 let g = super::extract(uri, source, anchor, deep, &Presets::default());
115 assert_conformance::<super::Lang>(&g, anchor);
116 g
117 }
118
119 fn make_anchor() -> Moniker {
120 MonikerBuilder::new()
121 .project(b"my-app")
122 .segment(b"path", b"main")
123 .build()
124 }
125
126 #[test]
127 fn parse_empty_source_returns_program() {
128 let tree = parse("");
129 assert_eq!(tree.root_node().kind(), "program");
130 assert_eq!(tree.root_node().child_count(), 0);
131 }
132
133 #[test]
134 fn parse_simple_class_has_class_declaration() {
135 let tree = parse("class Foo {}");
136 assert_eq!(
137 tree.root_node().child(0).unwrap().kind(),
138 "class_declaration"
139 );
140 }
141
142 #[test]
143 fn parse_invalid_syntax_marks_errors() {
144 assert!(parse("class { ").root_node().has_error());
145 }
146
147 #[test]
148 fn extract_strips_each_known_extension() {
149 let anchor = make_anchor();
150 for uri in [
151 "foo.ts", "foo.tsx", "foo.js", "foo.jsx", "foo.mjs", "foo.cjs",
152 ] {
153 let g = extract(uri, "", &anchor, false);
154 let last = g.root().as_view().segments().last().unwrap();
155 assert_eq!(last.name, b"foo", "extension not stripped on {uri}");
156 }
157 }
158
159 #[test]
160 fn extract_dot_only_specifier_resolves_relative_not_external() {
161 let g = extract(
162 "src/__tests__/foo.test.ts",
163 "import { z } from \"..\";",
164 &make_anchor(),
165 false,
166 );
167 let r = g.refs().next().unwrap();
168 let target = MonikerBuilder::new()
169 .project(b"my-app")
170 .segment(b"path", b"main")
171 .segment(b"lang", b"ts")
172 .segment(b"dir", b"src")
173 .segment(b"path", b"z")
174 .build();
175 assert_eq!(r.target, target);
176 }
177
178 #[test]
179 fn extract_dotdot_import_walks_up_then_down() {
180 let g = extract(
181 "src/lib/foo.ts",
182 "import { X } from '../other';",
183 &make_anchor(),
184 false,
185 );
186 let r = g.refs().next().unwrap();
187 let target = MonikerBuilder::new()
188 .project(b"my-app")
189 .segment(b"path", b"main")
190 .segment(b"lang", b"ts")
191 .segment(b"dir", b"src")
192 .segment(b"module", b"other")
193 .segment(b"path", b"X")
194 .build();
195 assert_eq!(r.target, target);
196 }
197
198 #[test]
199 fn extract_call_to_nested_function_is_resolved() {
200 let src = r#"
201function outer() {
202 function inner() {}
203 inner();
204}
205"#;
206 let g = extract("util.ts", src, &make_anchor(), false);
207 let r = g
208 .refs()
209 .find(|r| {
210 r.kind == b"calls"
211 && r.target
212 .as_view()
213 .segments()
214 .last()
215 .unwrap()
216 .name
217 .starts_with(b"inner")
218 })
219 .expect("calls ref for inner");
220 assert_eq!(
221 r.confidence,
222 b"resolved",
223 "call to nested fn must be resolved; got {:?}",
224 std::str::from_utf8(&r.confidence)
225 );
226 let segs: Vec<_> = r.target.as_view().segments().collect();
227 assert!(
228 segs.iter()
229 .any(|s| s.kind == b"function" && s.name.starts_with(b"outer")),
230 "target must be scoped under outer; got {:?}",
231 segs.iter()
232 .map(|s| (
233 std::str::from_utf8(s.kind).unwrap_or("?"),
234 std::str::from_utf8(s.name).unwrap_or("?")
235 ))
236 .collect::<Vec<_>>()
237 );
238 }
239
240 #[test]
241 fn extract_call_hoists_nested_fn_used_before_decl() {
242 let src = r#"
243function outer() {
244 inner();
245 function inner() {}
246}
247"#;
248 let g = extract("util.ts", src, &make_anchor(), false);
249 let r = g
250 .refs()
251 .find(|r| {
252 r.kind == b"calls"
253 && r.target
254 .as_view()
255 .segments()
256 .last()
257 .unwrap()
258 .name
259 .starts_with(b"inner")
260 })
261 .expect("calls ref for inner");
262 assert_eq!(
263 r.confidence,
264 b"resolved",
265 "hoisted nested fn call must be resolved; got {:?}",
266 std::str::from_utf8(&r.confidence)
267 );
268 }
269
270 #[test]
271 fn extract_reads_param_marks_confidence_local() {
272 let g = extract(
273 "util.ts",
274 "function f(x) { return x; }",
275 &make_anchor(),
276 true,
277 );
278 let r = g.refs().find(|r| r.kind == b"reads").expect("reads ref");
279 assert_eq!(r.confidence, b"local".to_vec(), "ref to a param is local");
280 }
281
282 #[test]
283 fn extract_calls_local_function_marks_confidence_local() {
284 let g = extract(
285 "util.ts",
286 "function f() { const helper = () => 1; helper(); }",
287 &make_anchor(),
288 true,
289 );
290 let r = g.refs().find(|r| r.kind == b"calls").expect("calls ref");
291 assert_eq!(
292 r.confidence,
293 b"local".to_vec(),
294 "call into a locally-bound name is local"
295 );
296 }
297
298 #[test]
299 fn extract_local_def_has_no_visibility() {
300 let g = extract(
301 "util.ts",
302 "function f() { let x = 1; }",
303 &make_anchor(),
304 true,
305 );
306 let local = g.defs().find(|d| d.kind == b"local").expect("local def");
307 assert!(
308 local.visibility.is_empty(),
309 "locals must not carry a synthetic visibility, got {:?}",
310 String::from_utf8_lossy(&local.visibility)
311 );
312 }
313
314 #[test]
315 fn extract_param_def_has_no_visibility() {
316 let g = extract("util.ts", "function f(x) {}", &make_anchor(), true);
317 let p = g.defs().find(|d| d.kind == b"param").expect("param def");
318 assert!(p.visibility.is_empty());
319 }
320
321 #[test]
322 fn extract_di_register_fires_only_when_callee_in_preset() {
323 let presets = Presets {
324 di_register_callees: vec!["register".into(), "bind".into()],
325 ..Presets::default()
326 };
327 let g = super::extract(
328 "util.ts",
329 "register(UserService);",
330 &make_anchor(),
331 false,
332 &presets,
333 );
334 assert!(g.refs().any(|r| r.kind == b"di_register"));
335 }
336
337 #[test]
338 fn extract_di_register_silent_without_preset() {
339 let g = extract("util.ts", "register(UserService);", &make_anchor(), false);
340 assert!(
341 g.refs().all(|r| r.kind != b"di_register"),
342 "di_register must stay silent without a preset",
343 );
344 }
345
346 #[test]
347 fn extract_di_register_skips_non_matching_callee() {
348 let presets = Presets {
349 di_register_callees: vec!["register".into()],
350 ..Presets::default()
351 };
352 let g = super::extract("util.ts", "expect(value);", &make_anchor(), false, &presets);
353 assert!(g.refs().all(|r| r.kind != b"di_register"));
354 }
355
356 #[test]
357 fn extract_di_register_register_with_name_and_factory() {
358 let presets = Presets {
359 di_register_callees: vec!["register".into()],
360 ..Presets::default()
361 };
362 let g = super::extract(
363 "util.ts",
364 "register('repoStore', makeRepoStore);",
365 &make_anchor(),
366 false,
367 &presets,
368 );
369 assert!(
370 g.refs().any(|r| r.kind == b"di_register"),
371 "register('name', factory) must emit di_register on the factory identifier",
372 );
373 }
374
375 #[test]
376 fn extract_di_register_member_callee_register() {
377 let presets = Presets {
378 di_register_callees: vec!["register".into()],
379 ..Presets::default()
380 };
381 let g = super::extract(
382 "util.ts",
383 "container.register('repoStore', makeRepoStore);",
384 &make_anchor(),
385 false,
386 &presets,
387 );
388 assert!(
389 g.refs().any(|r| r.kind == b"di_register"),
390 "container.register(...) must emit di_register when 'register' is in the preset",
391 );
392 }
393
394 #[test]
395 fn extract_di_register_recurses_into_factory_call_argument() {
396 let presets = Presets {
397 di_register_callees: vec!["register".into()],
398 ..Presets::default()
399 };
400 let g = super::extract(
401 "util.ts",
402 "register('repoStore', asFunction(makeRepoStore));",
403 &make_anchor(),
404 false,
405 &presets,
406 );
407 assert!(
408 g.refs().any(|r| r.kind == b"di_register"),
409 "register('name', asFunction(make)) must recurse to find 'make'",
410 );
411 }
412
413 #[test]
414 fn extract_di_register_recurses_through_chained_call_postfix() {
415 let presets = Presets {
416 di_register_callees: vec!["asFunction".into()],
417 ..Presets::default()
418 };
419 let g = super::extract(
420 "util.ts",
421 "asFunction(makeRepoStore).singleton();",
422 &make_anchor(),
423 false,
424 &presets,
425 );
426 assert!(
427 g.refs().any(|r| r.kind == b"di_register"),
428 "asFunction(make).singleton() chain must still register the inner 'make'",
429 );
430 }
431
432 #[test]
433 fn extract_di_register_full_awilix_pattern() {
434 let presets = Presets {
435 di_register_callees: vec!["register".into()],
436 ..Presets::default()
437 };
438 let g = super::extract(
439 "util.ts",
440 "container.register('readResource', asFunction(makeReadResource).singleton());",
441 &make_anchor(),
442 false,
443 &presets,
444 );
445 assert!(
446 g.refs().any(|r| r.kind == b"di_register"),
447 "container.register('name', asFunction(make).singleton()) must emit di_register",
448 );
449 }
450 #[test]
451 fn extract_shallow_skips_param_and_local() {
452 let g = extract(
453 "util.ts",
454 "function f(a: number) { let x = 1; }",
455 &make_anchor(),
456 false,
457 );
458 assert!(
459 g.defs().all(|d| d.kind != b"param" && d.kind != b"local"),
460 "shallow extraction must not produce param/local defs"
461 );
462 }
463
464 #[test]
465 fn extract_deep_emits_params_and_locals() {
466 let g = extract(
467 "util.ts",
468 "function f(a: number, b: number) { let sum = a + b; }",
469 &make_anchor(),
470 true,
471 );
472 let pa = MonikerBuilder::new()
473 .project(b"my-app")
474 .segment(b"path", b"main")
475 .segment(b"lang", b"ts")
476 .segment(b"module", b"util")
477 .segment(b"function", b"f(a:number,b:number)")
478 .segment(b"param", b"a")
479 .build();
480 let pb = MonikerBuilder::new()
481 .project(b"my-app")
482 .segment(b"path", b"main")
483 .segment(b"lang", b"ts")
484 .segment(b"module", b"util")
485 .segment(b"function", b"f(a:number,b:number)")
486 .segment(b"param", b"b")
487 .build();
488 let sum = MonikerBuilder::new()
489 .project(b"my-app")
490 .segment(b"path", b"main")
491 .segment(b"lang", b"ts")
492 .segment(b"module", b"util")
493 .segment(b"function", b"f(a:number,b:number)")
494 .segment(b"local", b"sum")
495 .build();
496 assert!(
497 g.contains(&pa),
498 "missing param a; defs: {:?}",
499 g.def_monikers()
500 );
501 assert!(g.contains(&pb));
502 assert!(g.contains(&sum));
503 }
504
505 #[test]
506 fn extract_deep_anonymous_callback_uses_position_name() {
507 let g = extract(
508 "util.ts",
509 "function f() { [1].map(x => x); }",
510 &make_anchor(),
511 true,
512 );
513 let monikers = g.def_monikers();
514 let cb = monikers
515 .iter()
516 .find(|m| {
517 let last = m.as_view().segments().last().unwrap();
518 last.kind == b"function" && last.name.starts_with(b"__cb_")
519 })
520 .expect("anonymous callback def with __cb_ prefix")
521 .clone();
522 let view = cb.as_view();
523 let last = view.segments().last().unwrap();
524 assert_eq!(last.kind, b"function");
525 assert!(g.defs().any(|d| {
526 let dv = d.moniker.as_view();
527 dv.segment_count() == view.segment_count() + 1
528 && dv.segments().last().unwrap().kind == b"param"
529 }));
530 }
531
532 #[test]
533 fn extract_alias_import_routes_to_project_rooted_module() {
534 let presets = Presets {
535 path_aliases: vec![PathAlias {
536 pattern: "@/*".into(),
537 substitution: "./src/*".into(),
538 }],
539 ..Presets::default()
540 };
541 let g = super::extract(
542 "src/router.tsx",
543 "import { AppShell } from '@/components/layout/app-shell';",
544 &make_anchor(),
545 false,
546 &presets,
547 );
548 let r = g.refs().next().expect("one ref");
549 let target = MonikerBuilder::new()
550 .project(b"my-app")
551 .segment(b"path", b"main")
552 .segment(b"lang", b"ts")
553 .segment(b"dir", b"src")
554 .segment(b"dir", b"components")
555 .segment(b"dir", b"layout")
556 .segment(b"module", b"app-shell")
557 .segment(b"path", b"AppShell")
558 .build();
559 assert_eq!(
560 r.target, target,
561 "alias-resolved import must point at the project-rooted module, not external_pkg",
562 );
563 }
564
565 #[test]
566 fn extract_alias_import_keeps_external_when_no_alias_matches() {
567 let presets = Presets {
568 path_aliases: vec![PathAlias {
569 pattern: "@/*".into(),
570 substitution: "./src/*".into(),
571 }],
572 ..Presets::default()
573 };
574 let g = super::extract(
575 "util.ts",
576 "import { join } from '@scope/pkg/sub';",
577 &make_anchor(),
578 false,
579 &presets,
580 );
581 let r = g.refs().next().unwrap();
582 let head = r.target.as_view().segments().next().unwrap();
583 assert_eq!(head.kind, b"external_pkg");
584 }
585
586 #[test]
587 fn extract_unknown_member_call_does_not_claim_sdk_provenance() {
588 let g = extract(
589 "app.ts",
590 "function read(store: Store) { return store.getState(); }",
591 &make_anchor(),
592 false,
593 );
594 let call = g
595 .refs()
596 .find(|reference| {
597 reference.kind == b"method_call"
598 && reference
599 .target
600 .as_view()
601 .segments()
602 .last()
603 .is_some_and(|segment| segment.name == b"getState")
604 })
605 .expect("getState method call");
606 let first = call.target.as_view().segments().next().unwrap();
607 assert_ne!(first.kind, b"sdk");
608 assert_eq!(call.confidence.as_ref(), b"name_match");
609 }
610
611 #[test]
612 fn extract_global_member_call_preserves_runtime_owner() {
613 let g = extract(
614 "app.ts",
615 "function read() { return Object.fromEntries([]); }",
616 &make_anchor(),
617 false,
618 );
619 let call = g
620 .refs()
621 .find(|reference| reference.kind == b"method_call")
622 .expect("Object.fromEntries method call");
623 let segments = call.target.as_view().segments().collect::<Vec<_>>();
624 assert_eq!(
625 segments
626 .iter()
627 .map(|segment| (segment.kind, segment.name))
628 .collect::<Vec<_>>(),
629 vec![
630 (b"sdk".as_slice(), b"ts".as_slice()),
631 (b"path".as_slice(), b"runtime".as_slice()),
632 (b"path".as_slice(), b"Object".as_slice()),
633 (b"method".as_slice(), b"fromEntries".as_slice()),
634 ]
635 );
636 }
637
638 #[test]
639 fn extract_local_binding_shadows_runtime_global_for_member_calls() {
640 let g = extract(
641 "app.ts",
642 "interface Logger { flush(): void } function run(console: Logger) { console.flush(); }",
643 &make_anchor(),
644 true,
645 );
646 let call = g
647 .refs()
648 .find(|reference| {
649 reference.kind == b"method_call" && reference.call_name.as_ref() == b"flush"
650 })
651 .expect("console.flush method call");
652 assert_ne!(
653 call.target.as_view().segments().next().unwrap().kind,
654 b"sdk",
655 "a lexical binding named like a runtime global must win",
656 );
657 }
658
659 #[test]
660 fn extract_module_binding_shadows_runtime_global_for_member_calls() {
661 let g = extract(
662 "app.ts",
663 "const console = { flush() {} }; export function run() { console.flush(); }",
664 &make_anchor(),
665 true,
666 );
667 let call = g
668 .refs()
669 .find(|reference| {
670 reference.kind == b"method_call" && reference.call_name.as_ref() == b"flush"
671 })
672 .expect("console.flush method call");
673 assert_ne!(
674 call.target.as_view().segments().next().unwrap().kind,
675 b"sdk",
676 "a module binding named like a runtime global must win",
677 );
678 let segments = call.target.as_view().segments().collect::<Vec<_>>();
679 assert!(
680 segments.windows(2).any(|pair| {
681 pair[0].kind == b"const"
682 && pair[0].name == b"console"
683 && pair[1].kind == b"method"
684 && pair[1].name == b"flush"
685 }),
686 "unexpected target {segments:?}",
687 );
688 }
689
690 #[test]
691 fn extract_jsx_expression_identifier_still_emits_read() {
692 let g = extract(
693 "app.tsx",
694 "function App(label: string) { return <div>{label}</div>; }",
695 &make_anchor(),
696 true,
697 );
698 assert!(
699 g.refs().any(|r| r.kind == b"reads"
700 && r.target.as_view().segments().last().unwrap().name == b"label"),
701 "identifier inside jsx_expression must still surface as a read",
702 );
703 }
704
705 #[test]
706 fn extract_closure_read_targets_outer_param_def() {
707 let src = "function outer({ x }: { x: string }) { return function inner() { return x; }; }";
708 let g = extract("util.ts", src, &make_anchor(), true);
709 let read = g
710 .refs()
711 .find(|r| {
712 r.kind == b"reads" && r.target.as_view().segments().last().unwrap().name == b"x"
713 })
714 .expect("reads ref for x");
715 let segs: Vec<_> = read.target.as_view().segments().collect();
716 assert!(
717 segs.iter().any(|s| s.kind == b"param" && s.name == b"x"),
718 "target must terminate with param:x of the defining frame, got: {segs:?}"
719 );
720 assert!(
721 !segs
722 .iter()
723 .any(|s| s.kind == b"function" && s.name == b"inner()"),
724 "target must NOT carry the inner frame segment, got: {segs:?}"
725 );
726 }
727
728 #[test]
729 fn extract_closure_uses_type_targets_outer_type_alias_def() {
730 let src = "function outer() { type Local = string; function inner(x: Local): Local { return x; } return inner; }";
731 let g = extract("util.ts", src, &make_anchor(), true);
732 let r = g
733 .refs()
734 .find(|r| {
735 r.kind == b"uses_type"
736 && r.target.as_view().segments().last().unwrap().name == b"Local"
737 })
738 .expect("uses_type ref for Local");
739 let segs: Vec<_> = r.target.as_view().segments().collect();
740 assert!(
741 segs.iter().any(|s| s.kind == b"type" && s.name == b"Local"),
742 "target must terminate with type:Local of the defining frame, got: {segs:?}"
743 );
744 assert!(
745 segs.iter()
746 .any(|s| s.kind == b"function" && s.name.starts_with(b"outer")),
747 "target must be parented under outer (the defining frame), got: {segs:?}"
748 );
749 }
750
751 #[test]
752 fn extract_closure_uses_type_targets_outer_interface_def() {
753 let src = "function outer() { interface Local { v: string; } function inner(x: Local): Local { return x; } return inner; }";
754 let g = extract("util.ts", src, &make_anchor(), true);
755 let r = g
756 .refs()
757 .find(|r| {
758 r.kind == b"uses_type"
759 && r.target.as_view().segments().last().unwrap().name == b"Local"
760 })
761 .expect("uses_type ref for Local");
762 let segs: Vec<_> = r.target.as_view().segments().collect();
763 assert!(
764 segs.iter()
765 .any(|s| s.kind == b"interface" && s.name == b"Local"),
766 "target must terminate with interface:Local of the defining frame, got: {segs:?}"
767 );
768 assert!(
769 segs.iter()
770 .any(|s| s.kind == b"function" && s.name.starts_with(b"outer")),
771 "target must be parented under outer, got: {segs:?}"
772 );
773 }
774
775 #[test]
776 fn extract_closure_instantiates_targets_outer_class_def() {
777 let src = "function outer() { class Local { ok = true; } function inner() { return new Local(); } return inner; }";
778 let g = extract("util.ts", src, &make_anchor(), true);
779 let r = g
780 .refs()
781 .find(|r| {
782 r.kind == b"instantiates"
783 && r.target.as_view().segments().last().unwrap().name == b"Local"
784 })
785 .expect("instantiates ref for Local");
786 let segs: Vec<_> = r.target.as_view().segments().collect();
787 assert!(
788 segs.iter()
789 .any(|s| s.kind == b"class" && s.name == b"Local"),
790 "target must terminate with class:Local of the defining frame, got: {segs:?}"
791 );
792 assert!(
793 segs.iter()
794 .any(|s| s.kind == b"function" && s.name.starts_with(b"outer")),
795 "target must be parented under outer, got: {segs:?}"
796 );
797 }
798
799 #[test]
800 fn extract_closure_uses_type_targets_outer_enum_def() {
801 let src = "function outer() { enum Mode { A, B } function inner(m: Mode): Mode { return m; } return inner; }";
802 let g = extract("util.ts", src, &make_anchor(), true);
803 let r = g
804 .refs()
805 .find(|r| {
806 r.kind == b"uses_type"
807 && r.target.as_view().segments().last().unwrap().name == b"Mode"
808 })
809 .expect("uses_type ref for Mode");
810 let segs: Vec<_> = r.target.as_view().segments().collect();
811 assert!(
812 segs.iter().any(|s| s.kind == b"enum" && s.name == b"Mode"),
813 "target must terminate with enum:Mode of the defining frame, got: {segs:?}"
814 );
815 assert!(
816 segs.iter()
817 .any(|s| s.kind == b"function" && s.name.starts_with(b"outer")),
818 "target must be parented under outer, got: {segs:?}"
819 );
820 }
821
822 #[test]
823 fn extract_closure_call_targets_outer_local_def() {
824 let src = "function outer() { const helper = () => 1; return function inner() { return helper(); }; }";
825 let g = extract("util.ts", src, &make_anchor(), true);
826 let call = g
827 .refs()
828 .find(|r| {
829 r.kind == b"calls"
830 && r.target.as_view().segments().last().unwrap().name == b"helper"
831 })
832 .expect("calls ref for helper");
833 let segs: Vec<_> = call.target.as_view().segments().collect();
834 assert!(
835 segs.iter()
836 .any(|s| s.kind == b"local" && s.name == b"helper"),
837 "target must terminate with local:helper of the defining frame, got: {segs:?}"
838 );
839 assert!(
840 !segs
841 .iter()
842 .any(|s| s.kind == b"function" && s.name == b"inner()"),
843 "target must NOT carry the inner frame segment, got: {segs:?}"
844 );
845 }
846}