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