Lints and suggestions for the Nix programming language
1#![recursion_limit = "1024"]
2mod lints;
3mod make;
4mod utils;
5
6pub use lints::LINTS;
7
8use rnix::{SyntaxElement, SyntaxKind, TextRange, parser::ParseError};
9use std::{convert::Into, default::Default};
10
11#[cfg(feature = "json-out")]
12use serde::{
13 Serialize,
14 ser::{SerializeStruct, Serializer},
15};
16
17#[derive(Debug, Default)]
18#[cfg_attr(feature = "json-out", derive(Serialize))]
19pub enum Severity {
20 #[default]
21 Warn,
22 Error,
23 Hint,
24}
25
26/// Report generated by a lint
27#[derive(Debug, Default)]
28#[cfg_attr(feature = "json-out", derive(Serialize))]
29pub struct Report {
30 /// General information about this lint and where it applies.
31 pub note: &'static str,
32 /// An error code to uniquely identify this lint
33 pub code: u32,
34 /// Report severity level
35 pub severity: Severity,
36 /// Collection of diagnostics raised by this lint
37 pub diagnostics: Vec<Diagnostic>,
38}
39
40impl Report {
41 /// Construct a report. Do not invoke `Report::new` manually, see `lint` macro
42 #[must_use]
43 pub fn new(note: &'static str, code: u32) -> Self {
44 Self {
45 note,
46 code,
47 ..Default::default()
48 }
49 }
50 /// Add a diagnostic to this report
51 #[allow(clippy::return_self_not_must_use)]
52 pub fn diagnostic<S: AsRef<str>>(mut self, at: TextRange, message: S) -> Self {
53 self.diagnostics.push(Diagnostic::new(at, message));
54 self
55 }
56 /// Add a diagnostic with a fix to this report
57 #[allow(clippy::return_self_not_must_use)]
58 pub fn suggest<S: AsRef<str>>(
59 mut self,
60 at: TextRange,
61 message: S,
62 suggestion: Suggestion,
63 ) -> Self {
64 self.diagnostics
65 .push(Diagnostic::suggest(at, message, suggestion));
66 self
67 }
68 /// Set severity level
69 #[allow(clippy::return_self_not_must_use, clippy::must_use_candidate)]
70 pub fn severity(mut self, severity: Severity) -> Self {
71 self.severity = severity;
72 self
73 }
74 /// A range that encompasses all the suggestions provided in this report
75 pub fn total_suggestion_range(&self) -> Option<TextRange> {
76 self.diagnostics
77 .iter()
78 .filter_map(|d| Some(d.suggestion.as_ref()?.at))
79 .reduce(rnix::TextRange::cover)
80 }
81 /// A range that encompasses all the diagnostics provided in this report
82 pub fn total_diagnostic_range(&self) -> Option<TextRange> {
83 self.diagnostics
84 .iter()
85 .map(|d| d.at)
86 .reduce(rnix::TextRange::cover)
87 }
88 /// Unsafe but handy replacement for above
89 #[must_use]
90 pub fn range(&self) -> TextRange {
91 self.total_suggestion_range().unwrap()
92 }
93 /// Apply all diagnostics. Assumption: diagnostics do not overlap
94 pub fn apply(&self, src: &mut String) {
95 for d in &self.diagnostics {
96 d.apply(src);
97 }
98 }
99 /// Create a report out of a parse error
100 #[must_use]
101 pub fn from_parse_err(err: &ParseError) -> Self {
102 let at = match err {
103 ParseError::Unexpected(at)
104 | ParseError::UnexpectedExtra(at)
105 | ParseError::UnexpectedWanted(_, at, _)
106 | ParseError::UnexpectedDoubleBind(at)
107 | ParseError::DuplicatedArgs(at, _) => at,
108 ParseError::UnexpectedEOF | ParseError::UnexpectedEOFWanted(_) => {
109 &TextRange::empty(0u32.into())
110 }
111 _ => panic!("report a bug, pepper forgot to handle a parse error"),
112 };
113 let mut message = err.to_string();
114 message
115 .as_mut_str()
116 .get_mut(0..1)
117 .unwrap()
118 .make_ascii_uppercase();
119 Self::new("Syntax error", 0)
120 .diagnostic(*at, message)
121 .severity(Severity::Error)
122 }
123}
124
125/// Mapping from a bytespan to an error message.
126/// Can optionally suggest a fix.
127#[derive(Debug)]
128pub struct Diagnostic {
129 pub at: TextRange,
130 pub message: String,
131 pub suggestion: Option<Suggestion>,
132}
133
134impl Diagnostic {
135 /// Construct a diagnostic.
136 pub fn new<S: AsRef<str>>(at: TextRange, message: S) -> Self {
137 Self {
138 at,
139 message: message.as_ref().into(),
140 suggestion: None,
141 }
142 }
143 /// Construct a diagnostic with a fix.
144 pub fn suggest<S: AsRef<str>>(at: TextRange, message: S, suggestion: Suggestion) -> Self {
145 Self {
146 at,
147 message: message.as_ref().into(),
148 suggestion: Some(suggestion),
149 }
150 }
151 /// Apply a diagnostic to a source file
152 pub fn apply(&self, src: &mut String) {
153 if let Some(s) = &self.suggestion {
154 s.apply(src);
155 }
156 }
157}
158
159#[cfg(feature = "json-out")]
160impl Serialize for Diagnostic {
161 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
162 where
163 S: Serializer,
164 {
165 let mut s = serializer.serialize_struct("Diagnostic", 3)?;
166 let at = {
167 let start = usize::from(self.at.start());
168 let end = usize::from(self.at.end());
169 (start, end)
170 };
171 s.serialize_field("at", &at)?;
172 s.serialize_field("message", &self.message)?;
173 if let Some(suggestion) = &self.suggestion {
174 s.serialize_field("suggestion", suggestion)?;
175 }
176 s.end()
177 }
178}
179
180#[derive(Debug)]
181pub enum Replacement {
182 Empty,
183 SyntaxElement(SyntaxElement),
184}
185
186/// Suggested fix for a diagnostic, the fix is provided as a syntax element.
187/// Look at `make.rs` to construct fixes.
188#[derive(Debug)]
189pub struct Suggestion {
190 pub at: TextRange,
191 pub fix: Replacement,
192}
193
194impl std::fmt::Display for Replacement {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 match self {
197 Replacement::Empty => Ok(()),
198 Replacement::SyntaxElement(syntax_element) => write!(f, "{syntax_element}"),
199 }
200 }
201}
202
203impl Suggestion {
204 #[must_use]
205 pub fn with_replacement(at: TextRange, fix: impl Into<SyntaxElement>) -> Self {
206 Self {
207 at,
208 fix: Replacement::SyntaxElement(fix.into()),
209 }
210 }
211 #[must_use]
212 pub fn with_empty(at: TextRange) -> Self {
213 Self {
214 at,
215 fix: Replacement::Empty,
216 }
217 }
218 /// Apply a suggestion to a source file
219 pub fn apply(&self, src: &mut String) {
220 let start = usize::from(self.at.start());
221 let end = usize::from(self.at.end());
222 src.replace_range(start..end, &self.fix.to_string());
223 }
224}
225
226unsafe impl Send for Suggestion {}
227
228#[cfg(feature = "json-out")]
229impl Serialize for Suggestion {
230 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
231 where
232 S: Serializer,
233 {
234 let mut s = serializer.serialize_struct("Suggestion", 2)?;
235 let at = {
236 let start = usize::from(self.at.start());
237 let end = usize::from(self.at.end());
238 (start, end)
239 };
240 let fix = self.fix.to_string();
241 s.serialize_field("at", &at)?;
242 s.serialize_field("fix", &fix)?;
243 s.end()
244 }
245}
246
247/// Lint logic is defined via this trait. Do not implement manually,
248/// look at the `lint` attribute macro instead for implementing rules
249pub trait Rule {
250 fn validate(&self, node: &SyntaxElement) -> Option<Report>;
251}
252
253/// Contains information about the lint itself. Do not implement manually,
254/// look at the `lint` attribute macro instead for implementing rules
255pub trait Metadata {
256 fn name(&self) -> &'static str;
257 fn note(&self) -> &'static str;
258 fn code(&self) -> u32;
259 fn report(&self) -> Report;
260 fn match_with(&self, with: &SyntaxKind) -> bool;
261 fn match_kind(&self) -> Vec<SyntaxKind>;
262}
263
264/// Contains offline explanation for each lint
265/// The `lint` macro scans nearby doc comments for
266/// explanations and derives this trait.
267///
268/// FIXME: the lint macro does way too much, maybe
269/// split it into smaller macros.
270pub trait Explain {
271 fn explanation(&self) -> &'static str {
272 "no explanation found"
273 }
274}
275
276/// Combines Rule and Metadata, do not implement manually, this is derived by
277/// the `lint` macro.
278pub trait Lint: Metadata + Explain + Rule + Send + Sync {}
279
280/// Helper utility to take lints from modules and insert them into a map for efficient
281/// access. Mapping is from a `SyntaxKind` to a list of lints that apply on that Kind.
282///
283/// See `lints.rs` for usage.
284#[macro_export]
285macro_rules! lints {
286 ($($s:ident),*,) => {
287 lints!($($s),*);
288 };
289 ($($s:ident),*) => {
290 $(
291 mod $s;
292 )*
293 ::lazy_static::lazy_static! {
294 pub static ref LINTS: Vec<&'static Box<dyn $crate::Lint>> = {
295 let mut v = Vec::new();
296 $(
297 {
298 let temp_lint = &*$s::LINT;
299 v.push(temp_lint);
300 }
301 )*
302 v
303 };
304 }
305 }
306}