use std::collections::HashMap;
#[cfg(test)]
use std::cell::Cell;
use tree_sitter::Node;
use super::rs_attrs::{self, PathFact};
use super::rs_facts_types::{RsImpl, RsModPath};
use super::rs_fragment_types::{RsFileExtraction, RsFragment, RsItem};
use super::rs_paths::{join, path_segments, type_path_node};
use super::text::truncate;
use crate::parse;
pub fn extract_rs_file(source: &str) -> RsFileExtraction {
let units = parse::utf16_units(source);
let Some(tree) = parse::parse_rust(&units) else {
return RsFileExtraction {
purpose: None,
fragment: RsFragment::default(),
};
};
let src = parse::utf16_bytes(&units);
let root = tree.root_node();
let mut found = Vec::new();
let mut impls = Impls::default();
walk_items(root, &src, &mut found, &mut impls);
let purpose = compose_purpose(root, &src, &found);
let uses = super::rs_uses::collect_uses(root, &src);
let numbering = super::rs_sites::Numbering {
blocks: &uses.block_ids,
impls: &impls.ids,
};
let sites = super::rs_sites::collect_sites(root, &src, &numbering);
let mut fragment = assemble(found, impls.records);
fragment.uses = uses.uses;
fragment.blocks = uses.blocks;
fragment.sites = sites;
fragment.opaque_scopes = super::rs_scope::macro_scopes(root, &src);
RsFileExtraction { purpose, fragment }
}
fn assemble(found: Vec<Found>, mut impls: Vec<RsImpl>) -> RsFragment {
let marks: Vec<(Mark, Option<usize>, PathFact)> = found
.iter()
.map(|f| (f.mark, f.impl_index, f.mod_path.clone()))
.collect();
let items = dedupe(found);
let mut fragment = RsFragment {
items,
..RsFragment::default()
};
for (item, (mark, impl_index, mod_path)) in fragment.items.iter().zip(marks) {
if let Some(path) = mod_path {
fragment.mod_paths.push(RsModPath {
item: item.path.clone(),
path,
});
}
match mark {
Mark::ModFile => fragment.mod_files.push(item.path.clone()),
Mark::Ctor => fragment.ctors.push(item.path.clone()),
Mark::Test => fragment.tests.push(item.path.clone()),
Mark::None => {}
}
if let Some(i) = impl_index {
impls[i].items.push(item.path.clone());
}
}
fragment.impls = impls;
fragment
}
#[derive(Default)]
struct Impls {
records: Vec<RsImpl>,
ids: HashMap<usize, usize>,
}
struct Found {
name: String,
kind: &'static str,
path: String,
line: usize,
name_line: usize,
end_line: usize,
file_level: bool,
mark: Mark,
impl_index: Option<usize>,
mod_path: PathFact,
}
#[cfg(test)]
#[derive(Clone, Copy, Default)]
pub(super) struct ItemCost {
pub items: usize,
pub ancestry_work: usize,
pub parent_calls: usize,
pub path_bytes: usize,
}
#[cfg(test)]
thread_local! {
static ITEM_COST: Cell<ItemCost> = Cell::new(ItemCost::default());
}
#[cfg(test)]
pub(super) fn reset_item_cost() {
ITEM_COST.with(|cost| cost.set(ItemCost::default()));
}
#[cfg(test)]
pub(super) fn item_cost() -> ItemCost {
ITEM_COST.with(Cell::get)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Mark {
None,
ModFile,
Ctor,
Test,
}
fn record_kind(node_kind: &str) -> Option<&'static str> {
Some(match node_kind {
"mod_item" => "mod",
"struct_item" => "struct",
"enum_item" => "enum",
"union_item" => "union",
"trait_item" => "trait",
"function_item" | "function_signature_item" => "fn",
"const_item" => "const",
"static_item" => "static",
"type_item" | "associated_type" => "type",
"macro_definition" => "macro_rules",
_ => return None,
})
}
fn walk_items(root: Node, src: &[u8], out: &mut Vec<Found>, impls: &mut Impls) {
let mut pending: Vec<Pending> = Vec::new();
let top = Context {
segments: Vec::new(),
in_impl: None,
file_level: true,
under_path: false,
};
push_children(root, &top, &mut pending);
while let Some((node, ctx)) = pending.pop() {
let mut impl_index = None;
let mut under_path = ctx.under_path;
let segment = if let Some(kind) = record_kind(node.kind()) {
let recorded = record_item(node, kind, src, &ctx, out);
if kind == "mod" && node.child_by_field_name("body").is_some() {
under_path |= rs_attrs::path_fact(node, src).is_some();
}
match recorded {
Some(name) if kind == "mod" || kind == "trait" => Some(name),
_ => continue,
}
} else if node.kind() == "impl_item" {
impl_index = record_impl(node, src, &ctx.segments, &mut impls.records);
if let Some(index) = impl_index {
impls.ids.insert(node.id(), index);
}
Some(self_type_head(node.child_by_field_name("type"), src))
} else if node.kind() == "foreign_mod_item" {
None
} else {
continue;
};
if let Some(body) = node.child_by_field_name("body") {
let mut segments = ctx.segments;
segments.extend(segment);
let inner = Context {
segments,
in_impl: impl_index,
file_level: false,
under_path,
};
push_children(body, &inner, &mut pending);
}
}
}
#[derive(Clone)]
struct Context {
segments: Vec<String>,
in_impl: Option<usize>,
file_level: bool,
under_path: bool,
}
type Pending<'t> = (Node<'t>, Context);
fn push_children<'t>(body: Node<'t>, ctx: &Context, pending: &mut Vec<Pending<'t>>) {
let mut cursor = body.walk();
let children: Vec<Node> = body.named_children(&mut cursor).collect();
for child in children.into_iter().rev() {
pending.push((child, ctx.clone()));
}
}
fn record_impl(
node: Node,
src: &[u8],
segments: &[String],
impls: &mut Vec<RsImpl>,
) -> Option<usize> {
let ty = type_path_node(node.child_by_field_name("type")?)?;
let path = path_segments(ty, src)?;
impls.push(RsImpl {
scope: module_scope(segments),
self_type: join(&path),
items: Vec::new(),
});
Some(impls.len() - 1)
}
fn module_scope(segments: &[String]) -> String {
segments.join("::")
}
fn record_item(
node: Node,
kind: &'static str,
src: &[u8],
ctx: &Context,
out: &mut Vec<Found>,
) -> Option<String> {
let name_node = node.child_by_field_name("name")?;
let name = item_name(name_node, src)?;
let mark = mark_of(node, kind, src);
let mod_path = match mark {
Mark::ModFile if ctx.under_path => Some(None),
Mark::ModFile => rs_attrs::path_fact(node, src),
_ => None,
};
let mut path = ctx.segments.to_vec();
path.push(name.clone());
let path = path.join("::");
#[cfg(test)]
ITEM_COST.with(|cost| {
let mut value = cost.get();
value.items += 1;
value.path_bytes += path.len();
cost.set(value);
});
out.push(Found {
name: name.clone(),
kind,
path,
line: span_start_line(node),
name_line: name_node.start_position().row + 1,
end_line: end_line(node),
file_level: ctx.file_level,
mark,
impl_index: ctx.in_impl,
mod_path,
});
Some(name)
}
fn mark_of(node: Node, kind: &str, src: &[u8]) -> Mark {
match kind {
"mod" if node.child_by_field_name("body").is_none() => Mark::ModFile,
"struct" => match node.child_by_field_name("body") {
None => Mark::Ctor,
Some(body) if body.kind() == "ordered_field_declaration_list" => Mark::Ctor,
Some(_) => Mark::None,
},
"fn" if rs_attrs::is_test(node, src) => Mark::Test,
_ => Mark::None,
}
}
fn item_name(name_node: Node, src: &[u8]) -> Option<String> {
if name_node.is_missing() || name_node.kind() == "metavariable" {
return None;
}
let name = strip_raw(&parse::node_text(name_node, src));
(!name.is_empty() && name != "_").then_some(name)
}
fn strip_raw(text: &str) -> String {
text.strip_prefix("r#").unwrap_or(text).to_string()
}
fn self_type_head(node: Option<Node>, src: &[u8]) -> String {
let head = node.map(|n| type_head(n, src)).unwrap_or_default();
if head.is_empty() {
"_".to_string()
} else {
head
}
}
fn type_head(mut node: Node, src: &[u8]) -> String {
loop {
let field = match node.kind() {
"type_identifier" | "identifier" | "primitive_type" => {
return strip_raw(&parse::node_text(node, src));
}
"generic_type" | "reference_type" | "pointer_type" => "type",
"scoped_type_identifier" | "scoped_identifier" => "name",
"dynamic_type" => "trait",
_ => return String::new(),
};
match node.child_by_field_name(field) {
Some(next) => node = next,
None => return String::new(),
}
}
}
fn span_start_line(node: Node) -> usize {
let mut start = node.start_position().row;
let mut current = node.prev_sibling();
while let Some(prev) = current {
match prev.kind() {
"attribute_item" => start = prev.start_position().row,
"line_comment" | "block_comment" => {
if prev.child_by_field_name("outer").is_some() {
start = prev.start_position().row;
} else if prev.child_by_field_name("inner").is_some() {
break;
}
}
_ => break,
}
current = prev.prev_sibling();
}
start + 1
}
fn end_line(node: Node) -> usize {
let end = node.end_position();
if end.column == 0 && end.row > node.start_position().row {
end.row
} else {
end.row + 1
}
}
fn dedupe(found: Vec<Found>) -> Vec<RsItem> {
let mut seen: HashMap<String, usize> = HashMap::new();
found
.into_iter()
.map(|f| {
let repeats = seen.entry(f.path.clone()).or_insert(0);
let path = if *repeats == 0 {
f.path
} else {
format!("{}-{}", f.path, repeats)
};
*repeats += 1;
RsItem {
name: f.name,
kind: f.kind.to_string(),
path,
line: f.line,
name_line: f.name_line,
end_line: f.end_line,
}
})
.collect()
}
const SEGMENT_ORDER: &[&str] = &[
"struct",
"enum",
"union",
"trait",
"type",
"fn",
"const",
"static",
"macro_rules",
"mod",
];
fn compose_purpose(root: Node, src: &[u8], found: &[Found]) -> Option<String> {
let prefix = purpose_prefix(root, src);
let segments = purpose_segments(found);
match (prefix, segments.is_empty()) {
(Some(p), false) => Some(truncate(&format!("{p} — {}", segments.join(" | ")))),
(Some(p), true) => Some(truncate(&p)),
(None, false) => Some(truncate(&segments.join(" | "))),
(None, true) => None,
}
}
fn purpose_segments(found: &[Found]) -> Vec<String> {
SEGMENT_ORDER
.iter()
.flat_map(|kind| {
found
.iter()
.filter(move |f| f.file_level && f.kind == *kind)
.map(|f| format!("{} {}", f.kind, f.name))
})
.collect()
}
fn is_comment(node: Node) -> bool {
matches!(node.kind(), "line_comment" | "block_comment")
}
fn is_inner_doc(node: Node) -> bool {
is_comment(node) && node.child_by_field_name("inner").is_some()
}
fn purpose_prefix(root: Node, src: &[u8]) -> Option<String> {
let mut cursor = root.walk();
let children: Vec<Node> = root
.named_children(&mut cursor)
.skip_while(|n| matches!(n.kind(), "shebang" | "inner_attribute_item"))
.collect();
let first = *children.first()?;
if is_inner_doc(first) {
if first.kind() == "block_comment" {
return first_doc_line(&parse::node_text(first, src));
}
return children
.iter()
.take_while(|n| n.kind() == "line_comment" && is_inner_doc(**n))
.find_map(|n| first_doc_line(&parse::node_text(*n, src)));
}
if is_comment(first) && first.child_by_field_name("outer").is_none() {
return plain_comment_line(&parse::node_text(first, src));
}
None
}
fn first_doc_line(text: &str) -> Option<String> {
let body = text
.strip_prefix("//!")
.or_else(|| text.strip_prefix("/*!"))
.unwrap_or(text);
let body = body.strip_suffix("*/").unwrap_or(body);
body.lines()
.map(|l| {
let l = l.trim();
l.strip_prefix('*').unwrap_or(l).trim().to_string()
})
.find(|l| !l.is_empty())
}
fn plain_comment_line(text: &str) -> Option<String> {
let line = text.lines().next().unwrap_or("");
let line = line
.strip_prefix("//")
.or_else(|| line.strip_prefix("/*"))
.unwrap_or(line);
let line = line.trim_end().strip_suffix("*/").unwrap_or(line).trim();
(!line.is_empty()).then(|| line.to_string())
}