1use tree_sitter::{Language, Parser, Tree};
2
3use crate::core::code_graph::CodeGraph;
4use crate::core::moniker::Moniker;
5
6use crate::lang::canonical_walker::CanonicalWalker;
7
8pub mod build;
9mod canonicalize;
10mod kinds;
11mod strategy;
12
13use canonicalize::compute_module_moniker;
14use strategy::{Strategy, collect_callable_table, collect_export_ranges};
15
16pub fn parse(source: &str) -> Tree {
17 let mut parser = Parser::new();
18 let language: Language = tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into();
19 parser
20 .set_language(&language)
21 .expect("failed to load tree-sitter TypeScript grammar");
22 parser
23 .parse(source, None)
24 .expect("tree-sitter parse returned None on a non-cancelled call")
25}
26
27#[derive(Clone, Debug, Default)]
28pub struct Presets {
29 pub di_register_callees: Vec<String>,
30}
31
32pub fn extract(
33 uri: &str,
34 source: &str,
35 anchor: &Moniker,
36 deep: bool,
37 presets: &Presets,
38) -> CodeGraph {
39 let module = compute_module_moniker(anchor, uri);
40 let (def_cap, ref_cap) = CodeGraph::capacity_for_source(source.len());
41 let mut graph = CodeGraph::with_capacity(module.clone(), kinds::MODULE, def_cap, ref_cap);
42 let tree = parse(source);
43 let export_ranges = collect_export_ranges(tree.root_node());
44 let mut callable_table: std::collections::HashMap<(Moniker, Vec<u8>), Vec<u8>> =
45 std::collections::HashMap::new();
46 collect_callable_table(
47 tree.root_node(),
48 source.as_bytes(),
49 &module,
50 &mut callable_table,
51 );
52 let strat = Strategy {
53 module: module.clone(),
54 source_bytes: source.as_bytes(),
55 deep,
56 presets,
57 export_ranges,
58 local_scope: std::cell::RefCell::new(Vec::new()),
59 imports: std::cell::RefCell::new(std::collections::HashMap::new()),
60 callable_table,
61 };
62 let walker = CanonicalWalker::new(&strat, source.as_bytes());
63 walker.walk(tree.root_node(), &module, &mut graph);
64 graph
65}
66
67pub struct Lang;
68
69impl crate::lang::LangExtractor for Lang {
70 type Presets = Presets;
71 const LANG_TAG: &'static str = "ts";
72 const ALLOWED_KINDS: &'static [&'static str] = &[
73 "class",
74 "interface",
75 "type",
76 "function",
77 "method",
78 "const",
79 "enum",
80 "constructor",
81 "field",
82 "enum_constant",
83 "namespace",
84 ];
85 const ALLOWED_VISIBILITIES: &'static [&'static str] =
86 &["public", "private", "protected", "module"];
87
88 fn extract(
89 uri: &str,
90 source: &str,
91 anchor: &Moniker,
92 deep: bool,
93 presets: &Self::Presets,
94 ) -> CodeGraph {
95 extract(uri, source, anchor, deep, presets)
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use crate::core::moniker::MonikerBuilder;
103 use crate::lang::assert_conformance;
104
105 fn extract(uri: &str, source: &str, anchor: &Moniker, deep: bool) -> CodeGraph {
106 let g = super::extract(uri, source, anchor, deep, &Presets::default());
107 assert_conformance::<super::Lang>(&g, anchor);
108 g
109 }
110
111 fn make_anchor() -> Moniker {
112 MonikerBuilder::new()
113 .project(b"my-app")
114 .segment(b"path", b"main")
115 .build()
116 }
117
118 #[test]
119 fn parse_empty_source_returns_program() {
120 let tree = parse("");
121 assert_eq!(tree.root_node().kind(), "program");
122 assert_eq!(tree.root_node().child_count(), 0);
123 }
124
125 #[test]
126 fn parse_simple_class_has_class_declaration() {
127 let tree = parse("class Foo {}");
128 assert_eq!(
129 tree.root_node().child(0).unwrap().kind(),
130 "class_declaration"
131 );
132 }
133
134 #[test]
135 fn parse_invalid_syntax_marks_errors() {
136 assert!(parse("class { ").root_node().has_error());
137 }
138
139 #[test]
140 fn extract_empty_source_yields_module_only_graph() {
141 let anchor = make_anchor();
142 let graph = extract("src/lib/util.ts", "", &anchor, false);
143 assert_eq!(graph.def_count(), 1);
144 assert_eq!(graph.ref_count(), 0);
145
146 let expected = MonikerBuilder::new()
147 .project(b"my-app")
148 .segment(b"path", b"main")
149 .segment(b"lang", b"ts")
150 .segment(b"dir", b"src")
151 .segment(b"dir", b"lib")
152 .segment(b"module", b"util")
153 .build();
154 assert_eq!(graph.root(), &expected);
155 }
156
157 #[test]
158 fn extract_strips_each_known_extension() {
159 let anchor = make_anchor();
160 for uri in [
161 "foo.ts", "foo.tsx", "foo.js", "foo.jsx", "foo.mjs", "foo.cjs",
162 ] {
163 let g = extract(uri, "", &anchor, false);
164 let last = g.root().as_view().segments().last().unwrap();
165 assert_eq!(last.name, b"foo", "extension not stripped on {uri}");
166 }
167 }
168
169 #[test]
170 fn extract_simple_class_emits_class_def() {
171 let anchor = make_anchor();
172 let graph = extract("util.ts", "class Foo {}", &anchor, false);
173 assert_eq!(graph.def_count(), 2);
174
175 let foo = MonikerBuilder::new()
176 .project(b"my-app")
177 .segment(b"path", b"main")
178 .segment(b"lang", b"ts")
179 .segment(b"module", b"util")
180 .segment(b"class", b"Foo")
181 .build();
182 assert!(graph.contains(&foo));
183 }
184
185 #[test]
186 fn extract_export_class_descends_into_export_statement() {
187 let anchor = make_anchor();
188 let graph = extract("util.ts", "export class Foo {}", &anchor, false);
189 assert_eq!(graph.def_count(), 2);
190 }
191
192 #[test]
193 fn extract_class_with_method_emits_method_def() {
194 let anchor = make_anchor();
195 let graph = extract("util.ts", "class Foo { bar() {} }", &anchor, false);
196 assert_eq!(graph.def_count(), 3);
197
198 let bar = MonikerBuilder::new()
199 .project(b"my-app")
200 .segment(b"path", b"main")
201 .segment(b"lang", b"ts")
202 .segment(b"module", b"util")
203 .segment(b"class", b"Foo")
204 .segment(b"method", b"bar()")
205 .build();
206 assert!(graph.contains(&bar));
207 }
208
209 #[test]
210 fn extract_function_declaration_emits_def() {
211 let anchor = make_anchor();
212 let graph = extract("util.ts", "function foo() {}", &anchor, false);
213 assert_eq!(graph.def_count(), 2);
214
215 let foo = MonikerBuilder::new()
216 .project(b"my-app")
217 .segment(b"path", b"main")
218 .segment(b"lang", b"ts")
219 .segment(b"module", b"util")
220 .segment(b"function", b"foo()")
221 .build();
222 assert!(graph.contains(&foo));
223 }
224 #[test]
225 fn extract_named_import_emits_imports_symbol_per_specifier() {
226 let g = extract(
227 "src/util.ts",
228 "import { Bar, Baz } from './bar';",
229 &make_anchor(),
230 false,
231 );
232 let kinds: Vec<_> = g.refs().map(|r| r.kind.clone()).collect();
233 assert_eq!(kinds.len(), 2, "one ref per named specifier; got {kinds:?}");
234 assert!(kinds.iter().all(|k| k == b"imports_symbol"));
235
236 let bar = MonikerBuilder::new()
237 .project(b"my-app")
238 .segment(b"path", b"main")
239 .segment(b"lang", b"ts")
240 .segment(b"dir", b"src")
241 .segment(b"module", b"bar")
242 .segment(b"path", b"Bar")
243 .build();
244 let baz = MonikerBuilder::new()
245 .project(b"my-app")
246 .segment(b"path", b"main")
247 .segment(b"lang", b"ts")
248 .segment(b"dir", b"src")
249 .segment(b"module", b"bar")
250 .segment(b"path", b"Baz")
251 .build();
252 let targets: Vec<_> = g.refs().map(|r| r.target.clone()).collect();
253 assert!(targets.contains(&bar), "missing Bar target: {targets:?}");
254 assert!(targets.contains(&baz));
255 }
256
257 #[test]
258 fn extract_default_import_emits_imports_symbol_default() {
259 let g = extract("util.ts", "import Foo from './foo';", &make_anchor(), false);
260 let r = g.refs().next().expect("one ref");
261 assert_eq!(r.kind, b"imports_symbol".to_vec());
262 let target = MonikerBuilder::new()
263 .project(b"my-app")
264 .segment(b"path", b"main")
265 .segment(b"lang", b"ts")
266 .segment(b"module", b"foo")
267 .segment(b"path", b"default")
268 .build();
269 assert_eq!(r.target, target);
270 }
271
272 #[test]
273 fn extract_namespace_import_emits_imports_module() {
274 let g = extract(
275 "util.ts",
276 "import * as M from './foo';",
277 &make_anchor(),
278 false,
279 );
280 let r = g.refs().next().unwrap();
281 assert_eq!(r.kind, b"imports_module".to_vec());
282 let target = MonikerBuilder::new()
283 .project(b"my-app")
284 .segment(b"path", b"main")
285 .segment(b"lang", b"ts")
286 .segment(b"module", b"foo")
287 .build();
288 assert_eq!(r.target, target);
289 }
290
291 #[test]
292 fn extract_bare_import_resolves_to_external_pkg() {
293 let g = extract(
294 "util.ts",
295 "import { useState } from 'react';",
296 &make_anchor(),
297 false,
298 );
299 let r = g.refs().next().unwrap();
300 assert_eq!(r.kind, b"imports_symbol".to_vec());
301 let target = MonikerBuilder::new()
302 .project(b"my-app")
303 .segment(b"external_pkg", b"react")
304 .segment(b"path", b"useState")
305 .build();
306 assert_eq!(r.target, target);
307 }
308
309 #[test]
310 fn extract_scoped_bare_import_keeps_full_scope() {
311 let g = extract(
312 "util.ts",
313 "import { join } from '@scope/pkg/sub';",
314 &make_anchor(),
315 false,
316 );
317 let r = g.refs().next().unwrap();
318 let target = MonikerBuilder::new()
319 .project(b"my-app")
320 .segment(b"external_pkg", b"@scope/pkg")
321 .segment(b"path", b"sub")
322 .segment(b"path", b"join")
323 .build();
324 assert_eq!(r.target, target);
325 }
326
327 #[test]
328 fn extract_dot_only_specifier_resolves_relative_not_external() {
329 let g = extract(
330 "src/__tests__/foo.test.ts",
331 "import { z } from \"..\";",
332 &make_anchor(),
333 false,
334 );
335 let r = g.refs().next().unwrap();
336 let target = MonikerBuilder::new()
337 .project(b"my-app")
338 .segment(b"path", b"main")
339 .segment(b"lang", b"ts")
340 .segment(b"dir", b"src")
341 .segment(b"path", b"z")
342 .build();
343 assert_eq!(r.target, target);
344 }
345
346 #[test]
347 fn extract_dotdot_import_walks_up_then_down() {
348 let g = extract(
349 "src/lib/foo.ts",
350 "import { X } from '../other';",
351 &make_anchor(),
352 false,
353 );
354 let r = g.refs().next().unwrap();
355 let target = MonikerBuilder::new()
356 .project(b"my-app")
357 .segment(b"path", b"main")
358 .segment(b"lang", b"ts")
359 .segment(b"dir", b"src")
360 .segment(b"module", b"other")
361 .segment(b"path", b"X")
362 .build();
363 assert_eq!(r.target, target);
364 }
365
366 #[test]
367 fn extract_side_effect_import_emits_imports_module() {
368 let g = extract("util.ts", "import 'side-effects';", &make_anchor(), false);
369 let r = g.refs().next().unwrap();
370 assert_eq!(r.kind, b"imports_module".to_vec());
371 }
372 #[test]
373 fn extract_named_reexport_emits_reexports_per_specifier() {
374 let g = extract(
375 "index.ts",
376 "export { Foo, Bar } from './lib';",
377 &make_anchor(),
378 false,
379 );
380 let kinds: Vec<_> = g.refs().map(|r| r.kind.clone()).collect();
381 assert_eq!(kinds.len(), 2);
382 assert!(kinds.iter().all(|k| k == b"reexports"));
383 }
384
385 #[test]
386 fn extract_star_reexport_emits_single_reexports_ref() {
387 let g = extract("index.ts", "export * from './lib';", &make_anchor(), false);
388 assert_eq!(g.ref_count(), 1);
389 let r = g.refs().next().unwrap();
390 assert_eq!(r.kind, b"reexports".to_vec());
391 }
392 #[test]
393 fn call_to_named_import_carries_imported_confidence() {
394 let g = extract(
395 "util.ts",
396 "import { run } from './foo';\nrun();",
397 &make_anchor(),
398 false,
399 );
400 let r = g.refs().find(|r| r.kind == b"calls").expect("calls ref");
401 assert_eq!(r.confidence, b"imported");
402 }
403
404 #[test]
405 fn call_to_bare_import_carries_external_confidence() {
406 let g = extract(
407 "util.ts",
408 "import { useState } from 'react';\nuseState();",
409 &make_anchor(),
410 false,
411 );
412 let r = g.refs().find(|r| r.kind == b"calls").expect("calls ref");
413 assert_eq!(r.confidence, b"external");
414 }
415
416 #[test]
417 fn method_call_on_imported_namespace_carries_external_confidence() {
418 let g = extract(
419 "util.ts",
420 "import * as fs from 'fs';\nfs.readFile();",
421 &make_anchor(),
422 false,
423 );
424 let r = g
425 .refs()
426 .find(|r| r.kind == b"method_call")
427 .expect("method_call");
428 assert_eq!(r.confidence, b"external");
429 assert_eq!(r.receiver_hint, b"fs");
430 }
431
432 #[test]
433 fn new_on_imported_class_carries_imported_confidence() {
434 let g = extract(
435 "util.ts",
436 "import { Foo } from './foo';\nnew Foo();",
437 &make_anchor(),
438 false,
439 );
440 let r = g
441 .refs()
442 .find(|r| r.kind == b"instantiates")
443 .expect("instantiates");
444 assert_eq!(r.confidence, b"imported");
445 }
446
447 #[test]
448 fn uses_type_of_imported_type_carries_imported_confidence() {
449 let g = extract(
450 "util.ts",
451 "import type { Opts } from './types';\nfunction f(o: Opts) { return o; }",
452 &make_anchor(),
453 false,
454 );
455 let r = g
456 .refs()
457 .find(|r| r.kind == b"uses_type")
458 .expect("uses_type");
459 assert_eq!(r.confidence, b"imported");
460 }
461
462 #[test]
463 fn call_to_non_imported_identifier_stays_name_match() {
464 let g = extract("util.ts", "foo();", &make_anchor(), false);
465 let r = g.refs().find(|r| r.kind == b"calls").expect("calls ref");
466 assert_eq!(r.confidence, b"name_match");
467 }
468
469 #[test]
470 fn extract_interface_emits_interface_def() {
471 let g = extract(
472 "util.ts",
473 "interface Greet { hi(): void; }",
474 &make_anchor(),
475 false,
476 );
477 let greet = MonikerBuilder::new()
478 .project(b"my-app")
479 .segment(b"path", b"main")
480 .segment(b"lang", b"ts")
481 .segment(b"module", b"util")
482 .segment(b"interface", b"Greet")
483 .build();
484 assert!(g.contains(&greet));
485 let hi = MonikerBuilder::new()
486 .project(b"my-app")
487 .segment(b"path", b"main")
488 .segment(b"lang", b"ts")
489 .segment(b"module", b"util")
490 .segment(b"interface", b"Greet")
491 .segment(b"method", b"hi()")
492 .build();
493 assert!(
494 g.contains(&hi),
495 "method_signature in interface body must be a method def"
496 );
497 }
498
499 #[test]
500 fn extract_enum_emits_enum_constants() {
501 let g = extract(
502 "util.ts",
503 "enum Color { Red, Green = 1 }",
504 &make_anchor(),
505 false,
506 );
507 let red = MonikerBuilder::new()
508 .project(b"my-app")
509 .segment(b"path", b"main")
510 .segment(b"lang", b"ts")
511 .segment(b"module", b"util")
512 .segment(b"enum", b"Color")
513 .segment(b"enum_constant", b"Red")
514 .build();
515 assert!(
516 g.contains(&red),
517 "missing Red enum constant; defs: {:?}",
518 g.def_monikers()
519 );
520 }
521
522 #[test]
523 fn extract_type_alias_emits_type_alias_def() {
524 let g = extract("util.ts", "type Id = string;", &make_anchor(), false);
525 let id = MonikerBuilder::new()
526 .project(b"my-app")
527 .segment(b"path", b"main")
528 .segment(b"lang", b"ts")
529 .segment(b"module", b"util")
530 .segment(b"type", b"Id")
531 .build();
532 assert!(g.contains(&id));
533 }
534 #[test]
535 fn extract_method_signature_encoded_in_segment_name() {
536 let g = extract(
537 "util.ts",
538 "class Foo { bar(a: number, b: string) {} }",
539 &make_anchor(),
540 false,
541 );
542 let bar = MonikerBuilder::new()
543 .project(b"my-app")
544 .segment(b"path", b"main")
545 .segment(b"lang", b"ts")
546 .segment(b"module", b"util")
547 .segment(b"class", b"Foo")
548 .segment(b"method", b"bar(a:number,b:string)")
549 .build();
550 assert!(
551 g.contains(&bar),
552 "expected typed segment, defs: {:?}",
553 g.def_monikers()
554 );
555 }
556
557 #[test]
558 fn extract_constructor_uses_constructor_kind() {
559 let g = extract(
560 "util.ts",
561 "class Foo { constructor(x: number) {} }",
562 &make_anchor(),
563 false,
564 );
565 let ctor = MonikerBuilder::new()
566 .project(b"my-app")
567 .segment(b"path", b"main")
568 .segment(b"lang", b"ts")
569 .segment(b"module", b"util")
570 .segment(b"class", b"Foo")
571 .segment(b"constructor", b"constructor(x:number)")
572 .build();
573 assert!(g.contains(&ctor));
574 }
575
576 #[test]
577 fn extract_class_field_emits_field_def() {
578 let g = extract(
579 "util.ts",
580 "class Foo { x: number = 0; }",
581 &make_anchor(),
582 false,
583 );
584 let x = MonikerBuilder::new()
585 .project(b"my-app")
586 .segment(b"path", b"main")
587 .segment(b"lang", b"ts")
588 .segment(b"module", b"util")
589 .segment(b"class", b"Foo")
590 .segment(b"field", b"x")
591 .build();
592 assert!(g.contains(&x));
593 }
594
595 #[test]
596 fn extract_module_const_emits_const_def() {
597 let g = extract("util.ts", "const PI = 3.14;", &make_anchor(), false);
598 let pi = MonikerBuilder::new()
599 .project(b"my-app")
600 .segment(b"path", b"main")
601 .segment(b"lang", b"ts")
602 .segment(b"module", b"util")
603 .segment(b"const", b"PI")
604 .build();
605 assert!(g.contains(&pi));
606 }
607
608 #[test]
609 fn extract_arrow_const_emits_function_def() {
610 let g = extract(
611 "util.ts",
612 "const add = (a: number, b: number) => a + b;",
613 &make_anchor(),
614 false,
615 );
616 let add = MonikerBuilder::new()
617 .project(b"my-app")
618 .segment(b"path", b"main")
619 .segment(b"lang", b"ts")
620 .segment(b"module", b"util")
621 .segment(b"function", b"add(a:number,b:number)")
622 .build();
623 assert!(
624 g.contains(&add),
625 "arrow-as-const must be a function def; defs: {:?}",
626 g.def_monikers()
627 );
628 }
629 #[test]
630 fn extract_top_level_call_emits_calls_ref() {
631 let g = extract("util.ts", "foo(1);", &make_anchor(), false);
632 let r = g.refs().find(|r| r.kind == b"calls").expect("calls ref");
633 assert_eq!(r.source, 0, "top-level call sources on the module");
634 let target = MonikerBuilder::new()
635 .project(b"my-app")
636 .segment(b"path", b"main")
637 .segment(b"lang", b"ts")
638 .segment(b"module", b"util")
639 .segment(b"function", b"foo")
640 .build();
641 assert_eq!(r.target, target);
642 }
643
644 #[test]
645 fn extract_visibility_module_for_unexported_class() {
646 let g = extract("util.ts", "class Foo {}", &make_anchor(), false);
647 let foo = g.defs().find(|d| d.kind == b"class").unwrap();
648 assert_eq!(foo.visibility, b"module".to_vec());
649 }
650
651 #[test]
652 fn extract_visibility_public_for_exported_class() {
653 let g = extract("util.ts", "export class Foo {}", &make_anchor(), false);
654 let foo = g.defs().find(|d| d.kind == b"class").unwrap();
655 assert_eq!(foo.visibility, b"public".to_vec());
656 }
657
658 #[test]
659 fn extract_visibility_for_class_member_modifiers() {
660 let g = extract(
661 "util.ts",
662 "export class C { public a() {}; protected b() {}; private c() {}; d() {} }",
663 &make_anchor(),
664 false,
665 );
666 let by_name = |n: &[u8]| {
667 g.defs()
668 .find(|d| d.moniker.as_view().segments().last().unwrap().name == n)
669 .unwrap()
670 .visibility
671 .clone()
672 };
673 assert_eq!(by_name(b"a()"), b"public".to_vec());
674 assert_eq!(by_name(b"b()"), b"protected".to_vec());
675 assert_eq!(by_name(b"c()"), b"private".to_vec());
676 assert_eq!(
677 by_name(b"d()"),
678 b"public".to_vec(),
679 "no modifier defaults to public"
680 );
681 }
682
683 #[test]
684 fn extract_named_import_alias_recorded() {
685 let g = extract(
686 "util.ts",
687 "import { X as Y } from './foo';",
688 &make_anchor(),
689 false,
690 );
691 let r = g.refs().next().unwrap();
692 assert_eq!(r.alias, b"Y".to_vec());
693 }
694
695 #[test]
696 fn extract_namespace_import_alias_recorded() {
697 let g = extract(
698 "util.ts",
699 "import * as Mod from './foo';",
700 &make_anchor(),
701 false,
702 );
703 let r = g.refs().next().unwrap();
704 assert_eq!(r.alias, b"Mod".to_vec());
705 }
706
707 #[test]
708 fn extract_reads_param_marks_confidence_local() {
709 let g = extract(
710 "util.ts",
711 "function f(x) { return x; }",
712 &make_anchor(),
713 true,
714 );
715 let r = g.refs().find(|r| r.kind == b"reads").expect("reads ref");
716 assert_eq!(r.confidence, b"local".to_vec(), "ref to a param is local");
717 }
718
719 #[test]
720 fn extract_reads_unbound_identifier_marks_name_match() {
721 let g = extract(
722 "util.ts",
723 "function f() { return outsideVar; }",
724 &make_anchor(),
725 false,
726 );
727 let r = g.refs().find(|r| r.kind == b"reads").unwrap();
728 assert_eq!(r.confidence, b"name_match".to_vec());
729 }
730
731 #[test]
732 fn extract_calls_local_function_marks_confidence_local() {
733 let g = extract(
734 "util.ts",
735 "function f() { const helper = () => 1; helper(); }",
736 &make_anchor(),
737 true,
738 );
739 let r = g.refs().find(|r| r.kind == b"calls").expect("calls ref");
740 assert_eq!(
741 r.confidence,
742 b"local".to_vec(),
743 "call into a locally-bound name is local"
744 );
745 }
746
747 #[test]
748 fn extract_local_def_has_no_visibility() {
749 let g = extract(
750 "util.ts",
751 "function f() { let x = 1; }",
752 &make_anchor(),
753 true,
754 );
755 let local = g.defs().find(|d| d.kind == b"local").expect("local def");
756 assert!(
757 local.visibility.is_empty(),
758 "locals must not carry a synthetic visibility, got {:?}",
759 String::from_utf8_lossy(&local.visibility)
760 );
761 }
762
763 #[test]
764 fn extract_param_def_has_no_visibility() {
765 let g = extract("util.ts", "function f(x) {}", &make_anchor(), true);
766 let p = g.defs().find(|d| d.kind == b"param").expect("param def");
767 assert!(p.visibility.is_empty());
768 }
769
770 #[test]
771 fn extract_import_confidence_distinguishes_relative_vs_external() {
772 let g = extract(
773 "util.ts",
774 "import { a } from './local';\nimport { b } from 'react';",
775 &make_anchor(),
776 false,
777 );
778 let confs: Vec<&[u8]> = g.refs().map(|r| r.confidence.as_slice()).collect();
779 assert!(confs.contains(&b"imported".as_slice()));
780 assert!(confs.contains(&b"external".as_slice()));
781 }
782
783 #[test]
784 fn extract_method_call_carries_receiver_hint() {
785 let cases = [
786 ("class C { m() { this.bar(); } }", b"this".as_slice()),
787 ("class C { m() { super.bar(); } }", b"super".as_slice()),
788 ("obj.bar();", b"obj".as_slice()),
789 ("a.b.bar();", b"member".as_slice()),
790 ("foo().bar();", b"call".as_slice()),
791 ];
792 for (src, expected) in cases {
793 let g = extract("util.ts", src, &make_anchor(), false);
794 let r = g
795 .refs()
796 .find(|r| r.kind == b"method_call")
797 .unwrap_or_else(|| panic!("no method_call ref for: {src}"));
798 assert_eq!(
799 r.receiver_hint.as_slice(),
800 expected,
801 "receiver hint mismatch for {src:?}"
802 );
803 }
804 }
805
806 #[test]
807 fn extract_method_call_receiver_hint_carries_imported_alias() {
808 let g = extract(
809 "explorer.ts",
810 "import { z } from 'zod';\nconst schema = z.string();",
811 &make_anchor(),
812 false,
813 );
814 let r = g
815 .refs()
816 .find(|r| r.kind == b"method_call")
817 .expect("method_call ref");
818 assert_eq!(
819 r.receiver_hint.as_slice(),
820 b"z",
821 "receiver hint must carry the alias text so the consumer can join to imports_symbol",
822 );
823 }
824
825 #[test]
826 fn extract_method_call_emits_method_call_ref() {
827 let g = extract("util.ts", "obj.bar(1, 2);", &make_anchor(), false);
828 let r = g
829 .refs()
830 .find(|r| r.kind == b"method_call")
831 .expect("method_call ref");
832 let target = MonikerBuilder::new()
833 .project(b"my-app")
834 .segment(b"path", b"main")
835 .segment(b"lang", b"ts")
836 .segment(b"module", b"util")
837 .segment(b"method", b"bar")
838 .build();
839 assert_eq!(r.target, target);
840 }
841
842 #[test]
843 fn extract_call_inside_method_sources_on_method() {
844 let g = extract(
845 "util.ts",
846 "class C { m() { foo(); } }",
847 &make_anchor(),
848 false,
849 );
850 let r = g.refs().find(|r| r.kind == b"calls").expect("calls ref");
851 let m_def = MonikerBuilder::new()
852 .project(b"my-app")
853 .segment(b"path", b"main")
854 .segment(b"lang", b"ts")
855 .segment(b"module", b"util")
856 .segment(b"class", b"C")
857 .segment(b"method", b"m()")
858 .build();
859 assert_eq!(g.defs().nth(r.source).unwrap().moniker, m_def);
860 }
861
862 #[test]
863 fn extract_new_expression_emits_instantiates() {
864 let g = extract("util.ts", "const x = new Foo();", &make_anchor(), false);
865 let r = g
866 .refs()
867 .find(|r| r.kind == b"instantiates")
868 .expect("instantiates ref");
869 let target = MonikerBuilder::new()
870 .project(b"my-app")
871 .segment(b"path", b"main")
872 .segment(b"lang", b"ts")
873 .segment(b"module", b"util")
874 .segment(b"class", b"Foo")
875 .build();
876 assert_eq!(r.target, target);
877 }
878
879 #[test]
880 fn extract_class_extends_emits_extends_ref() {
881 let g = extract("util.ts", "class A extends B {}", &make_anchor(), false);
882 let r = g
883 .refs()
884 .find(|r| r.kind == b"extends")
885 .expect("extends ref");
886 let target = MonikerBuilder::new()
887 .project(b"my-app")
888 .segment(b"path", b"main")
889 .segment(b"lang", b"ts")
890 .segment(b"module", b"util")
891 .segment(b"class", b"B")
892 .build();
893 assert_eq!(r.target, target);
894 }
895
896 #[test]
897 fn extract_class_implements_emits_implements_ref() {
898 let g = extract("util.ts", "class A implements I {}", &make_anchor(), false);
899 let r = g
900 .refs()
901 .find(|r| r.kind == b"implements")
902 .expect("implements ref");
903 let target = MonikerBuilder::new()
904 .project(b"my-app")
905 .segment(b"path", b"main")
906 .segment(b"lang", b"ts")
907 .segment(b"module", b"util")
908 .segment(b"interface", b"I")
909 .build();
910 assert_eq!(r.target, target);
911 }
912
913 #[test]
914 fn extract_decorator_emits_annotates_ref() {
915 let g = extract("util.ts", "@Injectable class A {}", &make_anchor(), false);
916 let r = g
917 .refs()
918 .find(|r| r.kind == b"annotates")
919 .expect("annotates ref");
920 let target = MonikerBuilder::new()
921 .project(b"my-app")
922 .segment(b"path", b"main")
923 .segment(b"lang", b"ts")
924 .segment(b"module", b"util")
925 .segment(b"function", b"Injectable")
926 .build();
927 assert_eq!(r.target, target);
928 }
929
930 #[test]
931 fn extract_decorator_call_uses_name_only_target() {
932 let g = extract("util.ts", "@Bind('x') class A {}", &make_anchor(), false);
933 let r = g.refs().find(|r| r.kind == b"annotates").unwrap();
934 let target = MonikerBuilder::new()
935 .project(b"my-app")
936 .segment(b"path", b"main")
937 .segment(b"lang", b"ts")
938 .segment(b"module", b"util")
939 .segment(b"function", b"Bind")
940 .build();
941 assert_eq!(r.target, target);
942 }
943 #[test]
944 fn extract_param_type_annotation_emits_uses_type() {
945 let g = extract(
946 "util.ts",
947 "function f(x: Foo): Bar { return x as Bar; }",
948 &make_anchor(),
949 false,
950 );
951 let foo = MonikerBuilder::new()
952 .project(b"my-app")
953 .segment(b"path", b"main")
954 .segment(b"lang", b"ts")
955 .segment(b"module", b"util")
956 .segment(b"class", b"Foo")
957 .build();
958 let bar = MonikerBuilder::new()
959 .project(b"my-app")
960 .segment(b"path", b"main")
961 .segment(b"lang", b"ts")
962 .segment(b"module", b"util")
963 .segment(b"class", b"Bar")
964 .build();
965 let targets: Vec<_> = g
966 .refs()
967 .filter(|r| r.kind == b"uses_type")
968 .map(|r| r.target.clone())
969 .collect();
970 assert!(
971 targets.contains(&foo),
972 "missing Foo uses_type; got {targets:?}"
973 );
974 assert!(targets.contains(&bar));
975 }
976
977 #[test]
978 fn extract_class_field_type_annotation_emits_uses_type_sourced_from_field() {
979 let g = extract(
980 "util.ts",
981 "class Bar { private x: Foo; }",
982 &make_anchor(),
983 false,
984 );
985 let field = MonikerBuilder::new()
986 .project(b"my-app")
987 .segment(b"path", b"main")
988 .segment(b"lang", b"ts")
989 .segment(b"module", b"util")
990 .segment(b"class", b"Bar")
991 .segment(b"field", b"x")
992 .build();
993 let foo = MonikerBuilder::new()
994 .project(b"my-app")
995 .segment(b"path", b"main")
996 .segment(b"lang", b"ts")
997 .segment(b"module", b"util")
998 .segment(b"class", b"Foo")
999 .build();
1000 let r = g
1001 .refs()
1002 .find(|r| r.kind == b"uses_type" && r.target == foo)
1003 .expect("missing uses_type Foo from field");
1004 assert_eq!(
1005 g.def_at(r.source).moniker,
1006 field,
1007 "field type ref must be sourced from the field moniker, not the class scope"
1008 );
1009 }
1010
1011 #[test]
1012 fn extract_return_identifier_emits_reads() {
1013 let g = extract(
1014 "util.ts",
1015 "function f() { return x; }",
1016 &make_anchor(),
1017 false,
1018 );
1019 let r = g.refs().find(|r| r.kind == b"reads").expect("reads ref");
1020 let target = MonikerBuilder::new()
1021 .project(b"my-app")
1022 .segment(b"path", b"main")
1023 .segment(b"lang", b"ts")
1024 .segment(b"module", b"util")
1025 .segment(b"function", b"x")
1026 .build();
1027 assert_eq!(r.target, target);
1028 }
1029 #[test]
1030 fn extract_di_register_fires_only_when_callee_in_preset() {
1031 let presets = Presets {
1032 di_register_callees: vec!["register".into(), "bind".into()],
1033 };
1034 let g = super::extract(
1035 "util.ts",
1036 "register(UserService);",
1037 &make_anchor(),
1038 false,
1039 &presets,
1040 );
1041 assert!(g.refs().any(|r| r.kind == b"di_register"));
1042 }
1043
1044 #[test]
1045 fn extract_di_register_silent_without_preset() {
1046 let g = extract("util.ts", "register(UserService);", &make_anchor(), false);
1047 assert!(
1048 g.refs().all(|r| r.kind != b"di_register"),
1049 "di_register must stay silent without a preset",
1050 );
1051 }
1052
1053 #[test]
1054 fn extract_di_register_skips_non_matching_callee() {
1055 let presets = Presets {
1056 di_register_callees: vec!["register".into()],
1057 };
1058 let g = super::extract("util.ts", "expect(value);", &make_anchor(), false, &presets);
1059 assert!(g.refs().all(|r| r.kind != b"di_register"));
1060 }
1061
1062 #[test]
1063 fn extract_di_register_register_with_name_and_factory() {
1064 let presets = Presets {
1065 di_register_callees: vec!["register".into()],
1066 };
1067 let g = super::extract(
1068 "util.ts",
1069 "register('repoStore', makeRepoStore);",
1070 &make_anchor(),
1071 false,
1072 &presets,
1073 );
1074 assert!(
1075 g.refs().any(|r| r.kind == b"di_register"),
1076 "register('name', factory) must emit di_register on the factory identifier",
1077 );
1078 }
1079
1080 #[test]
1081 fn extract_di_register_member_callee_register() {
1082 let presets = Presets {
1083 di_register_callees: vec!["register".into()],
1084 };
1085 let g = super::extract(
1086 "util.ts",
1087 "container.register('repoStore', makeRepoStore);",
1088 &make_anchor(),
1089 false,
1090 &presets,
1091 );
1092 assert!(
1093 g.refs().any(|r| r.kind == b"di_register"),
1094 "container.register(...) must emit di_register when 'register' is in the preset",
1095 );
1096 }
1097
1098 #[test]
1099 fn extract_di_register_recurses_into_factory_call_argument() {
1100 let presets = Presets {
1101 di_register_callees: vec!["register".into()],
1102 };
1103 let g = super::extract(
1104 "util.ts",
1105 "register('repoStore', asFunction(makeRepoStore));",
1106 &make_anchor(),
1107 false,
1108 &presets,
1109 );
1110 assert!(
1111 g.refs().any(|r| r.kind == b"di_register"),
1112 "register('name', asFunction(make)) must recurse to find 'make'",
1113 );
1114 }
1115
1116 #[test]
1117 fn extract_di_register_recurses_through_chained_call_postfix() {
1118 let presets = Presets {
1119 di_register_callees: vec!["asFunction".into()],
1120 };
1121 let g = super::extract(
1122 "util.ts",
1123 "asFunction(makeRepoStore).singleton();",
1124 &make_anchor(),
1125 false,
1126 &presets,
1127 );
1128 assert!(
1129 g.refs().any(|r| r.kind == b"di_register"),
1130 "asFunction(make).singleton() chain must still register the inner 'make'",
1131 );
1132 }
1133
1134 #[test]
1135 fn extract_di_register_full_awilix_pattern() {
1136 let presets = Presets {
1137 di_register_callees: vec!["register".into()],
1138 };
1139 let g = super::extract(
1140 "util.ts",
1141 "container.register('readResource', asFunction(makeReadResource).singleton());",
1142 &make_anchor(),
1143 false,
1144 &presets,
1145 );
1146 assert!(
1147 g.refs().any(|r| r.kind == b"di_register"),
1148 "container.register('name', asFunction(make).singleton()) must emit di_register",
1149 );
1150 }
1151 #[test]
1152 fn extract_comment_emits_comment_def() {
1153 let g = extract("util.ts", "// hello\nclass Foo {}", &make_anchor(), false);
1154 let comments: Vec<_> = g.defs().filter(|d| d.kind == b"comment").collect();
1155 assert_eq!(comments.len(), 1);
1156 assert_eq!(comments[0].position, Some((0, 8)));
1157 }
1158
1159 #[test]
1160 fn extract_emits_one_comment_def_per_comment_node() {
1161 let g = extract(
1162 "util.ts",
1163 "// a\n// b\nclass Foo { /* c */ }",
1164 &make_anchor(),
1165 false,
1166 );
1167 let comments: Vec<_> = g.defs().filter(|d| d.kind == b"comment").collect();
1168 assert_eq!(comments.len(), 3);
1169 }
1170 #[test]
1171 fn extract_export_default_class_named_default() {
1172 let g = extract("util.ts", "export default class {}", &make_anchor(), false);
1173 let m = MonikerBuilder::new()
1174 .project(b"my-app")
1175 .segment(b"path", b"main")
1176 .segment(b"lang", b"ts")
1177 .segment(b"module", b"util")
1178 .segment(b"class", b"default")
1179 .build();
1180 assert!(g.contains(&m));
1181 }
1182 #[test]
1183 fn extract_shallow_skips_param_and_local() {
1184 let g = extract(
1185 "util.ts",
1186 "function f(a: number) { let x = 1; }",
1187 &make_anchor(),
1188 false,
1189 );
1190 assert!(
1191 g.defs().all(|d| d.kind != b"param" && d.kind != b"local"),
1192 "shallow extraction must not produce param/local defs"
1193 );
1194 }
1195
1196 #[test]
1197 fn extract_deep_emits_params_and_locals() {
1198 let g = extract(
1199 "util.ts",
1200 "function f(a: number, b: number) { let sum = a + b; }",
1201 &make_anchor(),
1202 true,
1203 );
1204 let pa = MonikerBuilder::new()
1205 .project(b"my-app")
1206 .segment(b"path", b"main")
1207 .segment(b"lang", b"ts")
1208 .segment(b"module", b"util")
1209 .segment(b"function", b"f(a:number,b:number)")
1210 .segment(b"param", b"a")
1211 .build();
1212 let pb = MonikerBuilder::new()
1213 .project(b"my-app")
1214 .segment(b"path", b"main")
1215 .segment(b"lang", b"ts")
1216 .segment(b"module", b"util")
1217 .segment(b"function", b"f(a:number,b:number)")
1218 .segment(b"param", b"b")
1219 .build();
1220 let sum = MonikerBuilder::new()
1221 .project(b"my-app")
1222 .segment(b"path", b"main")
1223 .segment(b"lang", b"ts")
1224 .segment(b"module", b"util")
1225 .segment(b"function", b"f(a:number,b:number)")
1226 .segment(b"local", b"sum")
1227 .build();
1228 assert!(
1229 g.contains(&pa),
1230 "missing param a; defs: {:?}",
1231 g.def_monikers()
1232 );
1233 assert!(g.contains(&pb));
1234 assert!(g.contains(&sum));
1235 }
1236
1237 #[test]
1238 fn extract_deep_anonymous_callback_uses_position_name() {
1239 let g = extract(
1240 "util.ts",
1241 "function f() { [1].map(x => x); }",
1242 &make_anchor(),
1243 true,
1244 );
1245 let monikers = g.def_monikers();
1246 let cb = monikers
1247 .iter()
1248 .find(|m| {
1249 let last = m.as_view().segments().last().unwrap();
1250 last.kind == b"function" && last.name.starts_with(b"__cb_")
1251 })
1252 .expect("anonymous callback def with __cb_ prefix")
1253 .clone();
1254 let view = cb.as_view();
1255 let last = view.segments().last().unwrap();
1256 assert_eq!(last.kind, b"function");
1257 assert!(g.defs().any(|d| {
1258 let dv = d.moniker.as_view();
1259 dv.segment_count() == view.segment_count() + 1
1260 && dv.segments().last().unwrap().kind == b"param"
1261 }));
1262 }
1263
1264 #[test]
1265 fn extract_position_covers_definition_node() {
1266 let g = extract("util.ts", "class Foo {}", &make_anchor(), false);
1267 let foo = g.defs().find(|d| d.kind == b"class").unwrap();
1268 let (s, e) = foo.position.unwrap();
1269 assert!(e > s);
1270 }
1271}