mod analysis;
mod ast;
mod call_graph;
mod extract;
mod html;
mod language;
mod qml;
mod svelte;
mod text;
pub mod types;
mod vue;
#[cfg(test)]
mod tests;
#[cfg(test)]
#[path = "test_core.rs"]
mod test_core;
pub use call_graph::build_call_graph;
pub use language::{detect_language, is_text_format};
pub use types::{CodeUnit, Language, UnitType};
use analysis::extract_file_imports;
use ast::{get_node_name, is_class_node, is_constant_node, is_function_node};
use extract::{extract_class, extract_constant, extract_function, fill_raw_code_gaps};
use language::get_tree_sitter_language;
use text::extract_text_units;
use crate::config::{Config, DEFAULT_MAX_RECURSION_DEPTH};
use std::path::Path;
use std::sync::OnceLock;
use tree_sitter::{Node, Parser};
pub(crate) fn max_recursion_depth() -> usize {
static MAX_DEPTH: OnceLock<usize> = OnceLock::new();
*MAX_DEPTH.get_or_init(|| {
let from_config = Config::load()
.ok()
.map(|c| c.get_max_recursion_depth())
.unwrap_or(DEFAULT_MAX_RECURSION_DEPTH);
std::env::var("COLGREP_MAX_RECURSION_DEPTH")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.filter(|v| *v > 0)
.unwrap_or(from_config)
})
}
pub fn extract_units(path: &Path, source: &str, lang: Language) -> Vec<CodeUnit> {
if is_text_format(lang) {
return extract_text_units(path, source, lang);
}
if lang == Language::Vue {
return vue::extract_vue_units(path, source);
}
if lang == Language::Svelte {
return svelte::extract_svelte_units(path, source);
}
if lang == Language::Qml {
return qml::extract_qml_units(path, source);
}
if lang == Language::Html {
return html::extract_html_units(path, source);
}
let mut parser = Parser::new();
if parser
.set_language(&get_tree_sitter_language(lang))
.is_err()
{
return Vec::new();
}
let tree = match parser.parse(source, None) {
Some(t) => t,
None => return Vec::new(),
};
let lines: Vec<&str> = source.lines().collect();
let bytes = source.as_bytes();
let file_imports = extract_file_imports(tree.root_node(), bytes, lang);
let max_depth = max_recursion_depth();
let mut units = Vec::new();
let mut depth_limit_hit = false;
extract_from_node(
tree.root_node(),
path,
&lines,
bytes,
lang,
&mut units,
None,
&file_imports,
0,
max_depth,
&mut depth_limit_hit,
);
if depth_limit_hit {
eprintln!(
"⚠️ Skipping {} (AST nesting exceeded max depth: {})",
path.display(),
max_depth
);
return Vec::new();
}
fill_raw_code_gaps(&mut units, path, &lines, lang, &file_imports);
units
}
#[allow(clippy::too_many_arguments)]
fn extract_from_node(
node: Node,
path: &Path,
lines: &[&str],
bytes: &[u8],
lang: Language,
units: &mut Vec<CodeUnit>,
parent_class: Option<&str>,
file_imports: &[String],
depth: usize,
max_depth: usize,
depth_limit_hit: &mut bool,
) {
if *depth_limit_hit {
return;
}
if depth > max_depth {
*depth_limit_hit = true;
return;
}
let kind = node.kind();
if is_function_node(kind, lang) {
if let Some(unit) =
extract_function(node, path, lines, bytes, lang, parent_class, file_imports)
{
units.push(unit);
}
}
else if is_class_node(kind, lang) {
if get_node_name(node, bytes, lang).is_some() {
if let Some(unit) = extract_class(node, path, lines, bytes, lang, file_imports) {
units.push(unit);
}
return; }
}
else if parent_class.is_none() && is_constant_node(kind, lang) {
if let Some(unit) = extract_constant(node, path, lines, bytes, lang, file_imports) {
units.push(unit);
}
return;
}
for child in node.children(&mut node.walk()) {
extract_from_node(
child,
path,
lines,
bytes,
lang,
units,
parent_class,
file_imports,
depth + 1,
max_depth,
depth_limit_hit,
);
}
}