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, pyo3_stub_gen::derive::gen_stub_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_stub_gen::derive::gen_stub_pymethods]
69#[pyo3::pymethods]
70impl Discovered {
71 #[getter]
72 pub fn finding(&self) -> Finding {
73 self.finding.clone()
74 }
75
76 #[getter]
77 pub fn description(&self) -> String {
78 self.description.clone()
79 }
80
81 #[getter]
82 pub fn pos(&self) -> (usize, usize) {
83 (self.pos.start(), self.pos.end())
84 }
85
86 #[getter]
87 pub fn overridden(&self) -> bool {
88 self.overridden
89 }
90}
91
92impl IntoDiagnostic for Discovered {
93 fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
94 (&self).to_lsp_diagnostics(code)
95 }
96
97 fn severity(&self) -> DiagnosticSeverity {
98 (&self).severity()
99 }
100}
101
102impl IntoDiagnostic for &Discovered {
103 fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
104 let message = self.finding.title.to_owned();
105 let source_range = self.pos;
106 let edit = self.suggestion.as_ref().map(|s| s.to_lsp_edit(code));
107
108 vec![Diagnostic {
109 range: source_range.to_lsp_range(code),
110 severity: Some(self.severity()),
111 code: None,
112 code_description: None,
114 source: Some("lint".to_string()),
115 message,
116 related_information: None,
117 tags: None,
118 data: edit.map(|e| serde_json::to_value(e).unwrap()),
119 }]
120 }
121
122 fn severity(&self) -> DiagnosticSeverity {
123 DiagnosticSeverity::INFORMATION
124 }
125}
126
127#[derive(Clone, Debug, PartialEq, ts_rs::TS, Serialize, JsonSchema)]
129#[ts(export)]
130#[cfg_attr(feature = "pyo3", pyo3::pyclass, pyo3_stub_gen::derive::gen_stub_pyclass)]
131#[serde(rename_all = "camelCase")]
132pub struct Finding {
133 pub code: &'static str,
135
136 pub title: &'static str,
138
139 pub description: &'static str,
141
142 pub experimental: bool,
144}
145
146impl Finding {
147 pub fn at(&self, description: String, pos: SourceRange, suggestion: Option<Suggestion>) -> Discovered {
149 Discovered {
150 description,
151 finding: self.clone(),
152 pos,
153 overridden: false,
154 suggestion,
155 }
156 }
157}
158
159#[cfg(feature = "pyo3")]
160#[pyo3_stub_gen::derive::gen_stub_pymethods]
161#[pyo3::pymethods]
162impl Finding {
163 #[getter]
164 pub fn code(&self) -> &'static str {
165 self.code
166 }
167
168 #[getter]
169 pub fn title(&self) -> &'static str {
170 self.title
171 }
172
173 #[getter]
174 pub fn description(&self) -> &'static str {
175 self.description
176 }
177
178 #[getter]
179 pub fn experimental(&self) -> bool {
180 self.experimental
181 }
182}
183
184macro_rules! def_finding {
185 ( $code:ident, $title:expr_2021, $description:expr_2021 ) => {
186 pub const $code: Finding = $crate::lint::rule::finding!($code, $title, $description);
188 };
189}
190pub(crate) use def_finding;
191
192macro_rules! finding {
193 ( $code:ident, $title:expr_2021, $description:expr_2021 ) => {
194 $crate::lint::rule::Finding {
195 code: stringify!($code),
196 title: $title,
197 description: $description,
198 experimental: false,
199 }
200 };
201}
202pub(crate) use finding;
203#[cfg(test)]
204pub(crate) use test::{assert_finding, assert_no_finding, test_finding, test_no_finding};
205
206#[cfg(test)]
207mod test {
208
209 macro_rules! assert_no_finding {
210 ( $check:expr_2021, $finding:expr_2021, $kcl:expr_2021 ) => {
211 let prog = $crate::Program::parse_no_errs($kcl).unwrap();
212
213 $crate::execution::parse_execute($kcl).await.unwrap();
215
216 for discovered_finding in prog.lint($check).unwrap() {
217 if discovered_finding.finding == $finding {
218 assert!(false, "Finding {:?} was emitted", $finding.code);
219 }
220 }
221 };
222 }
223
224 macro_rules! assert_finding {
225 ( $check:expr_2021, $finding:expr_2021, $kcl:expr_2021, $output:expr_2021, $suggestion:expr_2021 ) => {
226 let prog = $crate::Program::parse_no_errs($kcl).unwrap();
227
228 $crate::execution::parse_execute($kcl).await.unwrap();
230
231 for discovered_finding in prog.lint($check).unwrap() {
232 pretty_assertions::assert_eq!(discovered_finding.description, $output,);
233
234 if discovered_finding.finding == $finding {
235 pretty_assertions::assert_eq!(
236 discovered_finding.suggestion.clone().map(|s| s.insert),
237 $suggestion,
238 );
239
240 if discovered_finding.suggestion.is_some() {
241 let code = discovered_finding.apply_suggestion($kcl).unwrap();
243
244 $crate::execution::parse_execute(&code).await.unwrap();
246 }
247 return;
248 }
249 }
250 assert!(false, "Finding {:?} was not emitted", $finding.code);
251 };
252 }
253
254 macro_rules! test_finding {
255 ( $name:ident, $check:expr_2021, $finding:expr_2021, $kcl:expr_2021, $output:expr_2021, $suggestion:expr_2021 ) => {
256 #[tokio::test]
257 async fn $name() {
258 $crate::lint::rule::assert_finding!($check, $finding, $kcl, $output, $suggestion);
259 }
260 };
261 }
262
263 macro_rules! test_no_finding {
264 ( $name:ident, $check:expr_2021, $finding:expr_2021, $kcl:expr_2021 ) => {
265 #[tokio::test]
266 async fn $name() {
267 $crate::lint::rule::assert_no_finding!($check, $finding, $kcl);
268 }
269 };
270 }
271
272 pub(crate) use assert_finding;
273 pub(crate) use assert_no_finding;
274 pub(crate) use test_finding;
275 pub(crate) use test_no_finding;
276}