Skip to main content

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}