mod evidence;
mod pairs;
mod similarity;
mod suppression;
mod usage;
use crate::config::Config;
use crate::model::{AnalysisResult, FeatureStep, StepDefinition};
use anyhow::{bail, Result};
use serde::Serialize;
use std::collections::BTreeMap;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct CandidateSourceCensus {
pub(crate) evaluated: usize,
pub(crate) skipped: u64,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct AnalysisCensus {
pub(crate) truncated: bool,
pub(crate) candidate_comparisons_evaluated: usize,
pub(crate) skipped_candidate_comparisons: u64,
pub(crate) truncated_structural_classes: usize,
pub(crate) candidate_sources: BTreeMap<String, CandidateSourceCensus>,
}
#[derive(Debug)]
#[non_exhaustive]
pub struct AnalysisOutcome {
pub result: AnalysisResult,
pub incomplete: Vec<String>,
}
pub fn analyze(
definitions: Vec<StepDefinition>,
feature_steps: Vec<FeatureStep>,
config: &Config,
) -> Result<AnalysisResult> {
let outcome = analyze_with_diagnostics(definitions, feature_steps, config)?;
if !outcome.incomplete.is_empty() {
bail!(outcome.incomplete.join("; "));
}
Ok(outcome.result)
}
pub fn analyze_with_diagnostics(
definitions: Vec<StepDefinition>,
feature_steps: Vec<FeatureStep>,
config: &Config,
) -> Result<AnalysisOutcome> {
Ok(analyze_internal(definitions, feature_steps, config, false, None)?.0)
}
pub fn analyze_with_step_usage(
definitions: Vec<StepDefinition>,
feature_steps: Vec<FeatureStep>,
config: &Config,
usage: &crate::source_adapter::IndirectStepUsage,
) -> Result<AnalysisOutcome> {
Ok(analyze_internal(definitions, feature_steps, config, false, Some(usage))?.0)
}
pub(crate) fn analyze_for_cli(
indirect_usage: &crate::source_adapter::IndirectStepUsage,
definitions: Vec<StepDefinition>,
feature_steps: Vec<FeatureStep>,
config: &Config,
) -> Result<(
AnalysisOutcome,
AnalysisCensus,
suppression::UnmatchedSuppressionOutcome,
)> {
analyze_internal(
definitions,
feature_steps,
config,
true,
Some(indirect_usage),
)
}
fn analyze_internal(
definitions: Vec<StepDefinition>,
feature_steps: Vec<FeatureStep>,
config: &Config,
include_unmatched_suppressions: bool,
indirect_usage: Option<&crate::source_adapter::IndirectStepUsage>,
) -> Result<(
AnalysisOutcome,
AnalysisCensus,
suppression::UnmatchedSuppressionOutcome,
)> {
config.validate_analysis_limits()?;
let mut findings = Vec::new();
if let Some(first) = definitions.first() {
let domain = first.analysis_profile().comparison_domain;
if definitions
.iter()
.any(|d| d.analysis_profile().comparison_domain != domain)
{
bail!("Ruby and JS/TS definitions require separate analysis runs with explicit definition and feature roots");
}
}
let suppressions = suppression::SuppressionIndex::new(config, &definitions);
let pair_analysis =
pairs::analyze_definition_pairs(&definitions, config, &suppressions, &mut findings);
let overlap_budget = config
.max_candidate_comparisons
.saturating_sub(pair_analysis.census.candidate_comparisons_evaluated);
let mut usage = usage::analyze_feature_usage(
&definitions,
&feature_steps,
config,
&suppressions,
&mut findings,
overlap_budget,
);
usage::apply_indirect_usage(&definitions, indirect_usage, config, &mut usage);
usage::analyze_unused(
&definitions,
&usage.used,
config,
&suppressions,
&mut findings,
);
let unmatched_suppressions = if include_unmatched_suppressions {
suppressions.unmatched()
} else {
suppression::UnmatchedSuppressionOutcome::default()
};
findings.sort_by(|left, right| {
left.primary
.path
.cmp(&right.primary.path)
.then(left.primary.line.cmp(&right.primary.line))
.then(left.primary.column.cmp(&right.primary.column))
.then(left.rule.cmp(&right.rule))
});
let mut census = pair_analysis.census;
census.candidate_comparisons_evaluated = census
.candidate_comparisons_evaluated
.saturating_add(usage.overlap_census.evaluated);
census.skipped_candidate_comparisons = census
.skipped_candidate_comparisons
.saturating_add(usage.overlap_census.skipped);
census.truncated |= usage.overlap_census.skipped > 0;
census
.candidate_sources
.insert("matcherOverlap".to_owned(), usage.overlap_census);
let mut incomplete = usage.incomplete;
incomplete.extend(pair_analysis.incomplete);
Ok((
AnalysisOutcome {
result: AnalysisResult {
definitions,
feature_steps,
findings,
},
incomplete,
},
census,
unmatched_suppressions,
))
}
#[cfg(test)]
mod tests;