Skip to main content

klasp_core/
plugin.rs

1//! Plugin subprocess protocol types — v0.3 experimental.
2//!
3//! Design: [docs/plugin-protocol.md]. The protocol is `PLUGIN_PROTOCOL_VERSION = 0`
4//! — explicitly experimental. It may break in any v0.3.x release and graduates
5//! to `1` only at v1.0.
6//!
7//! Plugins are separate binaries named `klasp-plugin-<name>` discovered on
8//! `$PATH` at gate time. They communicate over stdin/stdout using JSON. Two
9//! subcommands: `--describe` (capability query) and `--gate` (execute a check).
10//!
11//! See `docs/plugin-protocol.md` for the full wire format specification.
12
13use serde::{Deserialize, Serialize};
14
15use crate::protocol::PLUGIN_PROTOCOL_VERSION;
16use crate::trigger::GitEvent;
17use crate::verdict::{Finding, Severity};
18
19/// Prefix for plugin binary names on `$PATH`. A plugin named `my-linter` is
20/// invoked as `klasp-plugin-my-linter`. Renaming this prefix is a single-site edit.
21pub const KLASP_PLUGIN_BIN_PREFIX: &str = "klasp-plugin-";
22
23/// Rule slug used for all plugin infrastructure errors (binary missing,
24/// non-zero exit, malformed JSON, version mismatch, timeout). Plugin-reported
25/// findings carry their own rule strings — this slug only identifies klasp's
26/// own plugin-runtime warnings.
27pub const KLASP_PLUGIN_RULE: &str = "klasp::plugin";
28
29/// What a plugin sends in response to `--describe`. Used by klasp to
30/// verify forward-compatibility before invoking `--gate`.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct PluginDescribe {
33    /// Must equal [`PLUGIN_PROTOCOL_VERSION`] for klasp to accept the plugin.
34    pub protocol_version: u32,
35    /// Canonical plugin name (e.g. `"klasp-plugin-pre-commit"`).
36    pub name: String,
37    /// List of config `type` names this plugin supports. Informational only.
38    pub config_types: Vec<String>,
39    /// Capability flags. Currently only `verdict_v0` is defined.
40    #[serde(default)]
41    pub supports: PluginSupports,
42}
43
44/// Capability flags advertised in `PluginDescribe`.
45#[derive(Debug, Clone, Default, Serialize, Deserialize)]
46pub struct PluginSupports {
47    /// Plugin speaks the v0 verdict protocol (`pass | warn | fail` + `findings`).
48    #[serde(default)]
49    pub verdict_v0: bool,
50}
51
52/// Git event tier as reported on the plugin wire. Mirrors `GitEvent` but
53/// kept distinct so wire-format evolution is decoupled from internal types.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "lowercase")]
56pub enum PluginTriggerKind {
57    Commit,
58    Push,
59}
60
61impl From<GitEvent> for PluginTriggerKind {
62    fn from(event: GitEvent) -> Self {
63        match event {
64            GitEvent::Commit => PluginTriggerKind::Commit,
65            GitEvent::Push => PluginTriggerKind::Push,
66        }
67    }
68}
69
70/// Git event information forwarded to plugins.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct PluginTrigger {
73    pub kind: PluginTriggerKind,
74    /// Absolute paths of staged files in scope for this check group.
75    /// Empty array when running in single-config / push mode.
76    pub files: Vec<String>,
77}
78
79impl PluginTrigger {
80    pub fn from_event(event: GitEvent, staged_files: &[std::path::PathBuf]) -> Self {
81        Self {
82            kind: event.into(),
83            files: staged_files
84                .iter()
85                .map(|p| p.to_string_lossy().into_owned())
86                .collect(),
87        }
88    }
89}
90
91/// The JSON object written to plugin stdin on `--gate`.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct PluginGateInput {
94    /// Must equal [`PLUGIN_PROTOCOL_VERSION`].
95    pub protocol_version: u32,
96    /// Mirrors `KLASP_GATE_SCHEMA` for plugins that inspect it.
97    pub schema_version: u32,
98    /// Current git event.
99    pub trigger: PluginTrigger,
100    /// Config block forwarded from `klasp.toml`.
101    pub config: PluginConfig,
102    /// Absolute path to the repo root.
103    pub repo_root: String,
104    /// Merge-base ref (same value exported as `KLASP_BASE_REF`).
105    pub base_ref: String,
106}
107
108impl PluginGateInput {
109    /// Build a `PluginGateInput` from gate runtime data.
110    pub fn new(
111        trigger: PluginTrigger,
112        config: PluginConfig,
113        repo_root: &std::path::Path,
114        base_ref: &str,
115    ) -> Self {
116        Self {
117            protocol_version: PLUGIN_PROTOCOL_VERSION,
118            schema_version: crate::protocol::GATE_SCHEMA_VERSION,
119            trigger,
120            config,
121            repo_root: repo_root.to_string_lossy().into_owned(),
122            base_ref: base_ref.to_string(),
123        }
124    }
125}
126
127/// Plugin-facing view of the `[checks.source]` block.
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct PluginConfig {
130    /// Plugin name (same as `name` in `CheckSourceConfig::Plugin`).
131    pub r#type: String,
132    /// Extra args forwarded from `klasp.toml`.
133    #[serde(default)]
134    pub args: Vec<String>,
135    /// Opaque settings blob forwarded from `klasp.toml`.
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    pub settings: Option<serde_json::Value>,
138}
139
140/// The JSON object a plugin writes to stdout on `--gate`.
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct PluginGateOutput {
143    /// Must equal [`PLUGIN_PROTOCOL_VERSION`].
144    pub protocol_version: u32,
145    /// `"pass"`, `"warn"`, or `"fail"`.
146    pub verdict: PluginVerdict,
147    /// Structured findings. Empty array is valid for `pass` verdicts.
148    #[serde(default)]
149    pub findings: Vec<PluginFinding>,
150}
151
152/// Verdict tier as reported by a plugin. Maps to klasp's `Verdict` enum.
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
154#[serde(rename_all = "lowercase")]
155pub enum PluginVerdict {
156    Pass,
157    Warn,
158    Fail,
159}
160
161/// A single finding reported by a plugin. Maps to klasp's `Finding` struct.
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct PluginFinding {
164    /// `"info"`, `"warn"`, or `"error"`.
165    pub severity: Severity,
166    /// Rule identifier (e.g. `"ruff/E501"`).
167    pub rule: String,
168    #[serde(default, skip_serializing_if = "Option::is_none")]
169    pub file: Option<String>,
170    #[serde(default, skip_serializing_if = "Option::is_none")]
171    pub line: Option<u32>,
172    pub message: String,
173}
174
175impl From<PluginFinding> for Finding {
176    fn from(pf: PluginFinding) -> Self {
177        Finding {
178            rule: pf.rule,
179            message: pf.message,
180            file: pf.file,
181            line: pf.line,
182            severity: pf.severity,
183        }
184    }
185}
186
187/// Construct a `Verdict::Warn` for a plugin infrastructure error. Plugin
188/// errors (non-zero exit, malformed JSON, timeout, unknown version) produce a
189/// `Verdict::Warn` with `rule = KLASP_PLUGIN_RULE`. The gate continues with
190/// the remaining checks — plugin errors never crash klasp.
191///
192/// The plugin name is prepended to the message so renderers and JUnit
193/// formatters can attribute the warning to a specific plugin.
194pub fn plugin_error_warn(plugin_name: &str, reason: impl Into<String>) -> crate::verdict::Verdict {
195    let reason = reason.into();
196    crate::verdict::Verdict::Warn {
197        findings: vec![Finding {
198            rule: KLASP_PLUGIN_RULE.to_string(),
199            message: format!("plugin `{plugin_name}`: {reason}"),
200            file: None,
201            line: None,
202            severity: Severity::Warn,
203        }],
204        message: None,
205    }
206}