use crate::application::rules::catalog;
use crate::application::suppress::{is_suppressed, parse_inline_suppressions};
use crate::domain::report::{AnalysisReport, SkippedRule};
use crate::domain::request::AnalysisRequest;
use crate::domain::rule::Rule;
use crate::domain::types::{LanguageId, SourceFile};
use crate::ports::inbound::{AnalysisService, LanguageAdapter, LanguageRegistry};
use crate::ports::outbound::{SourceLoader, WorkspaceDiscovery};
use std::time::Instant;
pub struct AdapterRegistry {
adapters: Vec<Box<dyn LanguageAdapter>>,
}
impl AdapterRegistry {
pub fn new(adapters: Vec<Box<dyn LanguageAdapter>>) -> Self {
Self { adapters }
}
pub fn with_defaults() -> Self {
Self::new(vec![Box::new(
crate::languages::rust::RustAdapter::default(),
)])
}
}
impl LanguageRegistry for AdapterRegistry {
fn supported(&self) -> Vec<LanguageId> {
self.adapters.iter().map(|a| a.id()).collect()
}
fn get(&self, id: &LanguageId) -> Option<&dyn LanguageAdapter> {
self.adapters
.iter()
.find(|a| &a.id() == id)
.map(|a| a.as_ref())
}
}
pub struct DefaultAnalysisService {
registry: AdapterRegistry,
discovery: Box<dyn WorkspaceDiscovery>,
loader: Box<dyn SourceLoader>,
}
impl DefaultAnalysisService {
pub fn new(
registry: AdapterRegistry,
discovery: Box<dyn WorkspaceDiscovery>,
loader: Box<dyn SourceLoader>,
) -> Self {
Self {
registry,
discovery,
loader,
}
}
pub fn with_defaults() -> Self {
Self::new(
AdapterRegistry::with_defaults(),
Box::new(crate::adapters::filesystem::IgnoreWalkDiscovery::default()),
Box::new(crate::adapters::filesystem::FsSourceLoader),
)
}
fn languages_for_request(&self, request: &AnalysisRequest) -> Vec<LanguageId> {
if let Some(ref only) = request.language_filter {
return vec![only.clone()];
}
if !request.config.languages.is_empty() {
return request.config.languages.clone();
}
self.registry.supported()
}
fn select_rules(
&self,
adapter: &dyn LanguageAdapter,
request: &AnalysisRequest,
skipped: &mut Vec<SkippedRule>,
) -> Vec<Box<dyn Rule>> {
let caps = adapter.capabilities();
let mut selected = Vec::new();
for rule in catalog() {
let meta = rule.meta();
if !request.config.rules.allows(&meta.id) {
continue;
}
if !caps.is_superset_of(&meta.required_capabilities) {
skipped.push(SkippedRule {
rule_id: meta.id.clone(),
reason: format!(
"adapter `{}` missing required capabilities for rule",
adapter.id()
),
});
continue;
}
selected.push(rule);
}
selected
}
}
impl AnalysisService for DefaultAnalysisService {
fn analyze(&self, request: &AnalysisRequest) -> Result<AnalysisReport, String> {
let started = Instant::now();
let languages = self.languages_for_request(request);
let mut exclusions = request.config.path_exclusions.clone();
exclusions.extend(request.config.generated_exclusions.iter().cloned());
let mut paths = if request.paths.is_empty() {
self.discovery
.discover(&request.root, &languages, &exclusions)?
} else {
request.paths.clone()
};
let _ = request.workspace;
paths.sort();
paths.dedup();
let mut report = AnalysisReport {
findings: Vec::new(),
files_analyzed: 0,
languages: languages.clone(),
duration_ms: 0,
skipped_rules: Vec::new(),
stats: Default::default(),
};
for lang in &languages {
let Some(adapter) = self.registry.get(lang) else {
continue;
};
let mut sources: Vec<SourceFile> = Vec::new();
for path in &paths {
if !adapter.detect(path) {
continue;
}
match self.loader.load(path, lang.clone()) {
Ok(src) => sources.push(src),
Err(e) => {
let _ = e;
}
}
}
report.files_analyzed += sources.len();
let mut config = request.config.clone();
for src in &sources {
let inline = parse_inline_suppressions(&src.content, &src.path);
config.suppressions.extend(inline);
}
let model = adapter.build_model(&sources)?;
let rules = self.select_rules(adapter, request, &mut report.skipped_rules);
let mut findings = adapter.analyze_language(&model, &rules);
findings.extend(adapter.language_specific_findings(&model));
for f in findings {
if is_suppressed(&f, &config) {
continue;
}
if let Some(min) = request.config.min_severity.or(request.config.rules.min_severity)
{
if f.severity < min {
continue;
}
}
report.findings.push(f);
}
}
report.sort_stable();
report.recompute_stats();
report.duration_ms = started.elapsed().as_millis() as u64;
Ok(report)
}
}
pub struct AnalysisEngine {
service: DefaultAnalysisService,
}
impl AnalysisEngine {
pub fn new() -> Self {
Self {
service: DefaultAnalysisService::with_defaults(),
}
}
pub fn analyze(&self, request: &AnalysisRequest) -> Result<AnalysisReport, String> {
self.service.analyze(request)
}
pub fn supported_languages(&self) -> Vec<LanguageId> {
self.service.registry.supported()
}
}
impl Default for AnalysisEngine {
fn default() -> Self {
Self::new()
}
}