1use std::collections::VecDeque;
4
5use convert_case::Case;
6use convert_case::Casing;
7use nonempty::NonEmpty;
8
9use crate::concern::Code;
10use crate::file::location;
11
12mod level;
13mod tag_set;
14pub mod warning;
15
16pub use level::Level;
17pub use tag_set::Tag;
18pub use tag_set::TagSet;
19pub use warning::Warning;
20
21#[derive(Debug)]
23pub enum Error {
24 Location(location::Error),
26}
27
28impl std::fmt::Display for Error {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 match self {
31 Error::Location(err) => write!(f, "location error: {err}"),
32 }
33 }
34}
35
36impl std::error::Error for Error {}
37
38pub type Result = std::result::Result<Option<NonEmpty<Warning>>, Error>;
40
41#[derive(Debug)]
43pub struct Linter;
44
45impl Linter {
46 pub fn lint<'a, E>(tree: &'a E, rules: Vec<Box<dyn Rule<&'a E>>>) -> Result {
49 let mut warnings = rules
50 .iter()
51 .map(|rule| rule.check(tree))
52 .collect::<std::result::Result<Vec<Option<NonEmpty<Warning>>>, Error>>()?
53 .into_iter()
54 .flatten()
55 .flatten()
56 .collect::<VecDeque<Warning>>();
57
58 match warnings.pop_front() {
59 Some(front) => {
60 let mut result = NonEmpty::new(front);
61 result.extend(warnings);
62 Ok(Some(result))
63 }
64 None => Ok(None),
65 }
66 }
67}
68
69pub trait Rule<E>: std::fmt::Debug + Sync {
71 fn name(&self) -> String {
73 format!("{:?}", self).to_case(Case::Snake)
74 }
75
76 fn code(&self) -> Code;
78
79 fn tags(&self) -> TagSet;
81
82 fn body(&self) -> &'static str;
84
85 fn check(&self, tree: E) -> Result;
87}