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