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}