rsconstruct 0.9.129

Rust based fast build system
//! checkstyle 8.36.1, in Rust.
//!
//! A port of the parts of checkstyle a `checkstyle.xml` names: the Java
//! grammar's ANTLR 2 lexer and recognizer as generated (`lexer`, `parser`,
//! their lookahead sets in `tables`, generated by
//! `scripts/gen-icheckstyle-tables.py`), the AST they build (`ast`),
//! `FileText`/`FileContents` (`contents`), the `TreeWalker` checks
//! (`checks`), messages and their order (`message`), and the configuration
//! (`config`). `check_file` runs a configuration over one file and returns
//! what `checkstyle -c config FILE` prints for it, or the exception that
//! aborts checkstyle on it (a parse error, or a check that throws).
//!
//! Implemented modules: `Checker`, `TreeWalker`, `NewlineAtEndOfFile`,
//! `ArrayTypeStyle`, `CovariantEquals`, `DefaultComesLast`,
//! `EmptyStatement`, `EqualsHashCode`, `FallThrough`,
//! `ModifiedControlVariable`, `OneStatementPerLine`, `RedundantImport`,
//! `SimplifyBooleanExpression`, `SimplifyBooleanReturn`,
//! `StringLiteralEquality`, `UnusedImports`, `UpperEll`. Not here: other
//! modules, filters, `${property}` expansion from Java system properties,
//! non-UTF-8 charsets, non-English messages, and the state two checks
//! carry from one file to the next within a checkstyle run (each file here
//! is checked on its own).

pub mod ast;
pub mod checks;
pub mod config;
pub mod contents;
pub mod lexer;
pub mod message;
pub mod parser;
// Generated: the grammar's token type names are lower case after
// `LITERAL_` (`LITERAL_void`), as in checkstyle, and the table lists every
// type of the grammar whether a rule names it or not.
#[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};

/// Why checkstyle stops on a file: the exception at the root of its stack
/// trace.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Failure {
    /// Where an ANTLR recognition error happened (1-based line and column).
    pub position: Option<(i32, i32)>,
    /// The error is the lexer's, which ANTLR reports without a file name.
    pub from_lexer: bool,
    pub message: String,
}

impl Failure {
    /// As checkstyle's root cause prints it after the file: `:line:col:
    /// message` for a parser error, ` line line:col: message` for a lexer
    /// error, the Java exception otherwise.
    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 {
    /// The `Checker`'s `fileExtensions` do not include the file.
    Skipped,
    /// The messages of the file, in checkstyle's order (`ignore` severity
    /// included: such messages still take part in deduplication; they are
    /// not printed).
    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,
        },
        // A `SemanticException` without a position: ANTLR's line formatter
        // still puts its separating space in front of the message.
        ParseError::Semantic(message) => Failure::exception(format!(" {message}")),
        ParseError::Stream(e) => stream_failure(e),
    }
}

/// `NewlineAtEndOfFile` over the file's bytes.
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")
        }
    }
}

/// Check one file. `name` is the path as given (used in parse errors).
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))
}

/// Parse the file (its comments are recorded on the way) and run the
/// checks over the tree.
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)
}