use std::collections::HashMap;
use std::path::Path;
use crate::core::{DiagnosticsReport, ToolDiagnostic};
pub(crate) fn abs_to_rel<'a>(
abs_diag_file: &str,
rel_real_pairs: &'a [(String, String)],
) -> Option<&'a str> {
for (rel, real) in rel_real_pairs {
if abs_diag_file == real.as_str() || abs_diag_file == rel.as_str() {
return Some(rel.as_str());
}
let real_suffix = real.trim_start_matches('/');
let rel_suffix = rel.trim_start_matches('/');
if abs_diag_file.ends_with(&format!("/{real_suffix}"))
|| abs_diag_file.ends_with(&format!("/{rel_suffix}"))
|| real.ends_with(&format!("/{abs_diag_file}"))
|| rel.ends_with(&format!("/{abs_diag_file}"))
{
return Some(rel.as_str());
}
}
None
}
pub fn run_diagnostics_blocking(
by_file: HashMap<String, String>,
language_filter: Option<String>,
tool_filter: Option<Vec<String>>,
root_path: Option<String>,
) -> DiagnosticsReport {
use crate::core::global_registry;
run_diagnostics_blocking_with_registry(
by_file,
language_filter,
tool_filter,
root_path,
global_registry(),
)
}
pub fn run_diagnostics_blocking_with_registry(
by_file: HashMap<String, String>,
language_filter: Option<String>,
tool_filter: Option<Vec<String>>,
root_path: Option<String>,
registry: &crate::core::tool_registry::ToolRegistry,
) -> DiagnosticsReport {
use crate::lang::LanguageDetector;
let tools_unavailable: Vec<String> = registry.unavailable_names().to_vec();
let scratch = match tempfile::tempdir() {
Ok(d) => d,
Err(e) => {
tracing::warn!("failed to create scratch dir for diagnostics: {e}");
return DiagnosticsReport {
tools_run: Vec::new(),
tools_unavailable,
diagnostics: Vec::new(),
};
}
};
let mut by_lang: HashMap<String, Vec<(String, String)>> = HashMap::new();
for (file, content) in by_file {
let Some(lang) = LanguageDetector::detect_file(&file) else {
continue;
};
if let Some(want) = &language_filter {
if &lang != want {
continue;
}
}
by_lang.entry(lang).or_default().push((file, content));
}
let mut out: Vec<ToolDiagnostic> = Vec::new();
let mut tools_run_set: std::collections::HashSet<String> = std::collections::HashSet::new();
for (lang, file_pairs) in &by_lang {
let tools = registry.tools_for(lang);
if tools.is_empty() {
continue;
}
let mut proj_tools: Vec<std::sync::Arc<dyn crate::core::tools::StaticTool>> = Vec::new();
let mut file_tools: Vec<std::sync::Arc<dyn crate::core::tools::StaticTool>> = Vec::new();
for tool in tools {
if let Some(names) = &tool_filter {
if !names.iter().any(|n| n == tool.name()) {
continue;
}
}
if tool.is_project_scoped() {
proj_tools.push(tool);
} else {
file_tools.push(tool);
}
}
if !file_tools.is_empty() {
for (idx, (rel_file, content)) in file_pairs.iter().enumerate() {
let name = Path::new(rel_file)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "chunk.txt".to_string());
let file_dir = scratch.path().join(idx.to_string());
if let Err(e) = std::fs::create_dir_all(&file_dir) {
tracing::warn!("failed to create scratch subdir for {name}: {e}");
continue;
}
let path = file_dir.join(&name);
if let Err(e) = std::fs::write(&path, content) {
tracing::warn!("failed to write scratch file {name}: {e}");
continue;
}
for tool in &file_tools {
tools_run_set.insert(tool.name().to_string());
let result = tool.run(&path, content);
match result {
Ok(mut diags) => {
for d in &mut diags {
d.file = rel_file.clone();
}
out.extend(diags);
}
Err(e) => tracing::warn!("diagnostics for {rel_file} failed: {e:#}"),
}
}
}
}
if proj_tools.is_empty() {
continue;
}
let root = match &root_path {
Some(r) => r,
None => {
tracing::info!(
"project-scoped tools available for {lang} but root_path is None; \
skipping (index was not fetched with ?details=true — \
C# diagnostics will be empty until root_path is available)"
);
continue;
}
};
let rel_real_pairs: Vec<(String, String)> = file_pairs
.iter()
.filter_map(|(rel, _)| {
let real = Path::new(root).join(rel);
if real.exists() {
Some((rel.clone(), real.to_string_lossy().into_owned()))
} else {
None
}
})
.collect();
if rel_real_pairs.is_empty() {
tracing::debug!(
"project-scoped tools for {lang}: no files exist under root {root}; skipping"
);
continue;
}
let real_paths: Vec<std::path::PathBuf> = rel_real_pairs
.iter()
.map(|(_, real)| std::path::PathBuf::from(real))
.collect();
for tool in &proj_tools {
tools_run_set.insert(tool.name().to_string());
match tool.run_project(&real_paths) {
Ok(diags) => {
for mut diag in diags {
match abs_to_rel(&diag.file, &rel_real_pairs) {
Some(rel) => {
diag.file = rel.to_string();
out.push(diag);
}
None => {
tracing::debug!(
"dropping project-scoped diag for unmapped file: {}",
diag.file
);
}
}
}
}
Err(e) => {
tracing::warn!("project-scoped diagnostics ({}) failed: {e:#}", tool.name())
}
}
}
}
let mut tools_run: Vec<String> = tools_run_set.into_iter().collect();
tools_run.sort();
DiagnosticsReport {
tools_run,
tools_unavailable,
diagnostics: out,
}
}
#[cfg(test)]
#[path = "diagnostics_dispatch_tests.rs"]
mod tests;