use anyhow::Result;
use schemars::JsonSchema;
use serde::Serialize;
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
use crate::{executor::SourceRange, lsp::IntoDiagnostic, walk::Node};
pub trait Rule<'a> {
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)
}
}
#[derive(Clone, Debug, ts_rs::TS, Serialize, JsonSchema)]
#[ts(export)]
#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
#[serde(rename_all = "camelCase")]
pub struct Discovered {
pub finding: Finding,
pub description: String,
pub pos: SourceRange,
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,
code_description: None,
source: Some("lint".to_string()),
message,
related_information: None,
tags: None,
data: None,
}
}
fn severity(&self) -> DiagnosticSeverity {
DiagnosticSeverity::INFORMATION
}
}
#[derive(Clone, Debug, PartialEq, ts_rs::TS, Serialize, JsonSchema)]
#[ts(export)]
#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
#[serde(rename_all = "camelCase")]
pub struct Finding {
pub code: &'static str,
pub title: &'static str,
pub description: &'static str,
pub experimental: bool,
}
impl Finding {
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 ) => {
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;
}