kcl_lib/lint/
rule.rs

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
8/// Check the provided AST for any found rule violations.
9///
10/// The Rule trait is automatically implemented for a few other types,
11/// but it can also be manually implemented as required.
12pub trait Rule<'a> {
13    /// Check the AST at this specific node for any Finding(s).
14    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/// Specific discovered lint rule Violation of a particular Finding.
27#[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    /// Zoo Lint Finding information.
33    pub finding: Finding,
34
35    /// Further information about the specific finding.
36    pub description: String,
37
38    /// Source code location.
39    pub pos: SourceRange,
40
41    /// Is this discovered issue overridden by the programmer?
42    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            // TODO: this is neat we can pass a URL to a help page here for this specific error.
89            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/// Abstract lint problem type.
104#[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    /// Unique identifier for this particular issue.
110    pub code: &'static str,
111
112    /// Short one-line description of this issue.
113    pub title: &'static str,
114
115    /// Long human-readable description of this issue.
116    pub description: &'static str,
117
118    /// Is this discovered issue experimental?
119    pub experimental: bool,
120}
121
122impl Finding {
123    /// Create a new Discovered finding at the specific Position.
124    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        /// Generated Finding
161        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}