Skip to main content

cuke_dedup/reporters/
context.rs

1use crate::analysis::AnalysisCensus;
2use crate::model::{AnalysisResult, DuplicationThreshold, Severity};
3use serde::Serialize;
4use std::collections::BTreeMap;
5use std::path::Path;
6
7/// Measured execution phases and discovered input counts for one CLI run.
8#[non_exhaustive]
9#[derive(Debug, Clone, PartialEq, Serialize)]
10#[serde(rename_all = "camelCase")]
11pub struct ExecutionMetrics {
12    /// Number of definition source files selected by discovery.
13    pub definition_files: usize,
14    /// Number of feature files selected by discovery.
15    pub feature_files: usize,
16    /// Total number of definition and feature files selected by discovery.
17    pub files_discovered: usize,
18    /// Time spent walking and classifying input files.
19    pub discovery_ms: f64,
20    /// Time spent parsing definition and feature sources.
21    pub parsing_ms: f64,
22    /// Time spent evaluating analysis rules.
23    pub analysis_ms: f64,
24}
25
26impl ExecutionMetrics {
27    /// Creates execution metrics and derives the combined discovered-file count.
28    pub fn new(
29        definition_files: usize,
30        feature_files: usize,
31        discovery_ms: f64,
32        parsing_ms: f64,
33        analysis_ms: f64,
34    ) -> Self {
35        Self {
36            definition_files,
37            feature_files,
38            files_discovered: definition_files.saturating_add(feature_files),
39            discovery_ms,
40            parsing_ms,
41            analysis_ms,
42        }
43    }
44}
45
46/// Deterministic input and extraction counts for one CLI run.
47#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
48#[serde(rename_all = "camelCase")]
49pub(crate) struct CorpusCensus {
50    /// Number of definition source files selected by discovery.
51    pub(crate) definition_files: usize,
52    /// Number of selected definition files that yielded at least one definition.
53    pub(crate) definition_files_with_definitions: usize,
54    /// Number of step definitions extracted before analysis and mode filtering.
55    pub(crate) definitions_extracted: usize,
56    /// Number of feature files selected by discovery.
57    pub(crate) feature_files: usize,
58    /// Number of selected feature files parsed successfully.
59    pub(crate) feature_files_parsed: usize,
60    /// Number of converted Gherkin Markdown files that parsed but yielded no concrete steps.
61    pub(crate) feature_files_without_steps: usize,
62    /// Whether definitions or feature steps are known to be missing from the extracted corpus.
63    ///
64    /// Set when a source imported a known registration through a module that could not be
65    /// resolved statically, or when Gherkin Markdown conversion yielded no steps. Those inputs
66    /// are absent from one or more rules, so the corpus — not just the comparison set — is a
67    /// subset of what a complete run would analyze.
68    pub(crate) incomplete: bool,
69}
70
71pub(crate) struct CliReportMetadata<'a> {
72    pub(crate) corpus: &'a CorpusCensus,
73    pub(crate) analysis: &'a AnalysisCensus,
74    pub(crate) execution_successful: bool,
75}
76
77#[derive(Debug, Clone, Serialize)]
78#[serde(rename_all = "camelCase")]
79/// Aggregate counts shared by every report format.
80#[non_exhaustive]
81pub struct Summary {
82    /// Number of extracted step definitions.
83    pub definitions_analyzed: usize,
84    /// Number of concrete feature steps.
85    pub feature_steps_analyzed: usize,
86    /// Number of active findings.
87    pub findings: usize,
88    /// Number of active error findings.
89    pub errors: usize,
90    /// Number of active warning findings.
91    pub warnings: usize,
92    /// Number of suppressed findings.
93    pub suppressed: usize,
94    /// Active findings grouped by stable rule name.
95    pub by_rule: BTreeMap<String, usize>,
96    /// Percentage-based duplication quality-gate result.
97    pub duplication: DuplicationThreshold,
98}
99
100impl Summary {
101    /// Computes report totals using the strict default duplication threshold.
102    pub fn from_result(result: &AnalysisResult) -> Self {
103        Self::from_result_with_threshold(result, 0.0)
104    }
105
106    /// Computes report totals using `threshold` as the allowed duplication percentage.
107    pub fn from_result_with_threshold(result: &AnalysisResult, threshold: f64) -> Self {
108        let active: Vec<_> = result
109            .findings
110            .iter()
111            .filter(|finding| finding.is_active())
112            .collect();
113        let mut by_rule = BTreeMap::new();
114        for finding in &active {
115            *by_rule.entry(finding.rule.to_string()).or_insert(0) += 1;
116        }
117        Self {
118            definitions_analyzed: result.definitions.len(),
119            feature_steps_analyzed: result.feature_steps.len(),
120            findings: active.len(),
121            errors: active
122                .iter()
123                .filter(|finding| finding.severity == Severity::Error)
124                .count(),
125            warnings: active
126                .iter()
127                .filter(|finding| finding.severity == Severity::Warning)
128                .count(),
129            suppressed: result
130                .findings
131                .iter()
132                .filter(|finding| finding.suppression.is_some())
133                .count(),
134            by_rule,
135            duplication: DuplicationThreshold::from_result(result, threshold),
136        }
137    }
138}
139
140/// Shared, precomputed input passed to every report renderer.
141#[non_exhaustive]
142pub struct ReportContext<'a> {
143    /// Complete analysis result, including suppressed findings.
144    pub result: &'a AnalysisResult,
145    /// Repository root used to relativize source locations.
146    pub root: &'a Path,
147    /// Configured duplication percentage threshold.
148    pub threshold: f64,
149    /// Aggregate counts shared across report formats.
150    pub summary: Summary,
151    /// Version of CukeDedup that produced the report.
152    pub tool_version: &'static str,
153    /// Optional CLI execution measurements.
154    pub metrics: Option<&'a ExecutionMetrics>,
155}
156
157impl<'a> ReportContext<'a> {
158    /// Creates a context and computes its shared summary once.
159    pub fn new(result: &'a AnalysisResult, root: &'a Path, threshold: f64) -> Self {
160        Self {
161            result,
162            root,
163            threshold,
164            summary: Summary::from_result_with_threshold(result, threshold),
165            tool_version: env!("CARGO_PKG_VERSION"),
166            metrics: None,
167        }
168    }
169
170    /// Creates a report context containing measured CLI execution phases.
171    pub fn with_metrics(
172        result: &'a AnalysisResult,
173        root: &'a Path,
174        threshold: f64,
175        metrics: &'a ExecutionMetrics,
176    ) -> Self {
177        Self {
178            metrics: Some(metrics),
179            ..Self::new(result, root, threshold)
180        }
181    }
182}