1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
use anyhow::Result;
use schemars::JsonSchema;
use serde::Serialize;
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};

use crate::{executor::SourceRange, lsp::IntoDiagnostic, walk::Node};

/// Check the provided AST for any found rule violations.
///
/// The Rule trait is automatically implemented for a few other types,
/// but it can also be manually implemented as required.
pub trait Rule<'a> {
    /// Check the AST at this specific node for any Finding(s).
    fn check(&self, node: Node<'a>) -> Result<Vec<Discovered>>;
}

impl<'a, FnT> Rule<'a> for FnT
where
    FnT: Fn(Node<'a>) -> Result<Vec<Discovered>>,
{
    fn check(&self, n: Node<'a>) -> Result<Vec<Discovered>> {
        self(n)
    }
}

/// Specific discovered lint rule Violation of a particular Finding.
#[derive(Clone, Debug, ts_rs::TS, Serialize, JsonSchema)]
#[ts(export)]
#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
#[serde(rename_all = "camelCase")]
pub struct Discovered {
    /// Zoo Lint Finding information.
    pub finding: Finding,

    /// Further information about the specific finding.
    pub description: String,

    /// Source code location.
    pub pos: SourceRange,

    /// Is this discovered issue overridden by the programmer?
    pub overridden: bool,
}

#[cfg(feature = "pyo3")]
#[pyo3::pymethods]
impl Discovered {
    #[getter]
    pub fn finding(&self) -> Finding {
        self.finding.clone()
    }

    #[getter]
    pub fn description(&self) -> String {
        self.description.clone()
    }

    #[getter]
    pub fn pos(&self) -> SourceRange {
        self.pos
    }

    #[getter]
    pub fn overridden(&self) -> bool {
        self.overridden
    }
}

impl IntoDiagnostic for Discovered {
    fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
        (&self).to_lsp_diagnostic(code)
    }

    fn severity(&self) -> DiagnosticSeverity {
        (&self).severity()
    }
}

impl IntoDiagnostic for &Discovered {
    fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
        let message = self.finding.title.to_owned();
        let source_range = self.pos;

        Diagnostic {
            range: source_range.to_lsp_range(code),
            severity: Some(self.severity()),
            code: None,
            // TODO: this is neat we can pass a URL to a help page here for this specific error.
            code_description: None,
            source: Some("lint".to_string()),
            message,
            related_information: None,
            tags: None,
            data: None,
        }
    }

    fn severity(&self) -> DiagnosticSeverity {
        DiagnosticSeverity::INFORMATION
    }
}

/// Abstract lint problem type.
#[derive(Clone, Debug, PartialEq, ts_rs::TS, Serialize, JsonSchema)]
#[ts(export)]
#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
#[serde(rename_all = "camelCase")]
pub struct Finding {
    /// Unique identifier for this particular issue.
    pub code: &'static str,

    /// Short one-line description of this issue.
    pub title: &'static str,

    /// Long human-readable description of this issue.
    pub description: &'static str,

    /// Is this discovered issue experimental?
    pub experimental: bool,
}

impl Finding {
    /// Create a new Discovered finding at the specific Position.
    pub fn at(&self, description: String, pos: SourceRange) -> Discovered {
        Discovered {
            description,
            finding: self.clone(),
            pos,
            overridden: false,
        }
    }
}

#[cfg(feature = "pyo3")]
#[pyo3::pymethods]
impl Finding {
    #[getter]
    pub fn code(&self) -> &'static str {
        self.code
    }

    #[getter]
    pub fn title(&self) -> &'static str {
        self.title
    }

    #[getter]
    pub fn description(&self) -> &'static str {
        self.description
    }

    #[getter]
    pub fn experimental(&self) -> bool {
        self.experimental
    }
}

macro_rules! def_finding {
    ( $code:ident, $title:expr, $description:expr ) => {
        /// Generated Finding
        pub const $code: Finding = $crate::lint::rule::finding!($code, $title, $description);
    };
}
pub(crate) use def_finding;

macro_rules! finding {
    ( $code:ident, $title:expr, $description:expr ) => {
        $crate::lint::rule::Finding {
            code: stringify!($code),
            title: $title,
            description: $description,
            experimental: false,
        }
    };
}
pub(crate) use finding;
#[cfg(test)]
pub(crate) use test::{assert_finding, assert_no_finding, test_finding, test_no_finding};

#[cfg(test)]
mod test {

    macro_rules! assert_no_finding {
        ( $check:expr, $finding:expr, $kcl:expr ) => {
            let tokens = $crate::token::lexer($kcl).unwrap();
            let parser = $crate::parser::Parser::new(tokens);
            let prog = parser.ast().unwrap();
            for discovered_finding in prog.lint($check).unwrap() {
                if discovered_finding.finding == $finding {
                    assert!(false, "Finding {:?} was emitted", $finding.code);
                }
            }
        };
    }

    macro_rules! assert_finding {
        ( $check:expr, $finding:expr, $kcl:expr ) => {
            let tokens = $crate::token::lexer($kcl).unwrap();
            let parser = $crate::parser::Parser::new(tokens);
            let prog = parser.ast().unwrap();

            for discovered_finding in prog.lint($check).unwrap() {
                if discovered_finding.finding == $finding {
                    return;
                }
            }
            assert!(false, "Finding {:?} was not emitted", $finding.code);
        };
    }

    macro_rules! test_finding {
        ( $name:ident, $check:expr, $finding:expr, $kcl:expr ) => {
            #[test]
            fn $name() {
                $crate::lint::rule::assert_finding!($check, $finding, $kcl);
            }
        };
    }

    macro_rules! test_no_finding {
        ( $name:ident, $check:expr, $finding:expr, $kcl:expr ) => {
            #[test]
            fn $name() {
                $crate::lint::rule::assert_no_finding!($check, $finding, $kcl);
            }
        };
    }

    pub(crate) use assert_finding;
    pub(crate) use assert_no_finding;
    pub(crate) use test_finding;
    pub(crate) use test_no_finding;
}