use std::collections::{HashMap, HashSet};
use tree_sitter::Node;
use super::text::{named_children, text, truncate};
use super::ts_file_scope::{file_scope_decls, local_defs, require_spec, ts_decl_kind, TsDecl};
use super::ts_fragment_forms;
use super::ts_fragment_types::{
TsBinding, TsDynamicImport, TsFragment, TsFragmentDef, TsImport, TsReexport, TsReexportName,
TsRef,
};
use super::ts_purpose::{
common_js_export_target, compose_hybrid_ts_purpose, is_default_export_statement,
ts_declared_name, ts_pattern_names, ts_purpose_segments, CjsTarget,
};
const TS_DISPATCH_CALLEES: &[&str] = &["dispatch", "ofType"];
const TS_REFERENCE_NODES: &[&str] = &[
"call_expression",
"new_expression",
"jsx_opening_element",
"jsx_self_closing_element",
];
fn ts_string_literal(node: Option<Node>, src: &[u8]) -> Option<String> {
let node = node?;
if node.kind() != "string" {
return None;
}
Some(
match named_children(node)
.into_iter()
.find(|c| c.kind() == "string_fragment")
{
Some(frag) => text(frag, src),
None => String::new(),
},
)
}
fn ts_line(node: Node) -> usize {
node.start_position().row + 1
}
fn is_component_tag_name(name: &str) -> bool {
matches!(name.chars().next(), Some(c) if c.is_ascii_uppercase())
}
fn ts_import_bindings(stmt: Node, src: &[u8]) -> Vec<TsBinding> {
let Some(clause) = named_children(stmt)
.into_iter()
.find(|c| c.kind() == "import_clause")
else {
return Vec::new();
};
let mut out = Vec::new();
for c in named_children(clause) {
match c.kind() {
"identifier" => out.push(TsBinding {
local: text(c, src),
imported: "default".to_string(),
}),
"namespace_import" => {
if let Some(id) = named_children(c)
.into_iter()
.find(|n| n.kind() == "identifier")
{
out.push(TsBinding {
local: text(id, src),
imported: "*".to_string(),
});
}
}
"named_imports" => {
for spec in named_children(c) {
if spec.kind() != "import_specifier" {
continue;
}
let Some(name) = spec.child_by_field_name("name") else {
continue;
};
let alias = spec.child_by_field_name("alias");
out.push(TsBinding {
local: text(alias.unwrap_or(name), src),
imported: text(name, src),
});
}
}
_ => {}
}
}
out
}
fn ts_reexport_entry(stmt: Node, src: &[u8]) -> Option<TsReexport> {
let spec = ts_string_literal(stmt.child_by_field_name("source"), src)?;
let line = ts_line(stmt);
if let Some(namespace) = ts_fragment_forms::namespace_export(stmt, src) {
return Some(TsReexport {
spec,
line,
star: false,
names: Vec::new(),
namespace: Some(namespace),
});
}
let Some(clause) = named_children(stmt)
.into_iter()
.find(|c| c.kind() == "export_clause")
else {
return Some(TsReexport {
spec,
line,
star: true,
names: Vec::new(),
namespace: None,
});
};
let mut names = Vec::new();
for s in named_children(clause) {
if s.kind() != "export_specifier" {
continue;
}
let Some(name) = s.child_by_field_name("name") else {
continue;
};
let alias = s.child_by_field_name("alias");
names.push(TsReexportName {
exported: text(alias.unwrap_or(name), src),
imported: text(name, src),
});
}
Some(TsReexport {
spec,
line,
star: false,
names,
namespace: None,
})
}
fn ts_default_export_name(program_node: Node, src: &[u8]) -> Option<String> {
for stmt in named_children(program_node) {
if stmt.kind() != "export_statement" {
continue;
}
if stmt.child_by_field_name("source").is_none() {
if let Some(name) = ts_fragment_forms::default_export_clause(stmt, src) {
return Some(name);
}
}
if !is_default_export_statement(stmt) {
continue;
}
if let Some(decl) = stmt.child_by_field_name("declaration") {
let name = ts_declared_name(decl, src);
if !name.is_empty() {
return Some(name);
}
continue;
}
if let Some(value) = stmt.child_by_field_name("value") {
if value.kind() == "identifier" {
return Some(text(value, src));
}
}
}
None
}
#[allow(
clippy::cognitive_complexity,
reason = "one flat dispatch over every export shape a top-level declaration can take; the shapes only make sense enumerated together"
)]
fn ts_exported_names(
program_node: Node,
decls: &HashMap<String, TsDecl>,
src: &[u8],
) -> Vec<String> {
let mut names: Vec<String> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
macro_rules! add {
($name:expr) => {{
let name: String = $name;
if !name.is_empty() && decls.contains_key(&name) && seen.insert(name.clone()) {
names.push(name);
}
}};
}
let export_stmts: Vec<Node> = named_children(program_node)
.into_iter()
.filter(|c| c.kind() == "export_statement")
.collect();
if !export_stmts.is_empty() {
for stmt in export_stmts {
if stmt.child_by_field_name("source").is_some() {
continue; }
if let Some(decl) = stmt.child_by_field_name("declaration") {
if ts_decl_kind(decl) == Some("const") {
for d in named_children(decl) {
if d.kind() != "variable_declarator" {
continue;
}
for n in ts_pattern_names(d.child_by_field_name("name"), src) {
add!(n);
}
}
} else {
add!(ts_declared_name(decl, src));
}
continue;
}
if let Some(clause) = named_children(stmt)
.into_iter()
.find(|c| c.kind() == "export_clause")
{
for s in named_children(clause) {
if s.kind() != "export_specifier" {
continue;
}
if let Some(name) = s.child_by_field_name("name") {
add!(text(name, src));
}
}
}
}
return names;
}
for stmt in named_children(program_node) {
if stmt.kind() != "expression_statement" {
continue;
}
let Some(assign) = named_children(stmt)
.into_iter()
.find(|n| n.kind() == "assignment_expression")
else {
continue;
};
let Some(target) = common_js_export_target(assign.child_by_field_name("left"), src) else {
continue;
};
let right = assign.child_by_field_name("right");
match target {
CjsTarget::Prop(prop) => {
add!(prop);
}
CjsTarget::Whole => match right {
Some(r) if r.kind() == "object" => {
for c in named_children(r) {
if c.kind() == "shorthand_property_identifier" {
add!(text(c, src));
} else if c.kind() == "pair" {
if let Some(value) = c.child_by_field_name("value") {
if value.kind() == "identifier" {
add!(text(value, src));
}
}
}
}
}
Some(r) if r.kind() == "identifier" => {
add!(text(r, src));
}
_ => {}
},
}
}
names
}
fn ts_require_import(decl_node: Node, src: &[u8]) -> Vec<TsImport> {
let mut out = Vec::new();
for d in named_children(decl_node) {
if d.kind() != "variable_declarator" {
continue;
}
let Some(spec) = require_spec(d, src) else {
continue;
};
let name_node = d.child_by_field_name("name");
let bindings = match name_node {
Some(n) if n.kind() == "identifier" => {
vec![TsBinding {
local: text(n, src),
imported: "*".to_string(),
}]
}
_ => ts_pattern_names(name_node, src)
.into_iter()
.map(|n| TsBinding {
local: n.clone(),
imported: n,
})
.collect(),
};
out.push(TsImport {
spec,
line: ts_line(d),
bindings,
require: true,
});
}
out
}
#[allow(
clippy::too_many_lines,
clippy::cognitive_complexity,
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"
)]
fn ts_record_ref(
node: Node,
src: &[u8],
refs: &mut Vec<TsRef>,
consumed: &mut HashSet<usize>,
known: &HashSet<String>,
member_known: &HashSet<String>,
) {
match node.kind() {
"call_expression" => {
let fnode = node.child_by_field_name("function");
let callee = match fnode {
Some(f) if f.kind() == "identifier" => Some(text(f, src)),
Some(f) if f.kind() == "member_expression" => {
f.child_by_field_name("property").map(|p| text(p, src))
}
_ => None,
};
let dispatching = callee
.as_deref()
.is_some_and(|c| TS_DISPATCH_CALLEES.contains(&c));
if dispatching {
let args = node
.child_by_field_name("arguments")
.map(named_children)
.unwrap_or_default();
for arg in args {
if arg.kind() == "identifier" {
let name = text(arg, src);
if known.contains(&name) {
refs.push(TsRef {
kind: "dispatch".to_string(),
name,
member: None,
line: ts_line(arg),
});
}
consumed.insert(arg.id());
} else if arg.kind() == "call_expression" {
if let Some(inner) = arg.child_by_field_name("function") {
if inner.kind() == "identifier" {
let name = text(inner, src);
if known.contains(&name) {
refs.push(TsRef {
kind: "dispatch".to_string(),
name,
member: None,
line: ts_line(inner),
});
}
consumed.insert(arg.id());
}
}
}
}
} else if !consumed.contains(&node.id()) {
match fnode {
Some(f) if f.kind() == "identifier" => {
let name = text(f, src);
if known.contains(&name) {
refs.push(TsRef {
kind: "call".to_string(),
name,
member: None,
line: ts_line(f),
});
}
}
Some(f) if f.kind() == "member_expression" => {
let obj = f.child_by_field_name("object");
let prop = f.child_by_field_name("property");
if let (Some(obj), Some(prop)) = (obj, prop) {
let name = text(obj, src);
if obj.kind() == "identifier" && member_known.contains(&name) {
refs.push(TsRef {
kind: "call".to_string(),
name,
member: Some(text(prop, src)),
line: ts_line(obj),
});
}
}
}
_ => {}
}
}
}
"new_expression" => {
if let Some(ctor) = node.child_by_field_name("constructor") {
let name = text(ctor, src);
if ctor.kind() == "identifier" && known.contains(&name) {
refs.push(TsRef {
kind: "call".to_string(),
name,
member: None,
line: ts_line(ctor),
});
}
}
}
"jsx_opening_element" | "jsx_self_closing_element" => {
let name_node = node.child_by_field_name("name");
match name_node {
Some(n) if n.kind() == "identifier" => {
let name = text(n, src);
if is_component_tag_name(&name) && known.contains(&name) {
refs.push(TsRef {
kind: "jsx-use".to_string(),
name,
member: None,
line: ts_line(n),
});
}
}
Some(n) if n.kind() == "member_expression" || n.kind() == "nested_identifier" => {
let obj = n.child_by_field_name("object");
let prop = n.child_by_field_name("property");
if let (Some(obj), Some(prop)) = (obj, prop) {
let name = text(obj, src);
if obj.kind() == "identifier"
&& is_component_tag_name(&name)
&& member_known.contains(&name)
{
refs.push(TsRef {
kind: "jsx-use".to_string(),
name,
member: Some(text(prop, src)),
line: ts_line(obj),
});
}
}
}
_ => {}
}
}
_ => {}
}
}
fn ts_ref_walk(
program_node: Node,
src: &[u8],
refs: &mut Vec<TsRef>,
known: &HashSet<String>,
member_known: &HashSet<String>,
dynamic_imports: &mut Vec<TsDynamicImport>,
non_literal_dynamic_imports: &mut usize,
) {
let mut consumed: HashSet<usize> = HashSet::new();
let mut cursor = program_node.walk();
loop {
if TS_REFERENCE_NODES.contains(&cursor.node().kind()) {
ts_record_ref(cursor.node(), src, refs, &mut consumed, known, member_known);
}
if cursor.node().kind() == "call_expression" {
match ts_fragment_forms::dynamic_import(cursor.node(), src) {
Some(Some(import)) => dynamic_imports.push(import),
Some(None) => *non_literal_dynamic_imports += 1,
None => {}
}
}
if cursor.goto_first_child() {
continue;
}
let mut advanced = false;
while cursor.depth() > 0 {
if cursor.goto_next_sibling() {
advanced = true;
break;
}
if !cursor.goto_parent() {
break;
}
}
if !advanced && !cursor.goto_next_sibling() {
break;
}
}
}
pub fn extract_ts_fragment(program_node: Node, src: &[u8]) -> TsFragment {
let (decls, inside) = file_scope_decls(program_node, src);
let mut imports: Vec<TsImport> = Vec::new();
let mut reexports: Vec<TsReexport> = Vec::new();
let mut export_equals = None;
for stmt in named_children(program_node) {
if stmt.kind() == "import_statement" {
let Some(spec) = ts_string_literal(stmt.child_by_field_name("source"), src) else {
if let Some(import) = ts_fragment_forms::import_equals(stmt, src) {
imports.push(import);
}
continue;
};
imports.push(TsImport {
spec,
line: ts_line(stmt),
bindings: ts_import_bindings(stmt, src),
require: false,
});
} else if stmt.kind() == "export_statement" && stmt.child_by_field_name("source").is_some()
{
if let Some(entry) = ts_reexport_entry(stmt, src) {
reexports.push(entry);
}
} else if stmt.kind() == "lexical_declaration" || stmt.kind() == "variable_declaration" {
imports.extend(ts_require_import(stmt, src));
}
if export_equals.is_none() {
export_equals = ts_fragment_forms::export_equals(stmt, src);
}
}
let mut exported = ts_exported_names(program_node, &decls.by_name, src);
if let Some(name) = export_equals
.as_ref()
.filter(|name| decls.by_name.contains_key(*name))
{
if !exported.contains(name) {
exported.push(name.clone());
}
}
let mut defs: Vec<TsFragmentDef> = exported
.iter()
.cloned()
.map(|name| {
let d = &decls.by_name[&name];
TsFragmentDef {
name,
kind: d.kind.to_string(),
line: d.line,
end_line: d.end_line,
local: false,
}
})
.collect();
let mut member_known: HashSet<String> = defs.iter().map(|d| d.name.clone()).collect();
for imp in &imports {
for b in &imp.bindings {
member_known.insert(b.local.clone());
}
}
defs.extend(local_defs(&decls, &inside, &exported, &member_known));
let known: HashSet<String> = defs
.iter()
.map(|d| d.name.clone())
.chain(member_known.iter().cloned())
.collect();
let mut refs: Vec<TsRef> = Vec::new();
let mut dynamic_imports = Vec::new();
let mut non_literal_dynamic_imports = 0;
ts_ref_walk(
program_node,
src,
&mut refs,
&known,
&member_known,
&mut dynamic_imports,
&mut non_literal_dynamic_imports,
);
let default =
ts_default_export_name(program_node, src).filter(|d| decls.by_name.contains_key(d));
TsFragment {
ts: 1,
defs,
imports,
reexports,
refs,
default,
export_equals: export_equals.filter(|name| decls.by_name.contains_key(name)),
dynamic_imports,
non_literal_dynamic_imports,
components: super::ts_component::components(program_node, src),
}
}
pub struct TsFileExtraction {
pub purpose: Option<String>,
pub fragment: TsFragment,
}
pub fn extract_ts_file(source: &str, grammar: crate::parse::TsGrammar) -> Option<TsFileExtraction> {
let units = crate::parse::utf16_units(source);
let tree = crate::parse::parse_ts_js(&units, grammar)?;
let src = crate::parse::utf16_bytes(&units);
let root_node = tree.root_node();
let purpose = ts_purpose_segments(root_node, &src).map(|raw| {
let detail = crate::walk::default_purpose_detailed_in(source);
if detail.is_comment {
compose_hybrid_ts_purpose(&detail.text, &raw)
} else {
truncate(&raw)
}
});
Some(TsFileExtraction {
purpose,
fragment: extract_ts_fragment(root_node, &src),
})
}