Skip to main content

code_moniker_core/lang/ts/
mod.rs

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}