crap_core/ports/mod.rs
1use crate::domain::types::{
2 BranchCoverage, ComplexityMetric, CrapError, FileChangeKind, FunctionComplexity, LineCoverage,
3};
4use serde::Serialize;
5use serde::de::DeserializeOwned;
6use std::collections::HashMap;
7use std::fmt::Debug;
8use std::path::Path;
9
10/// Port for extracting per-function complexity from source code.
11pub trait ComplexityPort {
12 fn extract(
13 &self,
14 source: &str,
15 file_path: &str,
16 metric: ComplexityMetric,
17 ) -> Result<Vec<FunctionComplexity>, CrapError>;
18}
19
20/// Trait implemented by adapter-specific parse diagnostic types.
21///
22/// `LcovParseDiagnostic` (in `crap4rs`) is the v0.5 LCOV implementation;
23/// future siblings (`crap4ts`'s Istanbul adapter) will define their own
24/// concrete type implementing this trait. Bounds are minimal but
25/// load-bearing: `Serialize + DeserializeOwned` lets `AnalysisDiagnostics<P>`
26/// and `ParseOutput<P>` keep their auto-derived serde shapes; `Debug + Clone`
27/// match the rest of the domain's pervasive derive set. The trait itself
28/// is not object-safe (`Clone` requires `Sized`); object safety is enforced
29/// one layer up on `&dyn CoveragePort<Diagnostic = …>` where the caller
30/// fixes the associated type. See ADR D9 (mixed-dispatch-strategy).
31pub trait ParseDiagnostic: Debug + Clone + Serialize + DeserializeOwned {}
32
33/// Result of parsing coverage data: coverage map + non-fatal diagnostics.
34///
35/// Generic over `P: ParseDiagnostic` so the LCOV adapter (`crap4rs`),
36/// the future Istanbul adapter (`crap4ts`), and any sibling adapter can
37/// thread their own diagnostic shape through one shared analyzer
38/// pipeline. Per ADR D9, `Vec<P>` storage of potentially thousands of
39/// parse diagnostics avoids the per-element `Box<dyn ParseDiagnostic>`
40/// heap-allocation cost.
41#[derive(Debug)]
42pub struct ParseOutput<P: ParseDiagnostic> {
43 pub coverage: HashMap<String, Vec<LineCoverage>>,
44 /// Branch coverage data from BRDA records, keyed by file path.
45 /// `None` when no BRDA records were encountered in the entire input.
46 pub branches: Option<HashMap<String, Vec<BranchCoverage>>>,
47 pub diagnostics: Vec<P>,
48}
49
50/// Port for parsing coverage data into per-file, per-line hit counts.
51///
52/// `Diagnostic` associated type lets concrete impls fix their parse
53/// diagnostic shape (`LcovParseDiagnostic` for the Rust adapter). The
54/// trait is dyn-compatible when callers fix the associated type at the
55/// dyn site: `&dyn CoveragePort<Diagnostic = LcovParseDiagnostic>`.
56/// See ADR D9 for the mixed-dispatch rationale.
57pub trait CoveragePort {
58 type Diagnostic: ParseDiagnostic;
59
60 /// Parse the coverage file at `path` into per-file line / branch
61 /// hit data.
62 ///
63 /// Takes `&Path` (not `&str`) so each impl owns its read strategy:
64 /// LCOV stays free to stream line-by-line via `BufReader`, Istanbul
65 /// JSON slurps once and hands the buffer to `serde_json`. Forcing
66 /// the caller to pre-read into a `String` would (a) require slurp
67 /// universally even when streaming is viable and (b) double peak
68 /// RSS on every invocation since [`Self::validate`] already takes
69 /// `&Path` and may stream the same file. Per the
70 /// `feedback_trait_io_input_path_over_str` operational rule, port
71 /// methods that gate on file content take `&Path`.
72 fn parse(&self, path: &Path) -> Result<ParseOutput<Self::Diagnostic>, CrapError>;
73
74 /// Adapter-aware pre-flight check: does the file at `path` contain
75 /// at least one usable coverage record? Runs before the analyzer
76 /// dispatches to `parse`, so the CLI can surface a helpful "your
77 /// coverage file has no data points" diagnostic before the full
78 /// parse pass.
79 ///
80 /// Takes `&Path` (not `&str`) so adapters can stream the file
81 /// line-by-line and short-circuit on the first valid record — the
82 /// LCOV format easily exceeds 100 MB on large workspaces and
83 /// loading the whole file into memory twice (once here, once in
84 /// `parse`) would double peak RSS for no semantic gain. Adapters
85 /// that need random access (Istanbul JSON) can still slurp via
86 /// `read_to_string` internally.
87 ///
88 /// Default implementation returns `Ok(())` (skip validation) —
89 /// adapters with a cheap structural signature override. The LCOV
90 /// adapter checks for `SF:` + `DA:` records; the Istanbul adapter
91 /// checks for at least one entry with a non-empty `statementMap`.
92 ///
93 /// The returned `Err(String)` is the structural reason
94 /// (e.g., `"no SF/DA records"`); the CLI layer pairs it with the
95 /// coverage path and adapter-specific generation hint
96 /// (`AdapterMeta::coverage_hint`) before surfacing to the user.
97 fn validate(&self, _path: &Path) -> Result<(), String> {
98 Ok(())
99 }
100}
101
102/// Port for computing which files/regions changed relative to a git ref.
103pub trait DiffPort {
104 fn changed_regions(
105 &self,
106 diff_ref: &str,
107 working_dir: &Path,
108 paths: &[String],
109 ) -> Result<HashMap<String, FileChangeKind>, CrapError>;
110}
111
112#[cfg(test)]
113mod object_safety {
114 //! Compile-fence: locks `&dyn CoveragePort<Diagnostic = ...>` and
115 //! `&dyn ComplexityPort` as object-safe, per ADR D9. If a future
116 //! port method becomes generic, returns `impl Trait`, or takes
117 //! `Self`, this test fails to compile and the dispatch contract
118 //! is the loud failure point.
119 use super::*;
120 use crate::test_strategies::DummyParseDiagnostic;
121
122 #[allow(dead_code)]
123 fn _coverage_port_is_dyn_safe(_port: &dyn CoveragePort<Diagnostic = DummyParseDiagnostic>) {}
124
125 #[allow(dead_code)]
126 fn _complexity_port_is_dyn_safe(_port: &dyn ComplexityPort) {}
127}