1use anyhow::Result;
2use schemars::JsonSchema;
3use serde::Serialize;
4use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
5
6use crate::{lsp::IntoDiagnostic, walk::Node, SourceRange};
7
8pub trait Rule<'a> {
13 fn check(&self, node: Node<'a>) -> Result<Vec<Discovered>>;
15}
16
17impl<'a, FnT> Rule<'a> for FnT
18where
19 FnT: Fn(Node<'a>) -> Result<Vec<Discovered>>,
20{
21 fn check(&self, n: Node<'a>) -> Result<Vec<Discovered>> {
22 self(n)
23 }
24}
25
26#[derive(Clone, Debug, ts_rs::TS, Serialize, JsonSchema)]
28#[ts(export)]
29#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
30#[serde(rename_all = "camelCase")]
31pub struct Discovered {
32 pub finding: Finding,
34
35 pub description: String,
37
38 pub pos: SourceRange,
40
41 pub overridden: bool,
43}
44
45#[cfg(feature = "pyo3")]
46#[pyo3::pymethods]
47impl Discovered {
48 #[getter]
49 pub fn finding(&self) -> Finding {
50 self.finding.clone()
51 }
52
53 #[getter]
54 pub fn description(&self) -> String {
55 self.description.clone()
56 }
57
58 #[getter]
59 pub fn pos(&self) -> (usize, usize) {
60 (self.pos.start(), self.pos.end())
61 }
62
63 #[getter]
64 pub fn overridden(&self) -> bool {
65 self.overridden
66 }
67}
68
69impl IntoDiagnostic for Discovered {
70 fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
71 (&self).to_lsp_diagnostics(code)
72 }
73
74 fn severity(&self) -> DiagnosticSeverity {
75 (&self).severity()
76 }
77}
78
79impl IntoDiagnostic for &Discovered {
80 fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
81 let message = self.finding.title.to_owned();
82 let source_range = self.pos;
83
84 vec![Diagnostic {
85 range: source_range.to_lsp_range(code),
86 severity: Some(self.severity()),
87 code: None,
88 code_description: None,
90 source: Some("lint".to_string()),
91 message,
92 related_information: None,
93 tags: None,
94 data: None,
95 }]
96 }
97
98 fn severity(&self) -> DiagnosticSeverity {
99 DiagnosticSeverity::INFORMATION
100 }
101}
102
103#[derive(Clone, Debug, PartialEq, ts_rs::TS, Serialize, JsonSchema)]
105#[ts(export)]
106#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
107#[serde(rename_all = "camelCase")]
108pub struct Finding {
109 pub code: &'static str,
111
112 pub title: &'static str,
114
115 pub description: &'static str,
117
118 pub experimental: bool,
120}
121
122impl Finding {
123 pub fn at(&self, description: String, pos: SourceRange) -> Discovered {
125 Discovered {
126 description,
127 finding: self.clone(),
128 pos,
129 overridden: false,
130 }
131 }
132}
133
134#[cfg(feature = "pyo3")]
135#[pyo3::pymethods]
136impl Finding {
137 #[getter]
138 pub fn code(&self) -> &'static str {
139 self.code
140 }
141
142 #[getter]
143 pub fn title(&self) -> &'static str {
144 self.title
145 }
146
147 #[getter]
148 pub fn description(&self) -> &'static str {
149 self.description
150 }
151
152 #[getter]
153 pub fn experimental(&self) -> bool {
154 self.experimental
155 }
156}
157
158macro_rules! def_finding {
159 ( $code:ident, $title:expr, $description:expr ) => {
160 pub const $code: Finding = $crate::lint::rule::finding!($code, $title, $description);
162 };
163}
164pub(crate) use def_finding;
165
166macro_rules! finding {
167 ( $code:ident, $title:expr, $description:expr ) => {
168 $crate::lint::rule::Finding {
169 code: stringify!($code),
170 title: $title,
171 description: $description,
172 experimental: false,
173 }
174 };
175}
176pub(crate) use finding;
177#[cfg(test)]
178pub(crate) use test::{assert_finding, assert_no_finding, test_finding, test_no_finding};
179
180#[cfg(test)]
181mod test {
182
183 macro_rules! assert_no_finding {
184 ( $check:expr, $finding:expr, $kcl:expr ) => {
185 let prog = $crate::parsing::top_level_parse($kcl).unwrap();
186 for discovered_finding in prog.lint($check).unwrap() {
187 if discovered_finding.finding == $finding {
188 assert!(false, "Finding {:?} was emitted", $finding.code);
189 }
190 }
191 };
192 }
193
194 macro_rules! assert_finding {
195 ( $check:expr, $finding:expr, $kcl:expr ) => {
196 let prog = $crate::parsing::top_level_parse($kcl).unwrap();
197
198 for discovered_finding in prog.lint($check).unwrap() {
199 if discovered_finding.finding == $finding {
200 return;
201 }
202 }
203 assert!(false, "Finding {:?} was not emitted", $finding.code);
204 };
205 }
206
207 macro_rules! test_finding {
208 ( $name:ident, $check:expr, $finding:expr, $kcl:expr ) => {
209 #[test]
210 fn $name() {
211 $crate::lint::rule::assert_finding!($check, $finding, $kcl);
212 }
213 };
214 }
215
216 macro_rules! test_no_finding {
217 ( $name:ident, $check:expr, $finding:expr, $kcl:expr ) => {
218 #[test]
219 fn $name() {
220 $crate::lint::rule::assert_no_finding!($check, $finding, $kcl);
221 }
222 };
223 }
224
225 pub(crate) use assert_finding;
226 pub(crate) use assert_no_finding;
227 pub(crate) use test_finding;
228 pub(crate) use test_no_finding;
229}