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::{find_class_body, 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;
fn is_abstract_type_container(kind: &str, lang: Language) -> bool {
match lang {
Language::Rust => kind == "trait_item",
Language::TypeScript | Language::Vue | Language::Svelte => matches!(
kind,
"interface_declaration" | "type_alias_declaration" | "enum_declaration"
),
Language::Java | Language::CSharp => {
matches!(kind, "interface_declaration" | "enum_declaration")
}
Language::Scala => kind == "trait_definition",
Language::Swift => matches!(kind, "protocol_declaration" | "enum_declaration"),
Language::Kotlin => kind == "interface_declaration",
Language::Php => matches!(
kind,
"interface_declaration" | "trait_declaration" | "enum_declaration"
),
Language::Cpp => kind == "enum_specifier",
_ => false,
}
}
fn body_has_function_descendant(body: Node, lang: Language) -> bool {
let mut stack: Vec<Node> = body.children(&mut body.walk()).collect();
while let Some(node) = stack.pop() {
if is_function_node(node.kind(), lang) {
return true;
}
for child in node.children(&mut node.walk()) {
stack.push(child);
}
}
false
}
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 let Some(class_name) = get_node_name(node, bytes, lang) {
if let Some(unit) = extract_class(node, path, lines, bytes, lang, file_imports) {
units.push(unit);
}
if !is_abstract_type_container(kind, lang) {
if let Some(body) = find_class_body(node, lang) {
if !body_has_function_descendant(body, lang) {
return;
}
for child in body.children(&mut body.walk()) {
extract_from_node(
child,
path,
lines,
bytes,
lang,
units,
Some(&class_name),
file_imports,
depth + 1,
max_depth,
depth_limit_hit,
);
}
}
}
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,
);
}
}