use crate::ceprintlns;
use aeruginous_io::PathBufLikeReader;
use std::path::PathBuf;
use sysexits::Result;
#[allow(clippy::struct_excessive_bools)]
#[derive(clap::Parser, Clone)]
pub struct Complain {
files: Vec<PathBuf>,
#[arg(long)]
ignore_carriage_return_line_feeds: bool,
#[arg(long)]
ignore_line_width_issues: bool,
#[arg(long)]
ignore_missing_final_line_feed: bool,
#[arg(long)]
ignore_mixed_indentation: bool,
#[arg(long)]
ignore_tabs_within_lines: bool,
#[arg(long)]
ignore_trailing_white_space_characters: bool,
#[arg(long)]
ignore_wrong_indentation: bool,
#[arg(default_value = "spaces", long, short)]
indent_by: IndentationUnit,
#[arg(
default_value = "80",
long,
short,
visible_aliases = ["length", "line", "width"]
)]
line_width: usize,
#[arg(long)]
verbose: bool,
}
impl Complain {
pub fn ignore_carriage_return_line_feeds(&mut self) {
self.ignore_carriage_return_line_feeds = true;
}
pub fn ignore_line_width_issues(&mut self) {
self.ignore_line_width_issues = true;
}
pub fn ignore_missing_final_line_feed(&mut self) {
self.ignore_missing_final_line_feed = true;
}
pub fn ignore_mixed_indentation(&mut self) {
self.ignore_mixed_indentation = true;
}
pub fn ignore_tabs_within_lines(&mut self) {
self.ignore_tabs_within_lines = true;
}
pub fn ignore_trailing_white_space_characters(&mut self) {
self.ignore_trailing_white_space_characters = true;
}
pub fn ignore_wrong_indentation(&mut self) {
self.ignore_wrong_indentation = true;
}
pub fn indent_by(&mut self, i: IndentationUnit) {
self.indent_by = i;
}
pub fn main(&self) -> Result<()> {
self.wrap().main()
}
#[must_use]
pub fn new(files: Vec<PathBuf>) -> Self {
Self {
files,
ignore_carriage_return_line_feeds: false,
ignore_line_width_issues: false,
ignore_missing_final_line_feed: false,
ignore_mixed_indentation: false,
ignore_tabs_within_lines: false,
ignore_trailing_white_space_characters: false,
ignore_wrong_indentation: false,
indent_by: IndentationUnit::Spaces,
line_width: 80,
verbose: false,
}
}
pub fn push<T>(&mut self, path: T)
where
PathBuf: From<T>,
{
self.files.push(PathBuf::from(path));
}
pub fn process(&self) -> Result<usize> {
self.wrap().process()
}
#[must_use]
pub const fn state(
&self,
) -> (&Vec<PathBuf>, [bool; 7], IndentationUnit, usize) {
(
&self.files,
[
self.ignore_carriage_return_line_feeds,
self.ignore_line_width_issues,
self.ignore_missing_final_line_feed,
self.ignore_mixed_indentation,
self.ignore_tabs_within_lines,
self.ignore_trailing_white_space_characters,
self.ignore_wrong_indentation,
],
self.indent_by,
self.line_width,
)
}
fn wrap(&self) -> Logic {
Logic {
cli: self.clone(),
data: String::new(),
errors: 0,
total_errors: 0,
}
}
}
impl Default for Complain {
fn default() -> Self {
Self::new(Vec::new())
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum IndentationUnit {
#[default]
Spaces,
Tabs,
}
crate::enum_trait!(IndentationUnit {
Spaces <-> "spaces",
Tabs <-> "tabs"
});
struct Logic {
cli: Complain,
data: String,
errors: usize,
total_errors: usize,
}
impl Logic {
fn aec_0001(&mut self) -> Result<()> {
if !self.data.ends_with('\n') {
self.errors += 1;
ceprintlns!(
"ÆC-0001"!Green,
"File not terminated by line feed."
);
}
Ok(())
}
fn aec_0002(&mut self) -> Result<()> {
let mut line = 1;
for l in self.data.split_inclusive('\n') {
if l.ends_with("\r\n") {
self.errors += 1;
ceprintlns!(
"ÆC-0002"!Yellow,
"CRLF in line {line}."
);
}
line += 1;
}
Ok(())
}
fn aec_0003(&mut self) -> Result<()> {
let mut line = 1;
let mut mercy = false;
for l in self.data.lines() {
if l.contains("#[aeruginous::mercy::0003::start]") {
mercy = true;
} else if l.contains("#[aeruginous::mercy::0003::end]") {
mercy = false;
}
if mercy {
continue;
}
let c = l.chars().count();
if c > self.cli.line_width
&& !l.contains("#[aeruginous::mercy::0003]")
{
self.errors += 1;
ceprintlns!(
"ÆC-0003"!Red,
"Line {line} is {} character(s) too long.",
c - self.cli.line_width
);
}
line += 1;
}
Ok(())
}
fn aec_0004(&mut self) -> Result<()> {
let mut line = 1;
for l in self.data.lines() {
if l.ends_with(char::is_whitespace) {
self.errors += 1;
ceprintlns!(
"ÆC-0004"!Green,
"TWS in line {line}."
);
}
line += 1;
}
Ok(())
}
fn aec_0005(&mut self) -> Result<()> {
let mut line = 1;
let trigger = match self.cli.indent_by {
IndentationUnit::Spaces => '\t',
IndentationUnit::Tabs => ' ',
};
for l in self.data.lines() {
if l.starts_with(trigger) {
self.errors += 1;
ceprintlns!(
"ÆC-0005"!Green,
"Line {line} indented by {}.",
if trigger == '\t' { "tabs" } else { "spaces" }
);
}
line += 1;
}
Ok(())
}
fn aec_0006(&mut self) -> Result<()> {
let mut line = 1;
for l in self.data.lines() {
if l.split_once(|c| !char::is_whitespace(c)).is_some_and(
|(indentation, _)| {
indentation.contains('\t') && indentation.contains(' ')
},
) {
self.errors += 1;
ceprintlns!(
"ÆC-0006"!Yellow,
"Line {line} is indented by both spaces and tabs."
);
}
line += 1;
}
Ok(())
}
fn aec_0007(&mut self) -> Result<()> {
let mut line = 1;
for l in self.data.lines() {
if l.trim().contains('\t') {
self.errors += 1;
ceprintlns!(
"ÆC-0007"!Yellow,
"Tabs within line {line}."
);
}
line += 1;
}
Ok(())
}
fn complain(&mut self, f: &PathBuf) -> Result<()> {
self.data = f.read_loudly()?;
if !self.cli.ignore_missing_final_line_feed {
self.aec_0001()?;
}
if !self.cli.ignore_carriage_return_line_feeds {
self.aec_0002()?;
}
if !self.cli.ignore_line_width_issues {
self.aec_0003()?;
}
if !self.cli.ignore_trailing_white_space_characters {
self.aec_0004()?;
}
if !self.cli.ignore_wrong_indentation {
self.aec_0005()?;
}
if !self.cli.ignore_mixed_indentation {
self.aec_0006()?;
}
if !self.cli.ignore_tabs_within_lines {
self.aec_0007()?;
}
if self.cli.verbose || self.errors > 0 {
ceprintlns!("ˇ;{\"};ˇ"!Blue, "{} {}", self.errors, f.display());
}
self.total_errors += self.errors;
self.errors = 0;
Ok(())
}
fn main(&mut self) -> Result<()> {
if self.process()? == 0 {
Ok(())
} else {
Err(sysexits::ExitCode::DataErr)
}
}
fn process(&mut self) -> Result<usize> {
for f in self.cli.files.clone() {
if f.is_dir() {
self.process_dir(f.read_dir()?)?;
} else {
self.complain(&f)?;
}
}
Ok(self.total_errors)
}
fn process_dir(&mut self, directory: std::fs::ReadDir) -> Result<()> {
for entry in directory {
let entry = entry?.path();
if entry.is_dir() {
self.process_dir(entry.read_dir()?)?;
} else {
self.complain(&entry)?;
}
}
Ok(())
}
}