pub mod ast;
pub mod checks;
pub mod config;
pub mod contents;
pub mod lexer;
pub mod message;
pub mod parser;
#[allow(non_upper_case_globals, dead_code)]
pub mod tables;
#[cfg(test)]
mod tests;
pub use config::Config;
pub use message::Message;
use checks::{Check, Ctx, render};
use config::{FileSetModule, LineSeparator};
use contents::{FileContents, FileText};
use lexer::StreamError;
use parser::{ParseError, Parser};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Failure {
pub position: Option<(i32, i32)>,
pub from_lexer: bool,
pub message: String,
}
impl Failure {
pub fn render(&self, file: &str) -> String {
match (self.position, self.from_lexer) {
(Some((line, col)), false) => format!("{file}:{line}:{col}: {}", self.message),
(Some((line, col)), true) => format!("{file}: line {line}:{col}: {}", self.message),
(None, _) => format!("{file}: {}", self.message),
}
}
const fn exception(message: String) -> Self {
Self {
position: None,
from_lexer: false,
message,
}
}
}
#[derive(Debug)]
pub enum Outcome {
Skipped,
Checked(Vec<Message>),
Failed(Failure),
}
fn matches_extension(file: &str, extensions: &[String]) -> bool {
let name = file.rsplit('/').next().unwrap_or(file);
extensions.is_empty() || extensions.iter().any(|e| name.ends_with(e.as_str()))
}
fn stream_failure(error: StreamError) -> Failure {
match error {
StreamError::Recognition { line, col, message } => Failure {
position: Some((line, col)),
from_lexer: true,
message,
},
StreamError::Crash(message) => Failure::exception(message),
}
}
fn parse_failure(error: ParseError) -> Failure {
match error {
ParseError::Recognition { line, col, message } => Failure {
position: Some((line, col)),
from_lexer: false,
message,
},
ParseError::Semantic(message) => Failure::exception(format!(" {message}")),
ParseError::Stream(e) => stream_failure(e),
}
}
fn newline_at_end_of_file(bytes: &[u8], separator: LineSeparator) -> Option<&'static str> {
let ends_with = |s: &[u8]| bytes.len() >= s.len() && bytes.ends_with(s);
match separator {
LineSeparator::Lf if ends_with(b"\r\n") => Some("wrong.line.end"),
LineSeparator::Lf => (!ends_with(b"\n")).then_some("noNewlineAtEOF"),
LineSeparator::Crlf => (!ends_with(b"\r\n")).then_some("noNewlineAtEOF"),
LineSeparator::Cr => (!ends_with(b"\r")).then_some("noNewlineAtEOF"),
LineSeparator::LfCrCrlf => {
(!(ends_with(b"\n") || ends_with(b"\r"))).then_some("noNewlineAtEOF")
}
}
}
pub fn check_file(name: &str, bytes: &[u8], config: &Config) -> Outcome {
if !matches_extension(name, &config.file_extensions) {
return Outcome::Skipped;
}
let mut contents: Option<FileContents> = None;
let mut messages = Vec::new();
for module in &config.modules {
match module {
FileSetModule::NewlineAtEndOfFile {
common,
file_extensions,
line_separator,
} => {
if !matches_extension(name, file_extensions) {
continue;
}
if let Some(key) = newline_at_end_of_file(bytes, *line_separator) {
let pattern = common.messages.get(key).map(String::as_str);
match message::format_key(key, pattern, &[message::Arg::Str(name.to_string())])
{
Ok(text) => messages.push(Message {
line: 1,
col: 0,
severity: common.severity,
module_id: common.id.clone(),
source: "NewlineAtEndOfFile",
text,
}),
Err(e) => return Outcome::Failed(Failure::exception(e)),
}
}
}
FileSetModule::TreeWalker {
file_extensions,
checks,
} => {
if checks.is_empty() || !matches_extension(name, file_extensions) {
continue;
}
let file_contents =
contents.get_or_insert_with(|| FileContents::new(FileText::new(bytes)));
match walk_file(file_contents, checks) {
Ok(found) => messages.extend(found),
Err(failure) => return Outcome::Failed(failure),
}
}
}
}
Outcome::Checked(message::sort_and_dedupe(messages))
}
fn walk_file(
contents: &mut FileContents,
modules: &[config::CheckModule],
) -> Result<Vec<Message>, Failure> {
let stream = lexer::tokenize(&contents.text, &mut contents.comments);
let mut parser = Parser::new(&stream.tokens, stream.error.as_ref());
let root = parser.compilation_unit().map_err(parse_failure)?;
let ast = parser.ast;
let ctx = Ctx {
ast: &ast,
contents,
};
let mut checks: Vec<Check> = modules.iter().map(Check::new).collect();
let crash = Failure::exception;
checks::walk(&ctx, root, &mut checks).map_err(crash)?;
let mut messages = Vec::new();
for check in &checks {
for v in &check.violations {
messages.push(render(check.module, v).map_err(crash)?);
}
}
Ok(messages)
}