1use anyhow::Result;
2use serde::Serialize;
3use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
4
5use crate::{
6 SourceRange,
7 errors::Suggestion,
8 lsp::{IntoDiagnostic, ToLspRange, to_lsp_edit},
9 parsing::ast::types::{Node as AstNode, Program},
10 walk::Node,
11};
12
13pub trait Rule<'a> {
18 fn check(&self, node: Node<'a>, prog: &AstNode<Program>) -> Result<Vec<Discovered>>;
20}
21
22impl<'a, FnT> Rule<'a> for FnT
23where
24 FnT: Fn(Node<'a>, &AstNode<Program>) -> Result<Vec<Discovered>>,
25{
26 fn check(&self, n: Node<'a>, prog: &AstNode<Program>) -> Result<Vec<Discovered>> {
27 self(n, prog)
28 }
29}
30
31#[derive(Clone, Debug, ts_rs::TS, Serialize)]
33#[ts(export)]
34#[cfg_attr(feature = "pyo3", pyo3::pyclass, pyo3_stub_gen::derive::gen_stub_pyclass)]
35#[serde(rename_all = "camelCase")]
36pub struct Discovered {
37 pub finding: Finding,
39
40 pub description: String,
42
43 pub pos: SourceRange,
45
46 pub overridden: bool,
48
49 pub suggestion: Option<Suggestion>,
51}
52
53impl Discovered {
54 #[cfg(test)]
55 pub fn apply_suggestion(&self, src: &str) -> Option<String> {
56 let suggestion = self.suggestion.as_ref()?;
57 Some(format!(
58 "{}{}{}",
59 &src[0..suggestion.source_range.start()],
60 suggestion.insert,
61 &src[suggestion.source_range.end()..]
62 ))
63 }
64}
65
66#[cfg(feature = "pyo3")]
67#[pyo3_stub_gen::derive::gen_stub_pymethods]
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| to_lsp_edit(s, 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)]
128#[ts(export)]
129#[cfg_attr(feature = "pyo3", pyo3::pyclass, pyo3_stub_gen::derive::gen_stub_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_stub_gen::derive::gen_stub_pymethods]
160#[pyo3::pymethods]
161impl Finding {
162 #[getter]
163 pub fn code(&self) -> &'static str {
164 self.code
165 }
166
167 #[getter]
168 pub fn title(&self) -> &'static str {
169 self.title
170 }
171
172 #[getter]
173 pub fn description(&self) -> &'static str {
174 self.description
175 }
176
177 #[getter]
178 pub fn experimental(&self) -> bool {
179 self.experimental
180 }
181}
182
183macro_rules! def_finding {
184 ( $code:ident, $title:expr_2021, $description:expr_2021 ) => {
185 pub const $code: Finding = $crate::lint::rule::finding!($code, $title, $description);
187 };
188}
189pub(crate) use def_finding;
190
191macro_rules! finding {
192 ( $code:ident, $title:expr_2021, $description:expr_2021 ) => {
193 $crate::lint::rule::Finding {
194 code: stringify!($code),
195 title: $title,
196 description: $description,
197 experimental: false,
198 }
199 };
200}
201pub(crate) use finding;
202#[cfg(test)]
203pub(crate) use test::{assert_finding, assert_no_finding, test_finding, test_no_finding};
204
205#[cfg(test)]
206mod test {
207
208 macro_rules! assert_no_finding {
209 ( $check:expr_2021, $finding:expr_2021, $kcl:expr_2021 ) => {
210 let prog = $crate::Program::parse_no_errs($kcl).unwrap();
211
212 $crate::execution::parse_execute($kcl).await.unwrap();
214
215 for discovered_finding in prog.lint($check).unwrap() {
216 if discovered_finding.finding == $finding {
217 assert!(false, "Finding {:?} was emitted", $finding.code);
218 }
219 }
220 };
221 }
222
223 macro_rules! assert_finding {
224 ( $check:expr_2021, $finding:expr_2021, $kcl:expr_2021, $output:expr_2021, $suggestion:expr_2021 ) => {
225 let prog = $crate::Program::parse_no_errs($kcl).unwrap();
226
227 $crate::execution::parse_execute($kcl).await.unwrap();
229
230 for discovered_finding in prog.lint($check).unwrap() {
231 pretty_assertions::assert_eq!(discovered_finding.description, $output,);
232
233 if discovered_finding.finding == $finding {
234 pretty_assertions::assert_eq!(
235 discovered_finding.suggestion.clone().map(|s| s.insert),
236 $suggestion,
237 );
238
239 if discovered_finding.suggestion.is_some() {
240 let code = discovered_finding.apply_suggestion($kcl).unwrap();
242
243 $crate::execution::parse_execute(&code).await.unwrap();
245 }
246 return;
247 }
248 }
249 assert!(false, "Finding {:?} was not emitted", $finding.code);
250 };
251 }
252
253 macro_rules! test_finding {
254 ( $name:ident, $check:expr_2021, $finding:expr_2021, $kcl:expr_2021, $output:expr_2021, $suggestion:expr_2021 ) => {
255 #[tokio::test]
256 async fn $name() {
257 $crate::lint::rule::assert_finding!($check, $finding, $kcl, $output, $suggestion);
258 }
259 };
260 }
261
262 macro_rules! test_no_finding {
263 ( $name:ident, $check:expr_2021, $finding:expr_2021, $kcl:expr_2021 ) => {
264 #[tokio::test]
265 async fn $name() {
266 $crate::lint::rule::assert_no_finding!($check, $finding, $kcl);
267 }
268 };
269 }
270
271 pub(crate) use assert_finding;
272 pub(crate) use assert_no_finding;
273 pub(crate) use test_finding;
274 pub(crate) use test_no_finding;
275}