use anyhow::{Context, Result};
use std::{
fs,
path::{Path, PathBuf},
};
use tracing::{debug, warn};
use crate::models::ParsedEntity;
use tokio::sync::mpsc;
mod comments;
mod context;
pub(crate) mod extractor;
pub mod languages;
mod orphans;
mod utils;
#[cfg(test)]
mod test_utils;
const DEFAULT_JAVA_QUERY: &str = include_str!("../../../queries/java.scm");
const DEFAULT_KOTLIN_QUERY: &str = include_str!("../../../queries/kotlin.scm");
const DEFAULT_TS_QUERY: &str = include_str!("../../../queries/typescript.scm");
const DEFAULT_TSX_QUERY: &str = include_str!("../../../queries/tsx.scm");
const DEFAULT_JS_QUERY: &str = include_str!("../../../queries/javascript.scm");
#[expect(dead_code, reason = "reserved for future query-based HTML parsing")]
const DEFAULT_HTML_QUERY: &str = include_str!("../../../queries/html.scm");
const DEFAULT_CSS_QUERY: &str = include_str!("../../../queries/css.scm");
const DEFAULT_SCSS_QUERY: &str = include_str!("../../../queries/scss.scm");
const DEFAULT_RUST_QUERY: &str = include_str!("../../../queries/rust.scm");
const DEFAULT_PYTHON_QUERY: &str = include_str!("../../../queries/python.scm");
const DEFAULT_C_QUERY: &str = include_str!("../../../queries/c.scm");
const DEFAULT_CPP_QUERY: &str = include_str!("../../../queries/cpp.scm");
const DEFAULT_MD_QUERY: &str = include_str!("../../../queries/markdown.scm");
#[derive(Clone)]
pub struct ParseConfig {
pub repo_root: PathBuf,
pub custom_queries_path: Option<String>,
pub repo_name: String,
pub include_config_files: bool,
pub repo_path: Option<String>,
}
impl Default for ParseConfig {
fn default() -> Self {
Self {
repo_root: PathBuf::from("."),
custom_queries_path: None,
repo_name: String::new(),
include_config_files: false,
repo_path: None,
}
}
}
pub type FileParsedCallback = std::sync::Arc<dyn Fn() + Send + Sync>;
pub fn parse_files_stream(
files: &[PathBuf],
parse_cfg: &ParseConfig,
sender: mpsc::Sender<ParsedEntity>,
max_concurrent: usize,
on_file_parsed: Option<FileParsedCallback>,
) {
use std::sync::{Arc, Condvar, Mutex};
let sem = Arc::new((Mutex::new(0usize), Condvar::new()));
let buffer: Arc<Mutex<Vec<ParsedEntity>>> = Arc::new(Mutex::new(Vec::new()));
std::thread::scope(|s| {
for path in files {
let path = path.clone();
let parse_cfg = parse_cfg.clone();
let sem = Arc::clone(&sem);
let buffer = Arc::clone(&buffer);
{
let (lock, cvar) = &*sem;
let mut active = lock.lock().unwrap();
while *active >= max_concurrent {
active = cvar.wait(active).unwrap();
}
*active += 1;
}
let on_file_parsed = on_file_parsed.clone();
s.spawn(move || {
if let Ok(entities) = parse_single_file(&path, &parse_cfg) {
let mut buf = buffer.lock().unwrap();
buf.extend(entities);
}
if let Some(cb) = &on_file_parsed {
cb();
}
let (lock, cvar) = &*sem;
let mut active = lock.lock().unwrap();
*active -= 1;
cvar.notify_one();
});
}
});
let mut entities = Arc::try_unwrap(buffer)
.map(|m| m.into_inner().unwrap_or_default())
.unwrap_or_default();
languages::varnish::aggregate_varnish_builtin_subs(&mut entities, &parse_cfg.repo_name);
for entity in entities {
if sender.blocking_send(entity).is_err() {
warn!("Failed to send entity to channel");
break;
}
}
}
pub fn parse_files(files: &[PathBuf], parse_cfg: &ParseConfig) -> Vec<ParsedEntity> {
let (tx, mut rx) = mpsc::channel(1024);
let cpus = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4);
parse_files_stream(files, parse_cfg, tx, cpus, None);
let mut entities = Vec::with_capacity(1024);
while let Ok(entity) = rx.try_recv() {
entities.push(entity);
}
languages::varnish::aggregate_varnish_builtin_subs(&mut entities, &parse_cfg.repo_name);
entities
}
fn is_cpp_header(source: &str) -> bool {
let cpp_indicators = [
"class ",
"namespace ",
"template<",
"template <",
"virtual ",
"public:",
"private:",
"protected:",
"using namespace",
"constexpr ",
"noexcept",
"nullptr",
"override",
" final",
"::", ];
cpp_indicators.iter().any(|kw| source.contains(kw))
}
#[expect(
clippy::too_many_lines,
reason = "function is verbose but correct — extraction deferred"
)]
#[expect(
clippy::cognitive_complexity,
reason = "function is verbose but correct — extraction deferred"
)]
fn parse_single_file(path: &Path, parse_cfg: &ParseConfig) -> Result<Vec<ParsedEntity>> {
let source = {
let bytes =
fs::read(path).with_context(|| format!("Cannot read file: {}", path.display()))?;
String::from_utf8_lossy(&bytes).into_owned()
};
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or_default();
let filename = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default();
let file_path = crate::pipeline::files::to_repo_relative(path, &parse_cfg.repo_root);
if filename == "Jenkinsfile" {
return Ok(languages::jenkins::extract_entities_jenkins(
&source,
&file_path,
&parse_cfg.repo_name,
));
}
let entities = match ext {
"java" => {
let query_src = load_query_source("java.scm", DEFAULT_JAVA_QUERY, parse_cfg);
extractor::extract_entities(
&source,
tree_sitter_java::LANGUAGE.into(),
&query_src,
"java",
&file_path,
&parse_cfg.repo_name,
)?
}
"kt" | "kts" => {
let query_src = load_query_source("kotlin.scm", DEFAULT_KOTLIN_QUERY, parse_cfg);
extractor::extract_entities(
&source,
tree_sitter_kotlin_ng::LANGUAGE.into(),
&query_src,
"kotlin",
&file_path,
&parse_cfg.repo_name,
)?
}
"ts" | "tsx" | "cts" => {
let mut query_src = load_query_source("typescript.scm", DEFAULT_TS_QUERY, parse_cfg);
let lang: tree_sitter::Language = if ext == "tsx" {
let tsx_rules = load_query_source("tsx.scm", DEFAULT_TSX_QUERY, parse_cfg);
query_src.push('\n');
query_src.push_str(&tsx_rules);
tree_sitter_typescript::LANGUAGE_TSX.into()
} else {
tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()
};
extractor::extract_entities(
&source,
lang,
&query_src,
"typescript",
&file_path,
&parse_cfg.repo_name,
)?
}
"js" | "mjs" | "cjs" | "jsx" => {
let query_src = load_query_source("javascript.scm", DEFAULT_JS_QUERY, parse_cfg);
let lang: tree_sitter::Language = tree_sitter_javascript::LANGUAGE.into();
extractor::extract_entities(
&source,
lang,
&query_src,
"javascript",
&file_path,
&parse_cfg.repo_name,
)?
}
"html" | "htm" => {
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&tree_sitter_html::LANGUAGE.into())
.context("Failed to load HTML grammar")?;
let tree = parser
.parse(&source, None)
.context("Failed to parse HTML")?;
languages::html::extract_entities_html(
tree.root_node(),
source.as_bytes(),
&file_path,
&parse_cfg.repo_name,
)
}
"yml" | "yaml" => {
if !parse_cfg.include_config_files {
Vec::new()
} else {
dispatch_yaml(&source, path, &file_path, &parse_cfg.repo_name)
}
}
"json" => {
if !parse_cfg.include_config_files
&& filename != "package.json"
&& filename != "tsconfig.json"
{
Vec::new()
} else {
languages::json_config::extract_entities_json_config(
&source,
&file_path,
&parse_cfg.repo_name,
)
}
}
"properties" => {
if !parse_cfg.include_config_files {
Vec::new()
} else {
languages::properties::extract_entities_properties(
&source,
&file_path,
&parse_cfg.repo_name,
)
}
}
"tpl" => {
if !parse_cfg.include_config_files {
Vec::new()
} else {
let chart_name = detect_chart_name(path, &parse_cfg.repo_root);
languages::helm::extract_helm_template(
&source,
&file_path,
&parse_cfg.repo_name,
&chart_name,
)
}
}
"css" => {
let query_src = load_query_source("css.scm", DEFAULT_CSS_QUERY, parse_cfg);
extractor::extract_entities(
&source,
tree_sitter_css::LANGUAGE.into(),
&query_src,
"css",
&file_path,
&parse_cfg.repo_name,
)?
}
"scss" | "sass" => {
let query_src = load_query_source("scss.scm", DEFAULT_SCSS_QUERY, parse_cfg);
extractor::extract_entities(
&source,
tree_sitter_scss::language(),
&query_src,
"scss",
&file_path,
&parse_cfg.repo_name,
)?
}
"py" | "pyi" | "pyw" => {
let query_src = load_query_source("python.scm", DEFAULT_PYTHON_QUERY, parse_cfg);
extractor::extract_entities(
&source,
tree_sitter_python::LANGUAGE.into(),
&query_src,
"python",
&file_path,
&parse_cfg.repo_name,
)?
}
"rs" => {
let query_src = load_query_source("rust.scm", DEFAULT_RUST_QUERY, parse_cfg);
let mut rust_entities = extractor::extract_entities(
&source,
tree_sitter_rust::LANGUAGE.into(),
&query_src,
"rust",
&file_path,
&parse_cfg.repo_name,
)?;
languages::rust::qualify_rust_fqns(
&mut rust_entities,
&file_path,
parse_cfg.repo_path.as_deref(),
Some(&source),
);
rust_entities
}
"c" => {
let query_src = load_query_source("c.scm", DEFAULT_C_QUERY, parse_cfg);
extractor::extract_entities(
&source,
tree_sitter_c::LANGUAGE.into(),
&query_src,
"c",
&file_path,
&parse_cfg.repo_name,
)?
}
"h" => {
if is_cpp_header(&source) {
let query_src = load_query_source("cpp.scm", DEFAULT_CPP_QUERY, parse_cfg);
extractor::extract_entities(
&source,
tree_sitter_cpp::LANGUAGE.into(),
&query_src,
"cpp",
&file_path,
&parse_cfg.repo_name,
)?
} else {
let query_src = load_query_source("c.scm", DEFAULT_C_QUERY, parse_cfg);
extractor::extract_entities(
&source,
tree_sitter_c::LANGUAGE.into(),
&query_src,
"c",
&file_path,
&parse_cfg.repo_name,
)?
}
}
"cpp" | "cxx" | "cc" | "hpp" | "hxx" | "hh" => {
let query_src = load_query_source("cpp.scm", DEFAULT_CPP_QUERY, parse_cfg);
extractor::extract_entities(
&source,
tree_sitter_cpp::LANGUAGE.into(),
&query_src,
"cpp",
&file_path,
&parse_cfg.repo_name,
)?
}
"groovy" => {
languages::groovy::extract_entities_groovy(&source, &file_path, &parse_cfg.repo_name)
}
"gradle" => {
languages::gradle::extract_entities_gradle(&source, &file_path, &parse_cfg.repo_name)
}
"jenkinsfile" => {
languages::jenkins::extract_entities_jenkins(&source, &file_path, &parse_cfg.repo_name)
}
"xml" => languages::xml::extract_entities_xml(&source, &file_path, &parse_cfg.repo_name),
"toml" => languages::toml::extract_entities_toml(&source, &file_path, &parse_cfg.repo_name),
"md" | "markdown" => {
let query_src = load_query_source("markdown.scm", DEFAULT_MD_QUERY, parse_cfg);
extractor::extract_entities(
&source,
tree_sitter_md::LANGUAGE.into(),
&query_src,
"markdown",
&file_path,
&parse_cfg.repo_name,
)?
}
"vcl" => {
languages::varnish::extract_entities_vcl(&source, &file_path, &parse_cfg.repo_name)
}
"vtc" => {
languages::varnish::extract_entities_vtc(&source, &file_path, &parse_cfg.repo_name)
}
"vcc" => {
languages::varnish::extract_entities_vcc(&source, &file_path, &parse_cfg.repo_name)
}
other => {
warn!("Unsupported extension '{other}', skipping");
vec![]
}
};
debug!("Extracted {} entities from {}", entities.len(), file_path);
Ok(entities)
}
fn dispatch_yaml(
source: &str,
absolute_path: &Path,
relative_path: &str,
repo_name: &str,
) -> Vec<ParsedEntity> {
let filename = absolute_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
if filename == "Chart.yaml" {
return languages::helm::extract_chart_yaml(source, relative_path, repo_name);
}
if is_in_helm_chart_dir(absolute_path) {
if filename == "values.yaml" || filename == "values.yml" {
let chart_name = detect_chart_name(
absolute_path,
absolute_path.parent().unwrap_or(Path::new(".")),
);
return languages::helm::extract_values_yaml(
source,
relative_path,
repo_name,
&chart_name,
);
}
if is_in_templates_dir(absolute_path) {
let chart_name = detect_chart_name(
absolute_path,
absolute_path.parent().unwrap_or(Path::new(".")),
);
return languages::helm::extract_helm_template(
source,
relative_path,
repo_name,
&chart_name,
);
}
}
if let Ok(yaml) = serde_yaml::from_str::<serde_yaml::Value>(source)
&& yaml.get("apiVersion").is_some()
&& yaml.get("kind").is_some()
{
return languages::kubernetes::extract_entities_k8s(source, relative_path, repo_name);
}
languages::yaml::extract_entities_yaml(source, relative_path, repo_name)
}
fn is_in_helm_chart_dir(absolute_path: &Path) -> bool {
let mut current = absolute_path.parent();
while let Some(dir) = current {
if dir.join("Chart.yaml").exists() {
return true;
}
current = dir.parent();
}
false
}
fn is_in_templates_dir(absolute_path: &Path) -> bool {
let mut current = Some(absolute_path);
while let Some(p) = current {
if p.file_name().and_then(|n| n.to_str()) == Some("templates") {
return true;
}
current = p.parent();
}
false
}
fn detect_chart_name(absolute_path: &Path, _repo_root: &Path) -> String {
let mut current = absolute_path.parent();
while let Some(dir) = current {
let chart_yaml = dir.join("Chart.yaml");
if chart_yaml.exists() {
if let Ok(source) = fs::read_to_string(&chart_yaml)
&& let Ok(yaml) = serde_yaml::from_str::<serde_yaml::Value>(&source)
&& let Some(name) = yaml.get("name").and_then(|v| v.as_str())
{
return name.to_string();
}
break;
}
current = dir.parent();
}
absolute_path
.parent()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string()
}
#[expect(
clippy::cognitive_complexity,
reason = "function is verbose but correct — extraction deferred"
)]
fn load_query_source(filename: &str, default: &str, cfg: &ParseConfig) -> String {
if let Some(dir) = &cfg.custom_queries_path {
let custom_path = PathBuf::from(dir).join(filename);
if custom_path.exists() {
match fs::read_to_string(&custom_path) {
Ok(src) => {
tracing::info!("Using custom query: {}", custom_path.display());
return src;
}
Err(e) => warn!(
"Failed to load custom query {}: {e} — using built-in",
custom_path.display()
),
}
}
}
default.to_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_config_creation() {
let cfg = ParseConfig {
custom_queries_path: None,
repo_name: "test-repo".to_string(),
include_config_files: true,
repo_path: None,
..Default::default()
};
assert_eq!(cfg.repo_name, "test-repo");
assert!(cfg.custom_queries_path.is_none());
}
#[test]
fn test_parse_config_with_custom_queries() {
let cfg = ParseConfig {
custom_queries_path: Some("/custom/queries".to_string()),
repo_name: "my-repo".to_string(),
include_config_files: true,
repo_path: None,
..Default::default()
};
assert_eq!(cfg.repo_name, "my-repo");
assert_eq!(cfg.custom_queries_path, Some("/custom/queries".to_string()));
}
#[test]
fn test_load_query_source_uses_default() {
let cfg = ParseConfig {
custom_queries_path: None,
repo_name: "test-repo".to_string(),
include_config_files: true,
repo_path: None,
..Default::default()
};
let default_query = "MATCH (n) RETURN n";
let result = load_query_source("test.scm", default_query, &cfg);
assert_eq!(result, default_query);
}
#[test]
fn test_load_query_source_nonexistent_custom_path() {
let cfg = ParseConfig {
custom_queries_path: None,
repo_name: "test-repo".to_string(),
include_config_files: true,
repo_path: None,
..Default::default()
};
let default_query = "MATCH (n) RETURN n";
let result = load_query_source("test.scm", default_query, &cfg);
assert_eq!(result, default_query);
}
#[test]
fn test_parse_files_empty_list() {
let cfg = ParseConfig {
custom_queries_path: None,
repo_name: "test-repo".to_string(),
include_config_files: true,
repo_path: None,
..Default::default()
};
let files: Vec<PathBuf> = vec![];
let (sender, mut receiver) = mpsc::channel::<ParsedEntity>(32);
parse_files_stream(&files, &cfg, sender, 4, None);
assert!(receiver.try_recv().is_err());
}
#[test]
fn test_parse_files_with_mock_channel() {
let cfg = ParseConfig {
custom_queries_path: None,
repo_name: "test-repo".to_string(),
include_config_files: true,
repo_path: None,
..Default::default()
};
let files: Vec<PathBuf> = vec![];
let (sender, mut receiver) = mpsc::channel::<ParsedEntity>(32);
parse_files_stream(&files, &cfg, sender, 4, None);
assert!(receiver.try_recv().is_err()); }
#[test]
fn test_is_cpp_header_detects_class() {
assert!(is_cpp_header(
"class Print {\npublic:\n void write();\n};"
));
}
#[test]
fn test_is_cpp_header_detects_namespace() {
assert!(is_cpp_header("namespace Engine {\n class Foo {};\n}"));
}
#[test]
fn test_is_cpp_header_detects_virtual() {
assert!(is_cpp_header("virtual size_t write(uint8_t) = 0;"));
}
#[test]
fn test_is_cpp_header_detects_template() {
assert!(is_cpp_header("template <typename T>\nclass Container {};"));
}
#[test]
fn test_is_cpp_header_pure_c_returns_false() {
let c_header = r#"
#ifndef FOO_H
#define FOO_H
typedef struct { int x; int y; } Point;
void foo(int n);
int bar(const char *s);
#endif
"#;
assert!(!is_cpp_header(c_header));
}
#[test]
fn test_is_cpp_header_empty_returns_false() {
assert!(!is_cpp_header(""));
}
#[test]
fn test_is_cpp_header_detects_qualified_call() {
assert!(is_cpp_header(
"size_t Print::write(const uint8_t *buf, size_t s) { return 0; }"
));
}
#[test]
fn test_unsupported_file_extension_handling() {
let path = PathBuf::from("/test/file.unsupported");
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or_default();
assert_eq!(ext, "unsupported");
assert!(
ext != "java"
&& ext != "ts"
&& ext != "tsx"
&& ext != "cts"
&& ext != "js"
&& ext != "mjs"
&& ext != "cjs"
&& ext != "jsx"
);
}
#[test]
fn test_java_file_extension_detection() {
let path = PathBuf::from("/test/Service.java");
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or_default();
assert_eq!(ext, "java");
}
fn assert_extensions_detected(extensions: &[&str]) {
for ext_name in extensions {
let path = PathBuf::from(format!("/test/file.{}", ext_name));
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or_default();
assert_eq!(ext, *ext_name);
}
}
#[test]
fn test_kotlin_file_extension_detection() {
assert_extensions_detected(&["kt", "kts"]);
}
#[test]
fn test_typescript_file_extension_detection() {
assert_extensions_detected(&["ts", "tsx", "cts"]);
}
#[test]
fn test_javascript_file_extension_detection() {
assert_extensions_detected(&["js", "mjs", "cjs", "jsx"]);
}
#[test]
fn test_file_path_conversion() {
let parse_cfg = ParseConfig {
repo_root: PathBuf::from("/home/user/project"),
..Default::default()
};
let path = PathBuf::from("/home/user/project/src/Main.java");
let file_path = crate::pipeline::files::to_repo_relative(&path, &parse_cfg.repo_root);
assert!(file_path.contains("Main.java"));
assert_eq!(file_path, "src/Main.java");
}
#[test]
fn test_parsed_entity_file_path_is_relative() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let repo_root = dir.path().canonicalize().unwrap();
let src_dir = repo_root.join("src");
fs::create_dir_all(&src_dir).unwrap();
let java_file = src_dir.join("Foo.java");
fs::write(&java_file, "public class Foo { public void bar() {} }").unwrap();
let parse_cfg = ParseConfig {
repo_root: repo_root.clone(),
custom_queries_path: None,
repo_name: "test-repo".to_string(),
include_config_files: false,
repo_path: Some(repo_root.to_string_lossy().into_owned()),
};
let entities = parse_files(&[java_file], &parse_cfg);
assert!(
!entities.is_empty(),
"parser should produce at least one entity for Foo.java"
);
for entity in &entities {
assert!(
!entity.file_path.starts_with('/'),
"file_path must be relative (no leading /), got {}",
entity.file_path
);
assert!(
!entity.file_path.contains('\\'),
"file_path must use POSIX separators, got {}",
entity.file_path
);
}
let foo = entities.iter().find(|e| e.name == "Foo").expect("Foo");
assert_eq!(foo.file_path, "src/Foo.java");
}
#[test]
fn test_parsed_entity_file_path_verbatim_without_repo_root() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let java_file = dir.path().join("Main.java");
fs::write(&java_file, "public class Main {}").unwrap();
let parse_cfg = ParseConfig {
repo_root: PathBuf::from("."),
custom_queries_path: None,
repo_name: "test-repo".to_string(),
include_config_files: false,
repo_path: None,
};
let entities = parse_files(std::slice::from_ref(&java_file), &parse_cfg);
assert!(!entities.is_empty(), "parser must still produce entities");
let main = entities.iter().find(|e| e.name == "Main").expect("Main");
assert!(
main.file_path.contains("Main.java"),
"verbatim path should still contain the filename, got {}",
main.file_path
);
}
#[test]
fn test_parse_config_repo_name_assignment() {
let cfg = ParseConfig {
custom_queries_path: None,
repo_name: "myproject".to_string(),
include_config_files: true,
repo_path: None,
..Default::default()
};
let path = PathBuf::from("/src/Main.java");
let _entities = parse_files(&[path], &cfg);
assert_eq!(cfg.repo_name, "myproject");
}
#[test]
fn test_parse_files_with_empty_input() {
let cfg = ParseConfig {
custom_queries_path: None,
repo_name: "test-repo".to_string(),
include_config_files: true,
repo_path: None,
..Default::default()
};
let files: Vec<PathBuf> = vec![];
let entities = parse_files(&files, &cfg);
assert!(entities.is_empty());
}
#[test]
fn test_channel_sender_behavior_mock() {
let (sender, mut receiver) = mpsc::channel::<ParsedEntity>(32);
drop(sender);
assert!(receiver.try_recv().is_err());
}
#[test]
fn test_bounded_channel_blocking_send() {
let (sender, mut receiver) = mpsc::channel::<ParsedEntity>(2);
let entity = ParsedEntity::new(
"TestEntity",
crate::models::EntityKind::Class,
"com.test.TestEntity",
None,
None,
"java",
"/test/Test.java",
1,
5,
None,
"test-repo",
);
assert!(sender.blocking_send(entity.clone()).is_ok());
assert!(sender.blocking_send(entity).is_ok());
assert!(receiver.try_recv().is_ok());
assert!(receiver.try_recv().is_ok());
}
#[test]
fn test_bounded_channel_capacity_backpressure() {
let (sender, _receiver) = mpsc::channel::<ParsedEntity>(2);
let entity = ParsedEntity::new(
"TestEntity",
crate::models::EntityKind::Class,
"com.test.TestEntity",
None,
None,
"java",
"/test/Test.java",
1,
5,
None,
"test-repo",
);
assert!(sender.try_send(entity.clone()).is_ok());
assert!(sender.try_send(entity.clone()).is_ok());
assert!(sender.try_send(entity).is_err());
}
#[test]
fn test_bounded_channel_receives_after_blocking_send() {
let (sender, mut receiver) = mpsc::channel::<ParsedEntity>(1);
let entity = ParsedEntity::new(
"TestClass",
crate::models::EntityKind::Class,
"com.example.TestClass",
Some("public class TestClass".to_string()),
Some("A test class".to_string()),
"java",
"/proj/TestClass.java",
10,
25,
None,
"test-repo",
);
sender.blocking_send(entity).unwrap();
let received = receiver.try_recv().unwrap();
assert_eq!(received.name, "TestClass");
assert_eq!(received.fqn, "com.example.TestClass");
assert_eq!(received.language, "java");
}
#[test]
fn test_parse_files_stream_callback_once_per_file() {
use std::sync::atomic::{AtomicUsize, Ordering};
use tempfile::tempdir;
let dir = tempdir().unwrap();
for i in 0..3 {
fs::write(dir.path().join(format!("file_{}.rs", i)), "fn foo() {}").unwrap();
}
let files: Vec<PathBuf> = (0..3)
.map(|i| dir.path().join(format!("file_{}.rs", i)))
.collect();
let cfg = ParseConfig {
custom_queries_path: None,
repo_name: "test-repo".to_string(),
include_config_files: true,
repo_path: None,
..Default::default()
};
let (sender, mut receiver) = mpsc::channel::<ParsedEntity>(32);
let counter = std::sync::Arc::new(AtomicUsize::new(0));
let counter_clone = std::sync::Arc::clone(&counter);
let cb: FileParsedCallback = std::sync::Arc::new(move || {
counter_clone.fetch_add(1, Ordering::SeqCst);
});
parse_files_stream(&files, &cfg, sender, 4, Some(cb));
let mut count = 0;
while receiver.try_recv().is_ok() {
count += 1;
}
assert!(count > 0, "Should have extracted some entities");
assert_eq!(counter.load(Ordering::SeqCst), 3);
}
#[test]
fn test_parse_files_stream_callback_counts_unparseable() {
use std::sync::atomic::{AtomicUsize, Ordering};
use tempfile::tempdir;
let dir = tempdir().unwrap();
fs::write(dir.path().join("valid.rs"), "fn foo() {}").unwrap();
fs::write(dir.path().join("valid2.rs"), "enum Color { Red }").unwrap();
fs::write(dir.path().join("broken.rs"), "not valid rust @@@@!!").unwrap();
let files: Vec<PathBuf> = ["valid.rs", "valid2.rs", "broken.rs"]
.iter()
.map(|f| dir.path().join(f))
.collect();
let cfg = ParseConfig {
custom_queries_path: None,
repo_name: "test-repo".to_string(),
include_config_files: true,
repo_path: None,
..Default::default()
};
let (sender, _receiver) = mpsc::channel::<ParsedEntity>(32);
let counter = std::sync::Arc::new(AtomicUsize::new(0));
let counter_clone = std::sync::Arc::clone(&counter);
let cb: FileParsedCallback = std::sync::Arc::new(move || {
counter_clone.fetch_add(1, Ordering::SeqCst);
});
parse_files_stream(&files, &cfg, sender, 4, Some(cb));
assert_eq!(counter.load(Ordering::SeqCst), 3);
}
#[test]
fn test_parse_files_stream_none_callback() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
fs::write(dir.path().join("test.rs"), "fn foo() {}").unwrap();
let files: Vec<PathBuf> = vec![dir.path().join("test.rs")];
let cfg = ParseConfig {
custom_queries_path: None,
repo_name: "test-repo".to_string(),
include_config_files: true,
repo_path: None,
..Default::default()
};
let (sender, mut receiver) = mpsc::channel::<ParsedEntity>(32);
parse_files_stream(&files, &cfg, sender, 4, None);
let mut count = 0;
while receiver.try_recv().is_ok() {
count += 1;
}
assert!(count > 0, "Should parse entities with None callback");
}
#[test]
fn test_multiple_file_extensions_in_batch() {
let files = [
PathBuf::from("file1.java"),
PathBuf::from("file2.ts"),
PathBuf::from("file3.tsx"),
PathBuf::from("file4.kt"),
PathBuf::from("file5.unsupported"),
];
let expected_extensions = ["java", "ts", "tsx", "kt", "unsupported"];
for (file, expected_ext) in files.iter().zip(expected_extensions.iter()) {
let ext = file
.extension()
.and_then(|e| e.to_str())
.unwrap_or_default();
assert_eq!(ext, *expected_ext);
}
}
}