Skip to main content

klasp_core/
source.rs

1//! `CheckSource` — abstraction over a runnable check.
2//!
3//! Design: [docs/design.md §3.2]. v0.1 ships exactly one impl (`Shell`,
4//! living in the `klasp` binary). The trait exists at v0.1 so v0.2's named
5//! recipes (`pre_commit`, `fallow`, `pytest`, `cargo`) and v0.3's
6//! subprocess plugins land as new impls without touching the trait.
7//!
8//! **Lifetime note**: `source_id(&self) -> &str` returns a `&str` tied to
9//! `&self`, *not* `&'static str`. v0.3 subprocess plugins have IDs derived
10//! from binary filenames discovered at runtime — a `'static` lifetime
11//! cannot represent that. This is the v0.3 plugin commitment locked in
12//! at v0.1. See issue [klasp#1] for the explicit callout.
13
14use std::path::PathBuf;
15
16use crate::config::CheckConfig;
17use crate::trigger::GitEvent;
18use crate::verdict::Verdict;
19
20/// Typed error for `CheckSource::run`. Covers runtime-level failures only;
21/// semantic failures (lint hits, test failures) ride inside `Verdict::Fail`.
22/// Use `CheckSourceError::Other` for impl-specific errors that don't fit the
23/// predefined variants.
24#[derive(Debug, thiserror::Error)]
25pub enum CheckSourceError {
26    #[error("failed to spawn check process: {source}")]
27    Spawn {
28        #[source]
29        source: std::io::Error,
30    },
31
32    #[error("check produced unparseable output: {0}")]
33    Output(String),
34
35    #[error("check exceeded {secs}s timeout")]
36    Timeout { secs: u64 },
37
38    #[error(transparent)]
39    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
40}
41
42/// Snapshot of repo metadata passed to every check execution.
43///
44/// `base_ref` is the merge-base ref between `HEAD` and the upstream tracking
45/// branch — the "branch divergence point" diff-aware tools (`pre-commit
46/// run --from-ref`, `fallow audit --base`) want. The gate runtime exposes it
47/// to shell checks via the `KLASP_BASE_REF` env var; sources that talk to
48/// other check tools (named recipes in v0.2, subprocess plugins in v0.3) read
49/// it directly off this struct.
50///
51/// Falls back to `HEAD~1` when no upstream is configured (a fresh checkout,
52/// a detached HEAD, or a branch that has never been pushed). The fallback is
53/// best-effort — diff-aware tools that don't recognise the ref simply lint
54/// the whole tree, which is the same behaviour they'd have without klasp.
55#[derive(Debug, Clone)]
56pub struct RepoState {
57    pub root: PathBuf,
58    pub git_event: GitEvent,
59    pub base_ref: String,
60}
61
62/// Outcome of a single `CheckSource::run` invocation.
63#[derive(Debug, Clone)]
64pub struct CheckResult {
65    pub source_id: String,
66    pub check_name: String,
67    pub verdict: Verdict,
68    pub raw_stdout: Option<String>,
69    pub raw_stderr: Option<String>,
70}
71
72/// Object-safe trait. Implementations are stored as `Box<dyn CheckSource>`
73/// in the source registry.
74pub trait CheckSource: Send + Sync {
75    /// Stable identifier for this source (e.g. `"shell"`, `"pre_commit"`,
76    /// `"plugin:klasp-plugin-foo"`). Tied to `&self` lifetime — see module
77    /// docs for why this isn't `&'static str`.
78    fn source_id(&self) -> &str;
79
80    /// Pre-flight check: does this source know how to handle the given
81    /// `CheckConfig`? Used by the registry to dispatch a check to the
82    /// right source.
83    fn supports_config(&self, config: &CheckConfig) -> bool;
84
85    /// Execute the check and return a structured result.
86    ///
87    /// Errors here are runtime failures (process spawn errors, malformed
88    /// output) — semantic failures (lint hits, test failures) ride inside
89    /// `Verdict::Fail`. Use `CheckSourceError::Other` for impl-specific
90    /// errors that don't fit the predefined variants.
91    fn run(&self, config: &CheckConfig, state: &RepoState)
92        -> Result<CheckResult, CheckSourceError>;
93}