mod _log;
mod check;
mod cli;
mod config;
mod core;
mod document;
mod format;
mod formatting;
mod language;
mod line_break;
mod markdown_prose;
mod parser;
mod spacing;
mod spacing_checker;
use std::io::stdout;
use anyhow::Context;
use clap::Parser;
use crate::{
cli::{
args::{self, CliArgs, ColorOutputMode},
check::check_command,
debug_cst::debug_cst_command,
format::format_command,
},
config::Config,
};
fn main() -> anyhow::Result<()> {
let args = CliArgs::parse();
let config = Config::from_cli_args(&args).with_context(|| "failed to parse configuration")?;
let mut stdout = stdout();
let condition = match args.color {
ColorOutputMode::Always => yansi::Condition::ALWAYS,
ColorOutputMode::Never => yansi::Condition::NEVER,
ColorOutputMode::Auto => yansi::Condition::TTY_AND_COLOR,
};
yansi::whenever(condition);
match args.command {
args::Commands::Check {
language,
filenames,
} => check_command(&mut stdout, &config, filenames.as_slice(), language)?,
args::Commands::DebugCst {
language,
filenames,
} => debug_cst_command(&mut stdout, filenames.as_slice(), language)?,
args::Commands::Format {
write,
language,
filenames,
} => format_command(&mut stdout, &config, filenames.as_slice(), write, language)?,
}
Ok(())
}
#[cfg(test)]
mod file_based_tests {
use super::*;
use crate::core::diagnostic::Diagnostic;
use crate::core::position::Position;
use crate::parser::Grammar;
use regex::Regex;
use serde::Deserialize;
use test_generator::test_resources;
use crate::_log::test_log;
use crate::check::check_one_file;
use crate::cli::utils::format_diagnostic;
use crate::document::Document;
#[derive(Default, Debug, Deserialize)]
struct CheckTestCase {
config: Config,
input: String,
diagnostics: Vec<Diagnostic>,
}
#[test_resources("test_cases/check/*.json")]
fn check(resource: &str) {
let resource = resource.replace('\\', "/");
let content = std::fs::read_to_string(&resource)
.unwrap_or_else(|_| panic!("failed to read resource: {resource:?}"));
let test_case: CheckTestCase = serde_json::from_str(&content)
.unwrap_or_else(|_| panic!("failed to parse resource: {resource:?}"));
let document = Document::new(&test_case.input, Grammar::Markdown, Some(&resource));
let actual = check_one_file(&test_case.config, &document)
.unwrap_or_else(|_| panic!("failed on checking a file: {resource:?}"));
let matched_input = Regex::new(r##""input"\s*:\s*"(.*)""##)
.unwrap()
.captures(&content)
.unwrap_or_else(|| panic!("`input` field not found in the test data file: {resource}"))
.get(1)
.unwrap();
let lines_preceding: Vec<(usize, &str)> = content[..matched_input.start()]
.match_indices("\n")
.collect();
let line_number_offset = lines_preceding.len();
let (line_start_offset, _) = lines_preceding.last().expect("no preceding lines found");
let column_number_offset = matched_input.start() - (line_start_offset + 1);
for (i, diagnostic) in actual.iter().enumerate() {
let diagnostic = Diagnostic::new(
diagnostic.filename.clone(),
Position {
line: diagnostic.start.line + line_number_offset as u32,
column: diagnostic.start.column + column_number_offset as u32,
},
Position {
line: diagnostic.end.line + line_number_offset as u32,
column: diagnostic.end.column + column_number_offset as u32,
},
diagnostic.code.clone(),
diagnostic.message.clone(),
);
let formatted = format_diagnostic(&diagnostic);
test_log!("diagnostics[{i:2}] = {formatted}");
}
assert_eq!(
actual.len(),
test_case.diagnostics.len(),
"diagnostic count mismatch"
);
actual
.iter()
.zip(&test_case.diagnostics)
.for_each(|(a, e)| assert_diagnostics_are_equal(a, e));
}
fn assert_diagnostics_are_equal(a: &Diagnostic, b: &Diagnostic) {
match (&a.filename, &b.filename) {
(Some(f1), Some(f2)) => {
if f1.len() < f2.len() {
assert!(
f2.ends_with(f1),
"filename does not match: {f1:?} and {f2:?}"
);
} else {
assert!(
f1.ends_with(f2),
"filename does not match: {f1:?} and {f2:?}"
);
}
}
(f1, f2) => assert_eq!(f1, f2),
};
assert_eq!(a.start, b.start);
assert_eq!(a.end, b.end);
assert_eq!(a.code, b.code);
assert_eq!(a.message, b.message);
}
}