Lints and suggestions for the Nix programming language
at main 306 lines 9.0 kB view raw
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}