1use std::collections::{HashMap, HashSet};
2use std::path::Path;
3
4use tree_sitter::Node;
5
6use super::text::{named_children, text, truncate};
7use super::ts_fragment_types::{
8 TsBinding, TsFragment, TsFragmentDef, TsImport, TsReexport, TsReexportName, TsRef,
9};
10use super::ts_purpose::{
11 common_js_export_target, compose_hybrid_ts_purpose, is_default_export_statement,
12 ts_declared_name, ts_pattern_names, ts_purpose_segments, CjsTarget,
13};
14
15const TS_DISPATCH_CALLEES: &[&str] = &["dispatch", "ofType"];
22
23const TS_REFERENCE_NODES: &[&str] = &[
26 "call_expression",
27 "new_expression",
28 "jsx_opening_element",
29 "jsx_self_closing_element",
30];
31
32fn ts_string_literal(node: Option<Node>, src: &[u8]) -> Option<String> {
35 let node = node?;
36 if node.kind() != "string" {
37 return None;
38 }
39 Some(
40 match named_children(node)
41 .into_iter()
42 .find(|c| c.kind() == "string_fragment")
43 {
44 Some(frag) => text(frag, src),
45 None => String::new(),
46 },
47 )
48}
49
50fn ts_line(node: Node) -> usize {
51 node.start_position().row + 1
52}
53
54fn is_component_tag_name(name: &str) -> bool {
59 matches!(name.chars().next(), Some(c) if c.is_ascii_uppercase())
60}
61
62fn ts_import_bindings(stmt: Node, src: &[u8]) -> Vec<TsBinding> {
63 let Some(clause) = named_children(stmt)
64 .into_iter()
65 .find(|c| c.kind() == "import_clause")
66 else {
67 return Vec::new();
68 };
69 let mut out = Vec::new();
70 for c in named_children(clause) {
71 match c.kind() {
72 "identifier" => out.push(TsBinding {
73 local: text(c, src),
74 imported: "default".to_string(),
75 }),
76 "namespace_import" => {
77 if let Some(id) = named_children(c)
78 .into_iter()
79 .find(|n| n.kind() == "identifier")
80 {
81 out.push(TsBinding {
82 local: text(id, src),
83 imported: "*".to_string(),
84 });
85 }
86 }
87 "named_imports" => {
88 for spec in named_children(c) {
89 if spec.kind() != "import_specifier" {
90 continue;
91 }
92 let Some(name) = spec.child_by_field_name("name") else {
93 continue;
94 };
95 let alias = spec.child_by_field_name("alias");
96 out.push(TsBinding {
97 local: text(alias.unwrap_or(name), src),
98 imported: text(name, src),
99 });
100 }
101 }
102 _ => {}
103 }
104 }
105 out
106}
107
108fn ts_reexport_entry(stmt: Node, src: &[u8]) -> Option<TsReexport> {
114 let spec = ts_string_literal(stmt.child_by_field_name("source"), src)?;
115 let line = ts_line(stmt);
116 if named_children(stmt)
117 .iter()
118 .any(|c| c.kind() == "namespace_export")
119 {
120 return Some(TsReexport {
121 spec,
122 line,
123 star: false,
124 names: Vec::new(),
125 });
126 }
127 let Some(clause) = named_children(stmt)
128 .into_iter()
129 .find(|c| c.kind() == "export_clause")
130 else {
131 return Some(TsReexport {
132 spec,
133 line,
134 star: true,
135 names: Vec::new(),
136 });
137 };
138 let mut names = Vec::new();
139 for s in named_children(clause) {
140 if s.kind() != "export_specifier" {
141 continue;
142 }
143 let Some(name) = s.child_by_field_name("name") else {
144 continue;
145 };
146 let alias = s.child_by_field_name("alias");
147 names.push(TsReexportName {
148 exported: text(alias.unwrap_or(name), src),
149 imported: text(name, src),
150 });
151 }
152 Some(TsReexport {
153 spec,
154 line,
155 star: false,
156 names,
157 })
158}
159
160fn ts_decl_kind(node: Node) -> Option<&'static str> {
165 match node.kind() {
166 "class_declaration" | "abstract_class_declaration" => Some("class"),
167 "function_declaration" | "generator_function_declaration" => Some("function"),
168 "interface_declaration" => Some("interface"),
169 "type_alias_declaration" => Some("type"),
170 "enum_declaration" => Some("enum"),
171 "lexical_declaration" | "variable_declaration" => Some("const"),
172 _ => None,
173 }
174}
175
176#[derive(Debug, Clone)]
177struct TsDecl {
178 kind: &'static str,
179 line: usize,
180 end_line: usize,
181}
182
183fn ts_top_level_decls(program_node: Node, src: &[u8]) -> HashMap<String, TsDecl> {
187 let mut decls: HashMap<String, TsDecl> = HashMap::new();
188 fn add(
189 decls: &mut HashMap<String, TsDecl>,
190 name: String,
191 kind: &'static str,
192 line: usize,
193 end_line: usize,
194 ) {
195 if name.is_empty() {
196 return;
197 }
198 decls.entry(name).or_insert(TsDecl {
199 kind,
200 line,
201 end_line,
202 });
203 }
204 fn from_declaration(node: Node, src: &[u8], decls: &mut HashMap<String, TsDecl>) {
205 let Some(kind) = ts_decl_kind(node) else {
206 return;
207 };
208 if kind == "const" {
209 for d in named_children(node) {
210 if d.kind() != "variable_declarator" {
211 continue;
212 }
213 let name_node = d.child_by_field_name("name");
214 let line = ts_line(name_node.unwrap_or(d));
215 for n in ts_pattern_names(name_node, src) {
216 add(decls, n, "const", line, d.end_position().row + 1);
217 }
218 }
219 return;
220 }
221 add(
222 decls,
223 ts_declared_name(node, src),
224 kind,
225 ts_line(node),
226 node.end_position().row + 1,
227 );
228 }
229 for c in named_children(program_node) {
230 if c.kind() == "export_statement" {
231 if let Some(decl) = c.child_by_field_name("declaration") {
232 from_declaration(decl, src, &mut decls);
233 }
234 continue;
235 }
236 from_declaration(c, src, &mut decls);
237 }
238 decls
239}
240
241fn ts_default_export_name(program_node: Node, src: &[u8]) -> Option<String> {
242 for stmt in named_children(program_node) {
243 if stmt.kind() != "export_statement" || !is_default_export_statement(stmt) {
244 continue;
245 }
246 if let Some(decl) = stmt.child_by_field_name("declaration") {
247 let name = ts_declared_name(decl, src);
248 if !name.is_empty() {
249 return Some(name);
250 }
251 continue;
252 }
253 if let Some(value) = stmt.child_by_field_name("value") {
254 if value.kind() == "identifier" {
255 return Some(text(value, src));
256 }
257 }
258 }
259 None
260}
261
262#[allow(
267 clippy::cognitive_complexity,
268 reason = "one flat dispatch over every export shape a top-level declaration can take; the shapes only make sense enumerated together"
269)]
270fn ts_exported_names(
271 program_node: Node,
272 decls: &HashMap<String, TsDecl>,
273 src: &[u8],
274) -> Vec<String> {
275 let mut names: Vec<String> = Vec::new();
276 let mut seen: HashSet<String> = HashSet::new();
277 macro_rules! add {
278 ($name:expr) => {{
279 let name: String = $name;
280 if !name.is_empty() && decls.contains_key(&name) && seen.insert(name.clone()) {
281 names.push(name);
282 }
283 }};
284 }
285 let export_stmts: Vec<Node> = named_children(program_node)
286 .into_iter()
287 .filter(|c| c.kind() == "export_statement")
288 .collect();
289 if !export_stmts.is_empty() {
290 for stmt in export_stmts {
291 if stmt.child_by_field_name("source").is_some() {
292 continue; }
294 if let Some(decl) = stmt.child_by_field_name("declaration") {
295 if ts_decl_kind(decl) == Some("const") {
296 for d in named_children(decl) {
297 if d.kind() != "variable_declarator" {
298 continue;
299 }
300 for n in ts_pattern_names(d.child_by_field_name("name"), src) {
301 add!(n);
302 }
303 }
304 } else {
305 add!(ts_declared_name(decl, src));
306 }
307 continue;
308 }
309 if let Some(clause) = named_children(stmt)
310 .into_iter()
311 .find(|c| c.kind() == "export_clause")
312 {
313 for s in named_children(clause) {
314 if s.kind() != "export_specifier" {
315 continue;
316 }
317 if let Some(name) = s.child_by_field_name("name") {
322 add!(text(name, src));
323 }
324 }
325 }
326 }
327 return names;
328 }
329 for stmt in named_children(program_node) {
331 if stmt.kind() != "expression_statement" {
332 continue;
333 }
334 let Some(assign) = named_children(stmt)
335 .into_iter()
336 .find(|n| n.kind() == "assignment_expression")
337 else {
338 continue;
339 };
340 let Some(target) = common_js_export_target(assign.child_by_field_name("left"), src) else {
341 continue;
342 };
343 let right = assign.child_by_field_name("right");
344 match target {
345 CjsTarget::Prop(prop) => {
346 add!(prop);
347 }
348 CjsTarget::Whole => match right {
349 Some(r) if r.kind() == "object" => {
350 for c in named_children(r) {
351 if c.kind() == "shorthand_property_identifier" {
352 add!(text(c, src));
353 } else if c.kind() == "pair" {
354 if let Some(value) = c.child_by_field_name("value") {
355 if value.kind() == "identifier" {
356 add!(text(value, src));
357 }
358 }
359 }
360 }
361 }
362 Some(r) if r.kind() == "identifier" => {
363 add!(text(r, src));
364 }
365 _ => {}
366 },
367 }
368 }
369 names
370}
371
372fn ts_require_import(decl_node: Node, src: &[u8]) -> Vec<TsImport> {
377 let mut out = Vec::new();
378 for d in named_children(decl_node) {
379 if d.kind() != "variable_declarator" {
380 continue;
381 }
382 let Some(value) = d.child_by_field_name("value") else {
383 continue;
384 };
385 if value.kind() != "call_expression" {
386 continue;
387 }
388 let Some(fnode) = value.child_by_field_name("function") else {
389 continue;
390 };
391 if fnode.kind() != "identifier" || text(fnode, src) != "require" {
392 continue;
393 }
394 let args = value.child_by_field_name("arguments");
395 let first = args.and_then(|a| named_children(a).into_iter().next());
396 let Some(spec) = ts_string_literal(first, src) else {
397 continue;
398 };
399 let name_node = d.child_by_field_name("name");
400 let bindings = match name_node {
401 Some(n) if n.kind() == "identifier" => {
402 vec![TsBinding {
403 local: text(n, src),
404 imported: "*".to_string(),
405 }]
406 }
407 _ => ts_pattern_names(name_node, src)
408 .into_iter()
409 .map(|n| TsBinding {
410 local: n.clone(),
411 imported: n,
412 })
413 .collect(),
414 };
415 out.push(TsImport {
416 spec,
417 line: ts_line(d),
418 bindings,
419 });
420 }
421 out
422}
423
424#[allow(
425 clippy::too_many_lines,
426 clippy::cognitive_complexity,
427 reason = "one flat dispatch over every reference shape a TS node can carry; splitting it would separate branches that share the same consumed-set bookkeeping"
428)]
429fn ts_record_ref(
430 node: Node,
431 src: &[u8],
432 refs: &mut Vec<TsRef>,
433 consumed: &mut HashSet<usize>,
434 known: &HashSet<String>,
435) {
436 match node.kind() {
437 "call_expression" => {
438 let fnode = node.child_by_field_name("function");
439 let callee = match fnode {
440 Some(f) if f.kind() == "identifier" => Some(text(f, src)),
441 Some(f) if f.kind() == "member_expression" => {
442 f.child_by_field_name("property").map(|p| text(p, src))
443 }
444 _ => None,
445 };
446 let dispatching = callee
447 .as_deref()
448 .is_some_and(|c| TS_DISPATCH_CALLEES.contains(&c));
449 if dispatching {
450 let args = node
456 .child_by_field_name("arguments")
457 .map(named_children)
458 .unwrap_or_default();
459 for arg in args {
460 if arg.kind() == "identifier" {
461 let name = text(arg, src);
462 if known.contains(&name) {
463 refs.push(TsRef {
464 kind: "dispatch".to_string(),
465 name,
466 member: None,
467 line: ts_line(arg),
468 });
469 }
470 consumed.insert(arg.id());
471 } else if arg.kind() == "call_expression" {
472 if let Some(inner) = arg.child_by_field_name("function") {
473 if inner.kind() == "identifier" {
474 let name = text(inner, src);
475 if known.contains(&name) {
476 refs.push(TsRef {
477 kind: "dispatch".to_string(),
478 name,
479 member: None,
480 line: ts_line(inner),
481 });
482 }
483 consumed.insert(arg.id());
484 }
485 }
486 }
487 }
488 } else if !consumed.contains(&node.id()) {
489 match fnode {
490 Some(f) if f.kind() == "identifier" => {
491 let name = text(f, src);
492 if known.contains(&name) {
493 refs.push(TsRef {
494 kind: "call".to_string(),
495 name,
496 member: None,
497 line: ts_line(f),
498 });
499 }
500 }
501 Some(f) if f.kind() == "member_expression" => {
502 let obj = f.child_by_field_name("object");
503 let prop = f.child_by_field_name("property");
504 if let (Some(obj), Some(prop)) = (obj, prop) {
509 let name = text(obj, src);
510 if obj.kind() == "identifier" && known.contains(&name) {
511 refs.push(TsRef {
512 kind: "call".to_string(),
513 name,
514 member: Some(text(prop, src)),
515 line: ts_line(obj),
516 });
517 }
518 }
519 }
520 _ => {}
521 }
522 }
523 }
524 "new_expression" => {
525 if let Some(ctor) = node.child_by_field_name("constructor") {
526 let name = text(ctor, src);
527 if ctor.kind() == "identifier" && known.contains(&name) {
528 refs.push(TsRef {
529 kind: "call".to_string(),
530 name,
531 member: None,
532 line: ts_line(ctor),
533 });
534 }
535 }
536 }
537 "jsx_opening_element" | "jsx_self_closing_element" => {
538 let name_node = node.child_by_field_name("name");
539 match name_node {
540 Some(n) if n.kind() == "identifier" => {
541 let name = text(n, src);
542 if is_component_tag_name(&name) && known.contains(&name) {
543 refs.push(TsRef {
544 kind: "jsx-use".to_string(),
545 name,
546 member: None,
547 line: ts_line(n),
548 });
549 }
550 }
551 Some(n) if n.kind() == "member_expression" || n.kind() == "nested_identifier" => {
552 let obj = n.child_by_field_name("object");
553 let prop = n.child_by_field_name("property");
554 if let (Some(obj), Some(prop)) = (obj, prop) {
555 let name = text(obj, src);
556 if obj.kind() == "identifier"
557 && is_component_tag_name(&name)
558 && known.contains(&name)
559 {
560 refs.push(TsRef {
561 kind: "jsx-use".to_string(),
562 name,
563 member: Some(text(prop, src)),
564 line: ts_line(obj),
565 });
566 }
567 }
568 }
569 _ => {}
570 }
571 }
572 _ => {}
573 }
574}
575
576fn ts_ref_walk(program_node: Node, src: &[u8], refs: &mut Vec<TsRef>, known: &HashSet<String>) {
581 let mut consumed: HashSet<usize> = HashSet::new();
582 let mut cursor = program_node.walk();
583 loop {
584 if TS_REFERENCE_NODES.contains(&cursor.node().kind()) {
585 ts_record_ref(cursor.node(), src, refs, &mut consumed, known);
586 }
587 if cursor.goto_first_child() {
588 continue;
589 }
590 let mut advanced = false;
591 while cursor.depth() > 0 {
592 if cursor.goto_next_sibling() {
593 advanced = true;
594 break;
595 }
596 if !cursor.goto_parent() {
597 break;
598 }
599 }
600 if !advanced && !cursor.goto_next_sibling() {
601 break;
602 }
603 }
604}
605
606pub fn extract_ts_fragment(program_node: Node, src: &[u8]) -> TsFragment {
609 let decls = ts_top_level_decls(program_node, src);
610 let mut imports: Vec<TsImport> = Vec::new();
611 let mut reexports: Vec<TsReexport> = Vec::new();
612 for stmt in named_children(program_node) {
613 if stmt.kind() == "import_statement" {
614 let Some(spec) = ts_string_literal(stmt.child_by_field_name("source"), src) else {
615 continue;
616 };
617 imports.push(TsImport {
618 spec,
619 line: ts_line(stmt),
620 bindings: ts_import_bindings(stmt, src),
621 });
622 } else if stmt.kind() == "export_statement" && stmt.child_by_field_name("source").is_some()
623 {
624 if let Some(entry) = ts_reexport_entry(stmt, src) {
625 reexports.push(entry);
626 }
627 } else if stmt.kind() == "lexical_declaration" || stmt.kind() == "variable_declaration" {
628 imports.extend(ts_require_import(stmt, src));
629 }
630 }
631 let defs: Vec<TsFragmentDef> = ts_exported_names(program_node, &decls, src)
632 .into_iter()
633 .map(|name| {
634 let d = &decls[&name];
635 TsFragmentDef {
636 name,
637 kind: d.kind.to_string(),
638 line: d.line,
639 end_line: d.end_line,
640 }
641 })
642 .collect();
643 let mut known: HashSet<String> = defs.iter().map(|d| d.name.clone()).collect();
649 for imp in &imports {
650 for b in &imp.bindings {
651 known.insert(b.local.clone());
652 }
653 }
654 let mut refs: Vec<TsRef> = Vec::new();
655 ts_ref_walk(program_node, src, &mut refs, &known);
656 let default = ts_default_export_name(program_node, src).filter(|d| decls.contains_key(d));
659 TsFragment {
660 ts: 1,
661 defs,
662 imports,
663 reexports,
664 refs,
665 default,
666 }
667}
668
669pub struct TsFileExtraction {
673 pub purpose: Option<String>,
675 pub fragment: TsFragment,
677}
678
679pub fn extract_ts_file(
683 root: &Path,
684 rel: &str,
685 source: &str,
686 grammar: crate::parse::TsGrammar,
687) -> Option<TsFileExtraction> {
688 let units = crate::parse::utf16_units(source);
689 let tree = crate::parse::parse_ts_js(&units, grammar)?;
690 let src = crate::parse::utf16_bytes(&units);
691 let root_node = tree.root_node();
692 let purpose = ts_purpose_segments(root_node, &src).map(|raw| {
693 let detail = crate::walk::default_purpose_detailed(root, rel);
694 if detail.is_comment {
695 compose_hybrid_ts_purpose(&detail.text, &raw)
696 } else {
697 truncate(&raw)
698 }
699 });
700 Some(TsFileExtraction {
701 purpose,
702 fragment: extract_ts_fragment(root_node, &src),
703 })
704}