use anyhow::Result;
#[cfg(feature = "pyo3")]
use pyo3_stub_gen::inventory;
#[cfg(feature = "pyo3")]
use pyo3_stub_gen::type_info::PyEnumInfo;
use serde::Serialize;
use tower_lsp::lsp_types::Diagnostic;
use tower_lsp::lsp_types::DiagnosticSeverity;
use crate::SourceRange;
use crate::errors::Suggestion;
use crate::lsp::IntoDiagnostic;
use crate::lsp::ToLspRange;
use crate::lsp::to_lsp_edit;
use crate::parsing::ast::types::Node as AstNode;
use crate::parsing::ast::types::Program;
use crate::walk::Node;
pub trait Rule<'a> {
fn check(&self, node: Node<'a>, prog: &AstNode<Program>) -> Result<Vec<Discovered>>;
}
impl<'a, FnT> Rule<'a> for FnT
where
FnT: Fn(Node<'a>, &AstNode<Program>) -> Result<Vec<Discovered>>,
{
fn check(&self, n: Node<'a>, prog: &AstNode<Program>) -> Result<Vec<Discovered>> {
self(n, prog)
}
}
#[derive(Clone, Debug, ts_rs::TS, Serialize)]
#[ts(export)]
#[cfg_attr(feature = "pyo3", pyo3::pyclass, pyo3_stub_gen::derive::gen_stub_pyclass)]
#[serde(rename_all = "camelCase")]
pub struct Discovered {
pub finding: Finding,
pub description: String,
pub pos: SourceRange,
pub overridden: bool,
pub suggestion: Option<Suggestion>,
}
impl Discovered {
pub fn apply_suggestion(&self, src: &str) -> Option<String> {
self.suggestion.as_ref().map(|suggestion| suggestion.apply(src))
}
}
pub fn lint_and_fix_all(mut source: String) -> anyhow::Result<(String, Vec<Discovered>)> {
loop {
let (program, errors) = crate::Program::parse(&source)?;
if !errors.is_empty() {
anyhow::bail!("Found errors while parsing, please run the parser and fix them before linting.");
}
let Some(program) = program else {
anyhow::bail!("Could not parse, please run parser and ensure the program is valid before linting");
};
let lints = program.lint_all()?;
if let Some(to_fix) = lints.iter().find_map(|lint| lint.suggestion.clone()) {
source = to_fix.apply(&source);
} else {
return Ok((source, lints));
}
}
}
pub fn lint_and_fix_families(
mut source: String,
families_to_fix: &[FindingFamily],
) -> anyhow::Result<(String, Vec<Discovered>)> {
loop {
let (program, errors) = crate::Program::parse(&source)?;
if !errors.is_empty() {
anyhow::bail!("Found errors while parsing, please run the parser and fix them before linting.");
}
let Some(program) = program else {
anyhow::bail!("Could not parse, please run parser and ensure the program is valid before linting");
};
let lints = program.lint_all()?;
if let Some(to_fix) = lints.iter().find_map(|lint| {
if families_to_fix.contains(&lint.finding.family) {
lint.suggestion.clone()
} else {
None
}
}) {
source = to_fix.apply(&source);
} else {
return Ok((source, lints));
}
}
}
#[cfg(feature = "pyo3")]
#[pyo3_stub_gen::derive::gen_stub_pymethods]
#[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) -> (usize, usize) {
(self.pos.start(), self.pos.end())
}
#[getter]
pub fn overridden(&self) -> bool {
self.overridden
}
}
impl IntoDiagnostic for Discovered {
fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
(&self).to_lsp_diagnostics(code)
}
fn severity(&self) -> DiagnosticSeverity {
(&self).severity()
}
}
impl IntoDiagnostic for &Discovered {
fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
let message = self.finding.title.to_owned();
let source_range = self.pos;
let edit = self.suggestion.as_ref().map(|s| to_lsp_edit(s, code));
vec![Diagnostic {
range: source_range.to_lsp_range(code),
severity: Some(self.severity()),
code: Some(tower_lsp::lsp_types::NumberOrString::String(
self.finding.code.to_string(),
)),
code_description: None,
source: Some("lint".to_string()),
message,
related_information: None,
tags: None,
data: edit.map(|e| serde_json::to_value(e).unwrap()),
}]
}
fn severity(&self) -> DiagnosticSeverity {
DiagnosticSeverity::INFORMATION
}
}
#[derive(Clone, Debug, PartialEq, ts_rs::TS, Serialize)]
#[ts(export)]
#[cfg_attr(feature = "pyo3", pyo3::pyclass, pyo3_stub_gen::derive::gen_stub_pyclass)]
#[serde(rename_all = "camelCase")]
pub struct Finding {
pub code: &'static str,
pub title: &'static str,
pub description: &'static str,
pub experimental: bool,
pub family: FindingFamily,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, ts_rs::TS, Serialize, Hash)]
#[ts(export)]
#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
#[serde(rename_all = "camelCase")]
pub enum FindingFamily {
Style,
Correctness,
Simplify,
}
impl std::fmt::Display for FindingFamily {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FindingFamily::Style => write!(f, "style"),
FindingFamily::Correctness => write!(f, "correctness"),
FindingFamily::Simplify => write!(f, "simplify"),
}
}
}
#[cfg(feature = "pyo3")]
impl pyo3_stub_gen::PyStubType for FindingFamily {
fn type_output() -> pyo3_stub_gen::TypeInfo {
pyo3_stub_gen::TypeInfo::unqualified("FindingFamily")
}
}
#[cfg(feature = "pyo3")]
fn finding_family_type_id() -> std::any::TypeId {
std::any::TypeId::of::<FindingFamily>()
}
#[cfg(feature = "pyo3")]
inventory::submit! {
PyEnumInfo {
enum_id: finding_family_type_id,
pyclass_name: "FindingFamily",
module: None,
doc: "Lint families such as style or correctness.",
variants: &[
("Style", "KCL style guidelines, e.g. identifier casing."),
("Correctness", "The user is probably doing something incorrect or unintended."),
("Simplify", "The user has expressed something in a complex way that could be simplified."),
],
}
}
impl Finding {
pub fn at(&self, description: String, pos: SourceRange, suggestion: Option<Suggestion>) -> Discovered {
Discovered {
description,
finding: self.clone(),
pos,
overridden: false,
suggestion,
}
}
}
#[cfg(feature = "pyo3")]
#[pyo3_stub_gen::derive::gen_stub_pymethods]
#[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
}
#[getter]
pub fn family(&self) -> String {
self.family.to_string()
}
}
macro_rules! def_finding {
( $code:ident, $title:expr_2021, $description:expr_2021, $family:path) => {
pub const $code: Finding = $crate::lint::rule::finding!($code, $title, $description, $family);
};
}
pub(crate) use def_finding;
macro_rules! finding {
( $code:ident, $title:expr_2021, $description:expr_2021, $family:path ) => {
$crate::lint::rule::Finding {
code: stringify!($code),
title: $title,
description: $description,
experimental: false,
family: $family,
}
};
}
pub(crate) use finding;
#[cfg(test)]
pub(crate) use test::assert_finding;
#[cfg(test)]
pub(crate) use test::assert_no_finding;
#[cfg(test)]
pub(crate) use test::test_finding;
#[cfg(test)]
pub(crate) use test::test_no_finding;
#[cfg(test)]
mod test {
#[test]
fn test_lint_and_fix_all() {
let path = "../kcl-python-bindings/files/box_with_linter_errors.kcl";
let f = std::fs::read_to_string(path).unwrap();
let prog = crate::Program::parse_no_errs(&f).unwrap();
let lints = prog.lint_all().unwrap();
assert!(lints.len() >= 4);
let (new_code, unfixed) = lint_and_fix_all(f).unwrap();
assert!(unfixed.len() < 4);
assert!(!new_code.contains('_'));
}
#[test]
fn test_lint_and_fix_families() {
let path = "../kcl-python-bindings/files/box_with_linter_errors.kcl";
let original_code = std::fs::read_to_string(path).unwrap();
let prog = crate::Program::parse_no_errs(&original_code).unwrap();
let lints = prog.lint_all().unwrap();
assert!(lints.len() >= 4);
let (new_code, unfixed) =
lint_and_fix_families(original_code, &[FindingFamily::Correctness, FindingFamily::Simplify]).unwrap();
assert!(unfixed.len() >= 3);
assert!(new_code.contains("box_width"));
assert!(new_code.contains("box_depth"));
assert!(new_code.contains("box_height"));
}
macro_rules! assert_no_finding {
( $check:expr_2021, $finding:expr_2021, $kcl:expr_2021 ) => {
let prog = $crate::Program::parse_no_errs($kcl).unwrap();
$crate::execution::parse_execute($kcl).await.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_2021, $finding:expr_2021, $kcl:expr_2021, $output:expr_2021, $suggestion:expr_2021 ) => {
let prog = $crate::Program::parse_no_errs($kcl).unwrap();
$crate::execution::parse_execute($kcl).await.unwrap();
for discovered_finding in prog.lint($check).unwrap() {
pretty_assertions::assert_eq!(discovered_finding.description, $output,);
if discovered_finding.finding == $finding {
pretty_assertions::assert_eq!(
discovered_finding.suggestion.clone().map(|s| s.insert),
$suggestion,
);
if discovered_finding.suggestion.is_some() {
let code = discovered_finding.apply_suggestion($kcl).unwrap();
$crate::execution::parse_execute(&code).await.unwrap();
}
return;
}
}
assert!(false, "Finding {:?} was not emitted", $finding.code);
};
}
macro_rules! test_finding {
( $name:ident, $check:expr_2021, $finding:expr_2021, $kcl:expr_2021, $output:expr_2021, $suggestion:expr_2021 ) => {
#[tokio::test]
async fn $name() {
$crate::lint::rule::assert_finding!($check, $finding, $kcl, $output, $suggestion);
}
};
}
macro_rules! test_no_finding {
( $name:ident, $check:expr_2021, $finding:expr_2021, $kcl:expr_2021 ) => {
#[tokio::test]
async 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;
use super::*;
}