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