1mod evidence;
4mod pairs;
5mod similarity;
6mod string_metrics;
7mod suppression;
8mod usage;
9
10use crate::config::Config;
11use crate::model::{AnalysisResult, FeatureStep, StepDefinition};
12use anyhow::{bail, Result};
13use serde::Serialize;
14use std::collections::BTreeMap;
15
16#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
17#[serde(rename_all = "camelCase")]
18pub(crate) struct CandidateSourceCensus {
19 pub(crate) evaluated: usize,
20 pub(crate) skipped: u64,
21}
22
23#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
24#[serde(rename_all = "camelCase")]
25pub(crate) struct AnalysisCensus {
26 pub(crate) truncated: bool,
27 pub(crate) candidate_comparisons_evaluated: usize,
28 pub(crate) skipped_candidate_comparisons: u64,
29 pub(crate) truncated_structural_classes: usize,
30 pub(crate) candidate_sources: BTreeMap<String, CandidateSourceCensus>,
31}
32
33#[derive(Debug)]
34#[non_exhaustive]
39pub struct AnalysisOutcome {
40 pub result: AnalysisResult,
42 pub incomplete: Vec<String>,
47}
48
49pub fn analyze(
51 definitions: Vec<StepDefinition>,
52 feature_steps: Vec<FeatureStep>,
53 config: &Config,
54) -> Result<AnalysisResult> {
55 let outcome = analyze_with_diagnostics(definitions, feature_steps, config)?;
56 if !outcome.incomplete.is_empty() {
57 bail!(outcome.incomplete.join("; "));
58 }
59 Ok(outcome.result)
60}
61
62pub fn analyze_with_diagnostics(
69 definitions: Vec<StepDefinition>,
70 feature_steps: Vec<FeatureStep>,
71 config: &Config,
72) -> Result<AnalysisOutcome> {
73 Ok(analyze_internal(definitions, feature_steps, config, false, None)?.0)
74}
75
76pub fn analyze_with_step_usage(
79 definitions: Vec<StepDefinition>,
80 feature_steps: Vec<FeatureStep>,
81 config: &Config,
82 usage: &crate::source_adapter::IndirectStepUsage,
83) -> Result<AnalysisOutcome> {
84 Ok(analyze_internal(definitions, feature_steps, config, false, Some(usage))?.0)
85}
86
87pub(crate) fn analyze_for_cli(
90 indirect_usage: &crate::source_adapter::IndirectStepUsage,
91 definitions: Vec<StepDefinition>,
92 feature_steps: Vec<FeatureStep>,
93 config: &Config,
94) -> Result<(
95 AnalysisOutcome,
96 AnalysisCensus,
97 suppression::UnmatchedSuppressionOutcome,
98)> {
99 analyze_internal(
100 definitions,
101 feature_steps,
102 config,
103 true,
104 Some(indirect_usage),
105 )
106}
107
108fn analyze_internal(
114 definitions: Vec<StepDefinition>,
115 feature_steps: Vec<FeatureStep>,
116 config: &Config,
117 include_unmatched_suppressions: bool,
118 indirect_usage: Option<&crate::source_adapter::IndirectStepUsage>,
119) -> Result<(
120 AnalysisOutcome,
121 AnalysisCensus,
122 suppression::UnmatchedSuppressionOutcome,
123)> {
124 config.validate_analysis_limits()?;
125 let mut findings = Vec::new();
126 if let Some(first) = definitions.first() {
127 let domain = first.analysis_profile().comparison_domain;
128 if definitions
129 .iter()
130 .any(|d| d.analysis_profile().comparison_domain != domain)
131 {
132 bail!("Ruby and JS/TS definitions require separate analysis runs with explicit definition and feature roots");
133 }
134 }
135 let suppressions = suppression::SuppressionIndex::new(config, &definitions);
136 let pair_analysis =
137 pairs::analyze_definition_pairs(&definitions, config, &suppressions, &mut findings);
138 let overlap_budget = config
139 .max_candidate_comparisons
140 .saturating_sub(pair_analysis.census.candidate_comparisons_evaluated);
141 let mut usage = usage::analyze_feature_usage(
142 &definitions,
143 &feature_steps,
144 config,
145 &suppressions,
146 &mut findings,
147 overlap_budget,
148 );
149 usage::apply_indirect_usage(&definitions, indirect_usage, config, &mut usage);
150 usage::analyze_unused(
151 &definitions,
152 &usage.used,
153 config,
154 &suppressions,
155 &mut findings,
156 );
157 let unmatched_suppressions = if include_unmatched_suppressions {
158 suppressions.unmatched()
159 } else {
160 suppression::UnmatchedSuppressionOutcome::default()
161 };
162 findings.sort_by(|left, right| {
163 left.primary
164 .path
165 .cmp(&right.primary.path)
166 .then(left.primary.line.cmp(&right.primary.line))
167 .then(left.primary.column.cmp(&right.primary.column))
168 .then(left.rule.cmp(&right.rule))
169 });
170 let mut census = pair_analysis.census;
171 census.candidate_comparisons_evaluated = census
172 .candidate_comparisons_evaluated
173 .saturating_add(usage.overlap_census.evaluated);
174 census.skipped_candidate_comparisons = census
175 .skipped_candidate_comparisons
176 .saturating_add(usage.overlap_census.skipped);
177 census.truncated |= usage.overlap_census.skipped > 0;
178 census
179 .candidate_sources
180 .insert("matcherOverlap".to_owned(), usage.overlap_census);
181 let mut incomplete = usage.incomplete;
182 incomplete.extend(pair_analysis.incomplete);
183 Ok((
184 AnalysisOutcome {
185 result: AnalysisResult {
186 definitions,
187 feature_steps,
188 findings,
189 },
190 incomplete,
191 },
192 census,
193 unmatched_suppressions,
194 ))
195}
196
197#[cfg(test)]
198mod tests;