1mod 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 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 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}