use std::path::Path;
use std::sync::{Arc, OnceLock};
use dashmap::DashMap;
use crate::core::tool_impls::{
BiomeTool, ClangtidyTool, ClippyTool, DetektTool, PhpstanTool, PmdTool, RoslynTool,
RubocopTool, RuffTool, StaticcheckTool, SwiftlintTool,
};
use crate::core::tools::{StaticTool, ToolDiagnostic};
pub struct ToolRegistry {
tools: DashMap<String, Vec<Arc<dyn StaticTool>>>,
unavailable_names: Vec<String>,
}
impl ToolRegistry {
pub fn discover() -> Self {
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),
Arc::new(RoslynTool),
];
Self::from_tools(all_tools)
}
pub fn from_tools_for_test(all_tools: Vec<Arc<dyn StaticTool>>) -> Self {
Self::from_tools(all_tools)
}
fn from_tools(all_tools: Vec<Arc<dyn StaticTool>>) -> Self {
let mut unavailable_names = Vec::new();
let registry_tools = DashMap::new();
for tool in all_tools {
if tool.is_available() {
tracing::debug!(
tool = tool.name(),
language = tool.language(),
"static tool available"
);
for lang in std::iter::once(tool.language()).chain(tool.aliases().iter().copied()) {
registry_tools
.entry(lang.to_string())
.or_insert_with(Vec::new)
.push(Arc::clone(&tool));
}
} else {
tracing::debug!(tool = tool.name(), "static tool not available");
unavailable_names.push(tool.name().to_string());
}
}
ToolRegistry {
tools: registry_tools,
unavailable_names,
}
}
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 unavailable_names(&self) -> &[String] {
&self.unavailable_names
}
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) {
if tool.is_project_scoped() {
tracing::debug!(
tool = tool.name(),
"skipping project-scoped tool in run_all — \
use run_diagnostics_blocking for project-scoped dispatch"
);
continue;
}
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;
}
if tool.is_project_scoped() {
tracing::debug!(
tool = tool.name(),
"skipping project-scoped tool in run_named — \
use run_diagnostics_blocking for project-scoped dispatch"
);
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 run_all_skips_project_scoped_tools() {
let r = ToolRegistry::discover();
let scratch = Path::new("/tmp/test_dummy.cs");
let result = r.run_all("csharp", scratch, "class Foo {}");
assert!(
result.is_ok(),
"run_all must not fail for project-scoped language: {result:?}"
);
}
#[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");
}
struct FakeAliasedTool;
impl StaticTool for FakeAliasedTool {
fn name(&self) -> &str {
"fake-aliased"
}
fn language(&self) -> &str {
"typescript"
}
fn aliases(&self) -> &[&str] {
&["javascript"]
}
fn is_available(&self) -> bool {
true
}
fn run(&self, _file: &Path, _content: &str) -> anyhow::Result<Vec<ToolDiagnostic>> {
Ok(Vec::new())
}
}
#[test]
fn from_tools_records_all_unavailable() {
struct AlwaysAvailable;
impl StaticTool for AlwaysAvailable {
fn name(&self) -> &str {
"always-on"
}
fn language(&self) -> &str {
"rust"
}
fn is_available(&self) -> bool {
true
}
fn run(&self, _: &Path, _: &str) -> anyhow::Result<Vec<ToolDiagnostic>> {
Ok(Vec::new())
}
}
struct NeverAvailable;
impl StaticTool for NeverAvailable {
fn name(&self) -> &str {
"never-on"
}
fn language(&self) -> &str {
"python"
}
fn is_available(&self) -> bool {
false
}
fn run(&self, _: &Path, _: &str) -> anyhow::Result<Vec<ToolDiagnostic>> {
Ok(Vec::new())
}
}
let r = ToolRegistry::from_tools(vec![Arc::new(AlwaysAvailable), Arc::new(NeverAvailable)]);
assert_eq!(r.unavailable_names(), &["never-on"]);
assert!(
!r.unavailable_names().contains(&"always-on".to_string()),
"available tool must not appear in unavailable_names"
);
assert_eq!(r.tools_for("rust").len(), 1);
assert!(r.tools_for("python").is_empty());
}
#[test]
fn from_tools_available_only_has_empty_unavailable() {
struct OnlyAvailable;
impl StaticTool for OnlyAvailable {
fn name(&self) -> &str {
"on-tool"
}
fn language(&self) -> &str {
"go"
}
fn is_available(&self) -> bool {
true
}
fn run(&self, _: &Path, _: &str) -> anyhow::Result<Vec<ToolDiagnostic>> {
Ok(Vec::new())
}
}
let r = ToolRegistry::from_tools(vec![Arc::new(OnlyAvailable)]);
assert!(
r.unavailable_names().is_empty(),
"no unavailable tools expected, got: {:?}",
r.unavailable_names()
);
}
#[test]
fn aliases_register_tool_under_every_bucket() {
let r = ToolRegistry::from_tools(vec![Arc::new(FakeAliasedTool)]);
assert_eq!(r.tools_for("typescript").len(), 1, "primary bucket");
assert_eq!(
r.tools_for("javascript").len(),
1,
"alias bucket must be reachable"
);
assert_eq!(r.tools_for("typescript")[0].name(), "fake-aliased");
assert_eq!(r.tools_for("javascript")[0].name(), "fake-aliased");
}
}