use std::path::Path;
use std::sync::{Arc, OnceLock};
use dashmap::DashMap;
use crate::core::tool_impls::{
BiomeTool, ClangtidyTool, ClippyTool, DetektTool, PhpstanTool, PmdTool, RubocopTool, RuffTool,
StaticcheckTool, SwiftlintTool,
};
use crate::core::tools::{StaticTool, ToolDiagnostic};
pub struct ToolRegistry {
tools: DashMap<String, Vec<Arc<dyn StaticTool>>>,
}
impl ToolRegistry {
pub fn discover() -> Self {
let registry = ToolRegistry {
tools: DashMap::new(),
};
let all_tools: Vec<Arc<dyn StaticTool>> = vec![
Arc::new(ClippyTool),
Arc::new(RuffTool),
Arc::new(BiomeTool),
Arc::new(StaticcheckTool),
Arc::new(PmdTool),
Arc::new(RubocopTool),
Arc::new(PhpstanTool),
Arc::new(SwiftlintTool),
Arc::new(DetektTool),
Arc::new(ClangtidyTool),
];
for tool in all_tools {
if tool.is_available() {
tracing::debug!(
tool = tool.name(),
language = tool.language(),
"static tool available"
);
registry
.tools
.entry(tool.language().to_string())
.or_default()
.push(tool);
} else {
tracing::debug!(tool = tool.name(), "static tool not available");
}
}
registry
}
pub fn tools_for(&self, lang: &str) -> Vec<Arc<dyn StaticTool>> {
self.tools
.get(lang)
.map(|entry| entry.clone())
.unwrap_or_default()
}
pub fn languages(&self) -> Vec<String> {
self.tools.iter().map(|e| e.key().clone()).collect()
}
pub fn run_all(
&self,
lang: &str,
file: &Path,
content: &str,
) -> anyhow::Result<Vec<ToolDiagnostic>> {
let mut merged = Vec::new();
for tool in self.tools_for(lang) {
match tool.run(file, content) {
Ok(diags) => merged.extend(diags),
Err(e) => {
tracing::warn!(tool = tool.name(), "tool run failed: {e:#}");
}
}
}
Ok(merged)
}
pub fn run_named(
&self,
lang: &str,
names: &[String],
file: &Path,
content: &str,
) -> anyhow::Result<Vec<ToolDiagnostic>> {
let mut merged = Vec::new();
for tool in self.tools_for(lang) {
if !names.iter().any(|n| n == tool.name()) {
continue;
}
match tool.run(file, content) {
Ok(diags) => merged.extend(diags),
Err(e) => {
tracing::warn!(tool = tool.name(), "tool run failed: {e:#}");
}
}
}
Ok(merged)
}
}
static GLOBAL_REGISTRY: OnceLock<ToolRegistry> = OnceLock::new();
pub fn global_registry() -> &'static ToolRegistry {
GLOBAL_REGISTRY.get_or_init(ToolRegistry::discover)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn discover_does_not_panic() {
let r = ToolRegistry::discover();
for lang in r.languages() {
assert!(!r.tools_for(&lang).is_empty());
}
}
#[test]
fn run_all_unknown_language_is_empty() {
let r = ToolRegistry::discover();
let diags = r
.run_all("klingon", Path::new("foo.kl"), "")
.expect("run_all should not fail");
assert!(diags.is_empty());
}
#[test]
fn global_registry_is_stable() {
let a = global_registry() as *const ToolRegistry;
let b = global_registry() as *const ToolRegistry;
assert_eq!(a, b, "global registry must be a singleton");
}
}