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