#![recursion_limit = "1024"] mod lints; mod make; pub mod session; mod utils; pub use lints::LINTS; use session::SessionInfo; use rnix::{parser::ParseError, SyntaxElement, SyntaxKind, TextRange}; use std::{convert::Into, default::Default}; #[cfg(feature = "json-out")] use serde::{ ser::{SerializeStruct, Serializer}, Serialize, }; #[derive(Debug)] #[cfg_attr(feature = "json-out", derive(Serialize))] pub enum Severity { Warn, Error, Hint, } impl Default for Severity { fn default() -> Self { Self::Warn } } /// Report generated by a lint #[derive(Debug, Default)] #[cfg_attr(feature = "json-out", derive(Serialize))] pub struct Report { /// General information about this lint and where it applies. pub note: &'static str, /// An error code to uniquely identify this lint pub code: u32, /// Report severity level pub severity: Severity, /// Collection of diagnostics raised by this lint pub diagnostics: Vec, } impl Report { /// Construct a report. Do not invoke `Report::new` manually, see `lint` macro pub fn new(note: &'static str, code: u32) -> Self { Self { note, code, ..Default::default() } } /// Add a diagnostic to this report pub fn diagnostic>(mut self, at: TextRange, message: S) -> Self { self.diagnostics.push(Diagnostic::new(at, message)); self } /// Add a diagnostic with a fix to this report pub fn suggest>( mut self, at: TextRange, message: S, suggestion: Suggestion, ) -> Self { self.diagnostics .push(Diagnostic::suggest(at, message, suggestion)); self } /// Set severity level pub fn severity(mut self, severity: Severity) -> Self { self.severity = severity; self } /// A range that encompasses all the suggestions provided in this report pub fn total_suggestion_range(&self) -> Option { self.diagnostics .iter() .flat_map(|d| Some(d.suggestion.as_ref()?.at)) .reduce(|acc, next| acc.cover(next)) } /// A range that encompasses all the diagnostics provided in this report pub fn total_diagnostic_range(&self) -> Option { self.diagnostics .iter() .flat_map(|d| Some(d.at)) .reduce(|acc, next| acc.cover(next)) } /// Unsafe but handy replacement for above pub fn range(&self) -> TextRange { self.total_suggestion_range().unwrap() } /// Apply all diagnostics. Assumption: diagnostics do not overlap pub fn apply(&self, src: &mut String) { for d in self.diagnostics.iter() { d.apply(src); } } /// Create a report out of a parse error pub fn from_parse_err(err: ParseError) -> Self { let at = match err { ParseError::Unexpected(at) => at, ParseError::UnexpectedExtra(at) => at, ParseError::UnexpectedWanted(_, at, _) => at, ParseError::UnexpectedDoubleBind(at) => at, ParseError::UnexpectedEOF | ParseError::UnexpectedEOFWanted(_) => { TextRange::empty(0u32.into()) } _ => panic!("report a bug, pepper forgot to handle a parse error"), }; let mut message = err.to_string(); message .as_mut_str() .get_mut(0..1) .unwrap() .make_ascii_uppercase(); Self::new("Syntax error", 0) .diagnostic(at, message) .severity(Severity::Error) } } /// Mapping from a bytespan to an error message. /// Can optionally suggest a fix. #[derive(Debug)] pub struct Diagnostic { pub at: TextRange, pub message: String, pub suggestion: Option, } impl Diagnostic { /// Construct a diagnostic. pub fn new>(at: TextRange, message: S) -> Self { Self { at, message: message.as_ref().into(), suggestion: None, } } /// Construct a diagnostic with a fix. pub fn suggest>(at: TextRange, message: S, suggestion: Suggestion) -> Self { Self { at, message: message.as_ref().into(), suggestion: Some(suggestion), } } /// Apply a diagnostic to a source file pub fn apply(&self, src: &mut String) { if let Some(s) = &self.suggestion { s.apply(src); } } } #[cfg(feature = "json-out")] impl Serialize for Diagnostic { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let mut s = serializer.serialize_struct("Diagnostic", 3)?; let at = { let start = usize::from(self.at.start()); let end = usize::from(self.at.end()); (start, end) }; s.serialize_field("at", &at)?; s.serialize_field("message", &self.message)?; if let Some(suggestion) = &self.suggestion { s.serialize_field("suggestion", suggestion)?; } s.end() } } /// Suggested fix for a diagnostic, the fix is provided as a syntax element. /// Look at `make.rs` to construct fixes. #[derive(Debug)] pub struct Suggestion { pub at: TextRange, pub fix: SyntaxElement, } impl Suggestion { /// Construct a suggestion. pub fn new>(at: TextRange, fix: E) -> Self { Self { at, fix: fix.into(), } } /// Apply a suggestion to a source file pub fn apply(&self, src: &mut String) { let start = usize::from(self.at.start()); let end = usize::from(self.at.end()); src.replace_range(start..end, &self.fix.to_string()) } } #[cfg(feature = "json-out")] impl Serialize for Suggestion { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let mut s = serializer.serialize_struct("Suggestion", 2)?; let at = { let start = usize::from(self.at.start()); let end = usize::from(self.at.end()); (start, end) }; let fix = self.fix.to_string(); s.serialize_field("at", &at)?; s.serialize_field("fix", &fix)?; s.end() } } /// Lint logic is defined via this trait. Do not implement manually, /// look at the `lint` attribute macro instead for implementing rules pub trait Rule { fn validate(&self, node: &SyntaxElement, sess: &SessionInfo) -> Option; } /// Contains information about the lint itself. Do not implement manually, /// look at the `lint` attribute macro instead for implementing rules pub trait Metadata { fn name(&self) -> &'static str; fn note(&self) -> &'static str; fn code(&self) -> u32; fn report(&self) -> Report; fn match_with(&self, with: &SyntaxKind) -> bool; fn match_kind(&self) -> Vec; } /// Contains offline explanation for each lint /// The `lint` macro scans nearby doc comments for /// explanations and derives this trait. /// /// FIXME: the lint macro does way too much, maybe /// split it into smaller macros. pub trait Explain { fn explanation(&self) -> &'static str { "no explanation found" } } /// Combines Rule and Metadata, do not implement manually, this is derived by /// the `lint` macro. pub trait Lint: Metadata + Explain + Rule + Send + Sync {} /// Helper utility to take lints from modules and insert them into a map for efficient /// access. Mapping is from a SyntaxKind to a list of lints that apply on that Kind. /// /// See `lints.rs` for usage. #[macro_export] macro_rules! lints { ($($s:ident),*,) => { lints!($($s),*); }; ($($s:ident),*) => { $( mod $s; )* ::lazy_static::lazy_static! { pub static ref LINTS: Vec<&'static Box> = { let mut v = Vec::new(); $( { let temp_lint = &*$s::LINT; v.push(temp_lint); } )* v }; } } }