1use std::sync::Arc;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use serde_json::Value as JsonValue;
6
7use crate::{CallToolResult, CorpusReport, CoverageReport, RunOutcome, Tool, ToolInvocation};
8#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
10#[serde(rename_all = "snake_case")]
11pub enum LintLevel {
12 Error,
13 Warning,
14 Disabled,
15}
16
17impl LintLevel {
18 pub fn is_disabled(&self) -> bool {
19 matches!(self, Self::Disabled)
20 }
21}
22
23#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
25#[serde(rename_all = "snake_case")]
26pub enum LintPhase {
27 List,
28 Response,
29 Run,
30}
31
32#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
34#[serde(rename_all = "snake_case")]
35pub enum LintConfigSource {
36 Repo,
37 Home,
38 #[default]
39 Default,
40}
41
42#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
44pub struct LintDefinition {
45 pub id: String,
46 pub phase: LintPhase,
47 pub level: LintLevel,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub params: Option<JsonValue>,
50}
51
52impl LintDefinition {
53 pub fn new(id: impl Into<String>, phase: LintPhase, level: LintLevel) -> Self {
54 Self {
55 id: id.into(),
56 phase,
57 level,
58 params: None,
59 }
60 }
61
62 pub fn with_params(mut self, params: JsonValue) -> Self {
63 self.params = Some(params);
64 self
65 }
66}
67
68#[derive(Clone, Debug)]
70pub struct LintFinding {
71 pub message: String,
72 pub details: Option<JsonValue>,
73 pub code: Option<String>,
74}
75
76impl LintFinding {
77 pub fn new(message: impl Into<String>) -> Self {
78 Self {
79 message: message.into(),
80 details: None,
81 code: None,
82 }
83 }
84
85 pub fn with_details(mut self, details: JsonValue) -> Self {
86 self.details = Some(details);
87 self
88 }
89
90 pub fn with_code(mut self, code: impl Into<String>) -> Self {
91 self.code = Some(code.into());
92 self
93 }
94}
95
96#[derive(Clone, Debug)]
98#[non_exhaustive]
99pub struct ListLintContext<'a> {
100 pub raw_tool_count: usize,
102 pub protocol_version: Option<&'a str>,
104 pub tools: &'a [Tool],
105}
106
107#[derive(Clone, Debug)]
109#[non_exhaustive]
110pub struct ResponseLintContext<'a> {
111 pub tool: &'a Tool,
112 pub invocation: &'a ToolInvocation,
113 pub response: &'a CallToolResult,
114}
115
116#[derive(Clone, Debug)]
118#[non_exhaustive]
119pub struct RunLintContext<'a> {
120 pub coverage: Option<&'a CoverageReport>,
121 pub corpus: Option<&'a CorpusReport>,
122 pub coverage_allowlist: Option<&'a [String]>,
123 pub coverage_blocklist: Option<&'a [String]>,
124 pub outcome: &'a RunOutcome,
125}
126
127pub trait LintRule: Send + Sync {
129 fn definition(&self) -> &LintDefinition;
130
131 fn check_list(&self, _context: &ListLintContext<'_>) -> Vec<LintFinding> {
132 Vec::new()
133 }
134
135 fn check_response(&self, _context: &ResponseLintContext<'_>) -> Vec<LintFinding> {
136 Vec::new()
137 }
138
139 fn check_run(&self, _context: &RunLintContext<'_>) -> Vec<LintFinding> {
140 Vec::new()
141 }
142}
143
144#[derive(Clone, Default)]
146pub struct LintSuite {
147 rules: Vec<Arc<dyn LintRule>>,
148 source: LintConfigSource,
149}
150
151impl LintSuite {
152 pub fn new(rules: Vec<Arc<dyn LintRule>>) -> Self {
153 Self {
154 rules,
155 source: LintConfigSource::Default,
156 }
157 }
158
159 pub fn with_source(mut self, source: LintConfigSource) -> Self {
160 self.source = source;
161 self
162 }
163
164 pub fn source(&self) -> LintConfigSource {
165 self.source
166 }
167
168 pub fn len(&self) -> usize {
169 self.rules.len()
170 }
171
172 pub fn is_empty(&self) -> bool {
173 self.rules.is_empty()
174 }
175
176 pub(crate) fn rules(&self) -> &[Arc<dyn LintRule>] {
177 &self.rules
178 }
179
180 pub fn has_enabled(&self, id: &str) -> bool {
181 self.rules.iter().any(|rule| {
182 let definition = rule.definition();
183 definition.id == id && definition.level != LintLevel::Disabled
184 })
185 }
186}
187
188pub(crate) struct LintPhases {
189 pub(crate) list: Vec<Arc<dyn LintRule>>,
190 pub(crate) response: Vec<Arc<dyn LintRule>>,
191 pub(crate) run: Vec<Arc<dyn LintRule>>,
192}
193
194impl LintPhases {
195 pub(crate) fn from_suite(suite: &LintSuite) -> Self {
196 let mut list = Vec::new();
197 let mut response = Vec::new();
198 let mut run = Vec::new();
199 for rule in suite.rules() {
200 if rule.definition().level.is_disabled() {
201 continue;
202 }
203 match rule.definition().phase {
204 LintPhase::List => list.push(Arc::clone(rule)),
205 LintPhase::Response => response.push(Arc::clone(rule)),
206 LintPhase::Run => run.push(Arc::clone(rule)),
207 }
208 }
209 Self {
210 list,
211 response,
212 run,
213 }
214 }
215}