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)?.0)
73}
74
75pub(crate) fn analyze_for_cli(
76 definitions: Vec<StepDefinition>,
77 feature_steps: Vec<FeatureStep>,
78 config: &Config,
79) -> Result<(
80 AnalysisOutcome,
81 AnalysisCensus,
82 suppression::UnmatchedSuppressionOutcome,
83)> {
84 analyze_internal(definitions, feature_steps, config, true)
85}
86
87fn analyze_internal(
88 definitions: Vec<StepDefinition>,
89 feature_steps: Vec<FeatureStep>,
90 config: &Config,
91 include_unmatched_suppressions: bool,
92) -> Result<(
93 AnalysisOutcome,
94 AnalysisCensus,
95 suppression::UnmatchedSuppressionOutcome,
96)> {
97 config.validate_analysis_limits()?;
98 let mut findings = Vec::new();
99 let suppressions = suppression::SuppressionIndex::new(config, &definitions);
100 let pair_analysis =
101 pairs::analyze_definition_pairs(&definitions, config, &suppressions, &mut findings);
102 let overlap_budget = config
103 .max_candidate_comparisons
104 .saturating_sub(pair_analysis.census.candidate_comparisons_evaluated);
105 let usage = usage::analyze_feature_usage(
106 &definitions,
107 &feature_steps,
108 config,
109 &suppressions,
110 &mut findings,
111 overlap_budget,
112 );
113 usage::analyze_unused(
114 &definitions,
115 &usage.used,
116 config,
117 &suppressions,
118 &mut findings,
119 );
120 let unmatched_suppressions = if include_unmatched_suppressions {
121 suppressions.unmatched()
122 } else {
123 suppression::UnmatchedSuppressionOutcome::default()
124 };
125 findings.sort_by(|left, right| {
126 left.primary
127 .path
128 .cmp(&right.primary.path)
129 .then(left.primary.line.cmp(&right.primary.line))
130 .then(left.primary.column.cmp(&right.primary.column))
131 .then(left.rule.cmp(&right.rule))
132 });
133 let mut census = pair_analysis.census;
134 census.candidate_comparisons_evaluated = census
135 .candidate_comparisons_evaluated
136 .saturating_add(usage.overlap_census.evaluated);
137 census.skipped_candidate_comparisons = census
138 .skipped_candidate_comparisons
139 .saturating_add(usage.overlap_census.skipped);
140 census.truncated |= usage.overlap_census.skipped > 0;
141 census
142 .candidate_sources
143 .insert("matcherOverlap".to_owned(), usage.overlap_census);
144 let mut incomplete = usage.incomplete;
145 incomplete.extend(pair_analysis.incomplete);
146 Ok((
147 AnalysisOutcome {
148 result: AnalysisResult {
149 definitions,
150 feature_steps,
151 findings,
152 },
153 incomplete,
154 },
155 census,
156 unmatched_suppressions,
157 ))
158}
159
160#[cfg(test)]
161mod tests;