tnipv_lint/
lints.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5 */
6
7mod known_lints;
8pub mod markdown;
9pub mod preamble;
10
11use annotate_snippets::snippet::{AnnotationType, Snippet};
12
13use comrak::nodes::AstNode;
14
15use crate::reporters::{self, Reporter};
16
17use educe::Educe;
18
19use tnipv_preamble::Preamble;
20
21pub use self::known_lints::DefaultLint;
22
23use snafu::Snafu;
24
25use std::cell::RefCell;
26use std::cmp::max;
27use std::collections::{HashMap, HashSet};
28use std::fmt::Debug;
29use std::ops::Deref;
30use std::path::{Path, PathBuf};
31use std::string::FromUtf8Error;
32
33#[derive(Debug, Snafu)]
34#[non_exhaustive]
35pub enum Error {
36    #[snafu(context(false))]
37    ReportFailed { source: reporters::Error },
38    #[snafu(context(false))]
39    InvalidUtf8 { source: std::str::Utf8Error },
40    Custom {
41        source: Box<dyn std::error::Error + 'static>,
42    },
43}
44
45impl Error {
46    pub fn custom<E>(source: E) -> Self
47    where
48        E: 'static + std::error::Error,
49    {
50        Self::Custom {
51            source: Box::new(source) as Box<dyn std::error::Error>,
52        }
53    }
54}
55
56impl From<FromUtf8Error> for Error {
57    fn from(e: FromUtf8Error) -> Self {
58        Error::InvalidUtf8 {
59            source: e.utf8_error(),
60        }
61    }
62}
63
64#[derive(Debug, Clone)]
65pub(crate) struct InnerContext<'a> {
66    pub(crate) preamble: Preamble<'a>,
67    pub(crate) source: &'a str,
68    pub(crate) body_source: &'a str,
69    pub(crate) body: &'a AstNode<'a>,
70    pub(crate) origin: Option<&'a str>,
71}
72
73#[derive(Educe)]
74#[educe(Debug)]
75pub struct Context<'a, 'b>
76where
77    'b: 'a,
78{
79    pub(crate) inner: InnerContext<'a>,
80    pub(crate) tnips: &'b HashMap<&'b Path, Result<InnerContext<'b>, &'b crate::Error>>,
81    #[educe(Debug(ignore))]
82    pub(crate) reporter: &'b dyn Reporter,
83    pub(crate) annotation_type: AnnotationType,
84}
85
86impl<'a, 'b> Context<'a, 'b>
87where
88    'b: 'a,
89{
90    pub fn preamble(&self) -> &Preamble<'a> {
91        &self.inner.preamble
92    }
93
94    /// XXX: comrak doesn't include a source field with its `AstNode`, so use
95    ///      this instead. Don't expose it publicly since it's really hacky.
96    ///      Yes, lines start at one.
97    pub(crate) fn line(&self, mut line: usize) -> &'a str {
98        assert_ne!(line, 0);
99        line -= 1;
100        self.inner.source.split('\n').nth(line).unwrap()
101    }
102
103    /// XXX: comrak doesn't include a source field with its `AstNode`, so use
104    ///      this instead. Don't expose it publicly since it's really hacky.
105    pub(crate) fn source_for_text(&self, line: usize, text: &str) -> String {
106        assert_ne!(line, 0);
107
108        let newlines = max(1, text.chars().filter(|c| *c == '\n').count());
109
110        self.inner
111            .source
112            .split('\n')
113            .skip(line - 1)
114            .take(newlines)
115            .collect::<Vec<_>>()
116            .join("\n")
117    }
118
119    pub fn body_source(&self) -> &'a str {
120        self.inner.body_source
121    }
122
123    pub fn body(&self) -> &'a AstNode<'a> {
124        self.inner.body
125    }
126
127    pub fn origin(&self) -> Option<&'a str> {
128        self.inner.origin
129    }
130
131    pub fn annotation_type(&self) -> AnnotationType {
132        self.annotation_type
133    }
134
135    pub fn report(&self, snippet: Snippet<'_>) -> Result<(), Error> {
136        self.reporter.report(snippet)?;
137        Ok(())
138    }
139
140    pub fn tnip(&self, path: &Path) -> Result<Context<'b, 'b>, &crate::Error> {
141        let origin = self
142            .origin()
143            .expect("lint attempted to access an external resource without having an origin");
144
145        let origin_path = PathBuf::from(origin);
146        let root = origin_path.parent().unwrap_or_else(|| Path::new("."));
147
148        let key = root.join(path);
149
150        let inner = match self.tnips.get(key.as_path()) {
151            Some(Ok(i)) => i,
152            Some(Err(e)) => return Err(e),
153            None => panic!("no tnip found for key `{}`", key.display()),
154        };
155
156        Ok(Context {
157            inner: inner.clone(),
158            tnips: self.tnips,
159            reporter: self.reporter,
160            annotation_type: self.annotation_type,
161        })
162    }
163}
164
165#[derive(Debug)]
166pub struct FetchContext<'a> {
167    pub(crate) preamble: &'a Preamble<'a>,
168    pub(crate) body: &'a AstNode<'a>,
169    pub(crate) tnips: RefCell<HashSet<PathBuf>>,
170}
171
172impl<'a> FetchContext<'a> {
173    pub fn preamble(&self) -> &Preamble<'a> {
174        self.preamble
175    }
176
177    pub fn body(&self) -> &'a AstNode<'a> {
178        self.body
179    }
180
181    pub fn fetch(&self, path: PathBuf) {
182        self.tnips.borrow_mut().insert(path);
183    }
184}
185
186pub trait Lint: Debug {
187    fn find_resources(&self, _ctx: &FetchContext<'_>) -> Result<(), Error> {
188        Ok(())
189    }
190
191    fn lint<'a>(&self, slug: &'a str, ctx: &Context<'a, '_>) -> Result<(), Error>;
192}
193
194impl Lint for Box<dyn Lint> {
195    fn find_resources(&self, ctx: &FetchContext<'_>) -> Result<(), Error> {
196        let lint: &dyn Lint = self.deref();
197        lint.find_resources(ctx)
198    }
199
200    fn lint<'a>(&self, slug: &'a str, ctx: &Context<'a, '_>) -> Result<(), Error> {
201        let lint: &dyn Lint = self.deref();
202        lint.lint(slug, ctx)
203    }
204}