use crate::models::{EntityKind, ParsedEntity};
use crate::pipeline::parser::utils::node_text;
use crate::pipeline::rust_crate_discovery::CrateDiscovery;
use tree_sitter::Node;
#[derive(Debug, Clone)]
struct RustModuleContext {
name: String,
start_line: usize,
end_line: usize,
is_cfg_test: bool,
}
pub(crate) fn qualify_rust_fqns(
entities: &mut [ParsedEntity],
file_path: &str,
repo_path: Option<&str>,
source: Option<&str>,
) {
let Some(repo_path) = repo_path else {
return;
};
if entities.is_empty() {
return;
}
let repo_root = std::path::Path::new(repo_path);
let relative_path = std::path::Path::new(file_path);
let absolute_path: std::path::PathBuf = if relative_path.is_absolute() {
relative_path.to_path_buf()
} else {
repo_root.join(relative_path)
};
let discovery = CrateDiscovery::discover(repo_root);
let crate_root = discovery.crate_for_file(&absolute_path);
let file_kind = crate::pipeline::parser::context::compute_rust_file_kind(
&absolute_path.to_string_lossy(),
crate_root.map(|cr| cr.root_dir.as_path()),
repo_root,
);
let crate_name = crate_root
.map(|cr| cr.crate_name.as_str())
.unwrap_or("__loose");
let module_contexts: Vec<RustModuleContext> =
source.map(extract_rust_module_contexts).unwrap_or_default();
for entity in entities.iter_mut() {
if entity.language != "rust" {
continue;
}
if !is_qualifiable_rust_kind(&entity.kind) {
continue;
}
let (inline_path, is_test) =
inline_module_path_for_entity(&module_contexts, entity.start_line);
let new_fqn =
crate::pipeline::parser::context::compute_rust_qualified_fqn_with_inline_modules(
&entity.name,
&entity.kind,
&file_kind,
crate_name,
&inline_path,
entity.enclosing_class.as_deref(),
);
entity.fqn = new_fqn;
entity.is_test_context = is_test;
if entity.kind == EntityKind::RustMethod
&& let Some(enclosing_class) = &entity.enclosing_class
{
let class_fqn =
crate::pipeline::parser::context::compute_rust_qualified_fqn_with_inline_modules(
enclosing_class,
&EntityKind::RustStruct,
&file_kind,
crate_name,
&inline_path,
None,
);
entity.enclosing_class_fqn = Some(class_fqn);
}
}
}
fn inline_module_path_for_entity(contexts: &[RustModuleContext], line: usize) -> (String, bool) {
let mut containing: Vec<&RustModuleContext> = contexts
.iter()
.filter(|m| line > m.start_line && line <= m.end_line)
.collect();
containing.sort_by_key(|m| m.start_line);
let path = containing
.iter()
.map(|m| m.name.as_str())
.collect::<Vec<_>>()
.join("::");
let is_test = containing.iter().any(|m| m.is_cfg_test);
(path, is_test)
}
fn extract_rust_module_contexts(source: &str) -> Vec<RustModuleContext> {
let mut parser = tree_sitter::Parser::new();
if parser
.set_language(&tree_sitter_rust::LANGUAGE.into())
.is_err()
{
return Vec::new();
}
let Some(tree) = parser.parse(source, None) else {
return Vec::new();
};
let source_bytes = source.as_bytes();
let mut contexts = Vec::new();
collect_inline_mod_items(&tree.root_node(), source_bytes, &mut contexts);
contexts
}
fn collect_inline_mod_items(node: &Node<'_>, source: &[u8], out: &mut Vec<RustModuleContext>) {
if node.kind() == "mod_item"
&& let Some(ctx) = build_module_context(node, source)
{
out.push(ctx);
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_inline_mod_items(&child, source, out);
}
}
fn build_module_context(node: &Node<'_>, source: &[u8]) -> Option<RustModuleContext> {
let mut name: Option<String> = None;
let mut has_body = false;
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"identifier" if name.is_none() => {
name = Some(node_text(child, source).to_string());
}
"declaration_list" => {
has_body = true;
}
_ => {}
}
}
let name = name?;
if !has_body {
return None;
}
let is_cfg_test = node_attribute_marks_cfg_test(node, source);
Some(RustModuleContext {
name,
start_line: node.start_position().row + 1,
end_line: node.end_position().row + 1,
is_cfg_test,
})
}
fn node_attribute_marks_cfg_test(mod_node: &Node<'_>, source: &[u8]) -> bool {
let mut sibling = mod_node.prev_sibling();
while let Some(s) = sibling {
match s.kind() {
"attribute_item" | "inner_attribute_item" => {
let text = node_text(s, source);
if attribute_text_marks_cfg_test(&text) {
return true;
}
sibling = s.prev_sibling();
}
"line_comment" | "block_comment" => {
sibling = s.prev_sibling();
}
_ => break,
}
}
false
}
fn attribute_text_marks_cfg_test(text: &str) -> bool {
let normalised: String = text.chars().filter(|c| !c.is_whitespace()).collect();
normalised.contains("cfg(test)") || normalised.contains("cfg_attr(test,")
}
fn is_qualifiable_rust_kind(kind: &EntityKind) -> bool {
matches!(
kind,
EntityKind::RustStruct
| EntityKind::RustEnum
| EntityKind::RustUnion
| EntityKind::RustTrait
| EntityKind::RustImpl
| EntityKind::RustFunction
| EntityKind::RustMethod
| EntityKind::RustMacroDef
| EntityKind::RustTypeAlias
| EntityKind::RustConstant
| EntityKind::RustStatic
| EntityKind::RustModule
)
}