1use anyhow::Result;
2use schemars::JsonSchema;
3use serde::Serialize;
4use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
5
6use crate::{
7 SourceRange,
8 errors::Suggestion,
9 lsp::IntoDiagnostic,
10 parsing::ast::types::{Node as AstNode, Program},
11 walk::Node,
12};
13
14pub trait Rule<'a> {
19 fn check(&self, node: Node<'a>, prog: &AstNode<Program>) -> Result<Vec<Discovered>>;
21}
22
23impl<'a, FnT> Rule<'a> for FnT
24where
25 FnT: Fn(Node<'a>, &AstNode<Program>) -> Result<Vec<Discovered>>,
26{
27 fn check(&self, n: Node<'a>, prog: &AstNode<Program>) -> Result<Vec<Discovered>> {
28 self(n, prog)
29 }
30}
31
32#[derive(Clone, Debug, ts_rs::TS, Serialize, JsonSchema)]
34#[ts(export)]
35#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
36#[serde(rename_all = "camelCase")]
37pub struct Discovered {
38 pub finding: Finding,
40
41 pub description: String,
43
44 pub pos: SourceRange,
46
47 pub overridden: bool,
49
50 pub suggestion: Option<Suggestion>,
52}
53
54impl Discovered {
55 #[cfg(test)]
56 pub fn apply_suggestion(&self, src: &str) -> Option<String> {
57 let suggestion = self.suggestion.as_ref()?;
58 Some(format!(
59 "{}{}{}",
60 &src[0..suggestion.source_range.start()],
61 suggestion.insert,
62 &src[suggestion.source_range.end()..]
63 ))
64 }
65}
66
67#[cfg(feature = "pyo3")]
68#[pyo3::pymethods]
69impl Discovered {
70 #[getter]
71 pub fn finding(&self) -> Finding {
72 self.finding.clone()
73 }
74
75 #[getter]
76 pub fn description(&self) -> String {
77 self.description.clone()
78 }
79
80 #[getter]
81 pub fn pos(&self) -> (usize, usize) {
82 (self.pos.start(), self.pos.end())
83 }
84
85 #[getter]
86 pub fn overridden(&self) -> bool {
87 self.overridden
88 }
89}
90
91impl IntoDiagnostic for Discovered {
92 fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
93 (&self).to_lsp_diagnostics(code)
94 }
95
96 fn severity(&self) -> DiagnosticSeverity {
97 (&self).severity()
98 }
99}
100
101impl IntoDiagnostic for &Discovered {
102 fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
103 let message = self.finding.title.to_owned();
104 let source_range = self.pos;
105 let edit = self.suggestion.as_ref().map(|s| s.to_lsp_edit(code));
106
107 vec![Diagnostic {
108 range: source_range.to_lsp_range(code),
109 severity: Some(self.severity()),
110 code: None,
111 code_description: None,
113 source: Some("lint".to_string()),
114 message,
115 related_information: None,
116 tags: None,
117 data: edit.map(|e| serde_json::to_value(e).unwrap()),
118 }]
119 }
120
121 fn severity(&self) -> DiagnosticSeverity {
122 DiagnosticSeverity::INFORMATION
123 }
124}
125
126#[derive(Clone, Debug, PartialEq, ts_rs::TS, Serialize, JsonSchema)]
128#[ts(export)]
129#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
130#[serde(rename_all = "camelCase")]
131pub struct Finding {
132 pub code: &'static str,
134
135 pub title: &'static str,
137
138 pub description: &'static str,
140
141 pub experimental: bool,
143}
144
145impl Finding {
146 pub fn at(&self, description: String, pos: SourceRange, suggestion: Option<Suggestion>) -> Discovered {
148 Discovered {
149 description,
150 finding: self.clone(),
151 pos,
152 overridden: false,
153 suggestion,
154 }
155 }
156}
157
158#[cfg(feature = "pyo3")]
159#[pyo3::pymethods]
160impl Finding {
161 #[getter]
162 pub fn code(&self) -> &'static str {
163 self.code
164 }
165
166 #[getter]
167 pub fn title(&self) -> &'static str {
168 self.title
169 }
170
171 #[getter]
172 pub fn description(&self) -> &'static str {
173 self.description
174 }
175
176 #[getter]
177 pub fn experimental(&self) -> bool {
178 self.experimental
179 }
180}
181
182macro_rules! def_finding {
183 ( $code:ident, $title:expr_2021, $description:expr_2021 ) => {
184 pub const $code: Finding = $crate::lint::rule::finding!($code, $title, $description);
186 };
187}
188pub(crate) use def_finding;
189
190macro_rules! finding {
191 ( $code:ident, $title:expr_2021, $description:expr_2021 ) => {
192 $crate::lint::rule::Finding {
193 code: stringify!($code),
194 title: $title,
195 description: $description,
196 experimental: false,
197 }
198 };
199}
200pub(crate) use finding;
201#[cfg(test)]
202pub(crate) use test::{assert_finding, assert_no_finding, test_finding, test_no_finding};
203
204#[cfg(test)]
205mod test {
206
207 macro_rules! assert_no_finding {
208 ( $check:expr_2021, $finding:expr_2021, $kcl:expr_2021 ) => {
209 let prog = $crate::Program::parse_no_errs($kcl).unwrap();
210
211 $crate::execution::parse_execute($kcl).await.unwrap();
213
214 for discovered_finding in prog.lint($check).unwrap() {
215 if discovered_finding.finding == $finding {
216 assert!(false, "Finding {:?} was emitted", $finding.code);
217 }
218 }
219 };
220 }
221
222 macro_rules! assert_finding {
223 ( $check:expr_2021, $finding:expr_2021, $kcl:expr_2021, $output:expr_2021, $suggestion:expr_2021 ) => {
224 let prog = $crate::Program::parse_no_errs($kcl).unwrap();
225
226 $crate::execution::parse_execute($kcl).await.unwrap();
228
229 for discovered_finding in prog.lint($check).unwrap() {
230 pretty_assertions::assert_eq!(discovered_finding.description, $output,);
231
232 if discovered_finding.finding == $finding {
233 pretty_assertions::assert_eq!(
234 discovered_finding.suggestion.clone().map(|s| s.insert),
235 $suggestion,
236 );
237
238 if discovered_finding.suggestion.is_some() {
239 let code = discovered_finding.apply_suggestion($kcl).unwrap();
241
242 $crate::execution::parse_execute(&code).await.unwrap();
244 }
245 return;
246 }
247 }
248 assert!(false, "Finding {:?} was not emitted", $finding.code);
249 };
250 }
251
252 macro_rules! test_finding {
253 ( $name:ident, $check:expr_2021, $finding:expr_2021, $kcl:expr_2021, $output:expr_2021, $suggestion:expr_2021 ) => {
254 #[tokio::test]
255 async fn $name() {
256 $crate::lint::rule::assert_finding!($check, $finding, $kcl, $output, $suggestion);
257 }
258 };
259 }
260
261 macro_rules! test_no_finding {
262 ( $name:ident, $check:expr_2021, $finding:expr_2021, $kcl:expr_2021 ) => {
263 #[tokio::test]
264 async fn $name() {
265 $crate::lint::rule::assert_no_finding!($check, $finding, $kcl);
266 }
267 };
268 }
269
270 pub(crate) use assert_finding;
271 pub(crate) use assert_no_finding;
272 pub(crate) use test_finding;
273 pub(crate) use test_no_finding;
274}