diff --git a/README.md b/README.md index 6692862..6ec3513 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,7 @@ Our public API consists of: - The CLI tool (`$ alejandra`), command line flags, positional arguments, + exit codes, and stdout. ## Changelog diff --git a/buildkite.yaml b/buildkite.yaml index 32fb3cb..cdee5c9 100644 --- a/buildkite.yaml +++ b/buildkite.yaml @@ -33,6 +33,10 @@ steps: - formatting-before-vs-after.patch.txt - formatting-after.patch.txt command: + - echo +++ Formatting - demo + - nix run . -- flake.nix + + - echo --- Cloning nixpkgs - git config --global user.email CI/CD - git config --global user.name CI/CD - git clone --branch=master --depth 1 --origin=upstream file:///data/nixpkgs diff --git a/front/src/lib.rs b/front/src/lib.rs index 88202e2..e874fda 100644 --- a/front/src/lib.rs +++ b/front/src/lib.rs @@ -12,5 +12,5 @@ pub fn main() -> Result<(), JsValue> { #[wasm_bindgen] pub fn format(before: String, path: String) -> String { - alejandra_engine::format::string_or_passthrough(path, before) + alejandra_engine::format::in_memory(path, before).1 } diff --git a/src/alejandra_cli/src/cli.rs b/src/alejandra_cli/src/cli.rs index 6b68804..75b5507 100644 --- a/src/alejandra_cli/src/cli.rs +++ b/src/alejandra_cli/src/cli.rs @@ -1,3 +1,9 @@ +#[derive(Clone)] +pub struct FormattedPath { + pub path: String, + pub status: alejandra_engine::format::Status, +} + pub fn parse(args: Vec) -> clap::ArgMatches { clap::Command::new("Alejandra") .about("The Uncompromising Nix Code Formatter.") @@ -15,6 +21,11 @@ pub fn parse(args: Vec) -> clap::ArgMatches { .multiple_occurrences(true) .takes_value(true), ) + .arg( + clap::Arg::new("check") + .help("Check if the input is already formatted.") + .long("--check"), + ) .term_width(80) .after_help(indoc::indoc!( // Let's just use the same sorting as on GitHub @@ -24,65 +35,65 @@ pub fn parse(args: Vec) -> clap::ArgMatches { // // Feel free to add here your contact/blog/links if you want " + The program will exit with status code: + 1, if any error occurs. + 2, if --check was used and any file was changed. + 0, otherwise. + Shaped with love by: - - Kevin Amado ~ @kamadorueda on GitHub, matrix.org and Gmail. - - Thomas Bereknyei ~ @tomberek on GitHub and matrix.org. - - David Arnold ~ @blaggacao on GitHub and matrix.org. - - Vincent Ambo ~ @tazjin on GitHub. - - Mr Hedgehog ~ @ModdedGamers on GitHub. + Kevin Amado ~ @kamadorueda on GitHub, matrix.org and Gmail. + Thomas Bereknyei ~ @tomberek on GitHub and matrix.org. + David Arnold ~ @blaggacao on GitHub and matrix.org. + Vincent Ambo ~ @tazjin on GitHub. + Mr Hedgehog ~ @ModdedGamers on GitHub. " )) .get_matches_from(args) } -pub fn stdin() -> std::io::Result<()> { +pub fn stdin() -> FormattedPath { use std::io::Read; + let mut before = String::new(); + let path = "".to_string(); + eprintln!("Formatting stdin, run with --help to see all options."); - let mut stdin = String::new(); - std::io::stdin().read_to_string(&mut stdin).unwrap(); - let stdout = alejandra_engine::format::string("stdin".to_string(), stdin)?; - print!("{}", stdout); + std::io::stdin().read_to_string(&mut before).unwrap(); - Ok(()) + let (status, data) = + alejandra_engine::format::in_memory(path.clone(), before.clone()); + + print!("{}", data); + + FormattedPath { path, status } } -pub fn simple(paths: Vec) -> std::io::Result<()> { +pub fn simple(paths: Vec) -> Vec { use rayon::prelude::*; - eprintln!("Formatting {} files.", paths.len()); + eprintln!("Formatting: {} files", paths.len()); - let (results, errors): (Vec<_>, Vec<_>) = paths + paths .par_iter() - .map(|path| -> std::io::Result { - alejandra_engine::format::file(path.to_string()).map(|changed| { + .map(|path| { + let status = alejandra_engine::format::in_place(path.clone()); + + if let alejandra_engine::format::Status::Changed(changed) = status { if changed { - eprintln!("Formatted: {}", &path); + eprintln!("Changed: {}", &path); } - changed - }) + } + + FormattedPath { path: path.clone(), status } }) - .partition(Result::is_ok); - - eprintln!( - "Changed: {}", - results.into_iter().map(Result::unwrap).filter(|&x| x).count(), - ); - eprintln!("Errors: {}", errors.len(),); - - Ok(()) + .collect() } -pub fn tui(paths: Vec) -> std::io::Result<()> { +pub fn tui(paths: Vec) -> std::io::Result> { use rayon::prelude::*; use termion::{input::TermRead, raw::IntoRawMode}; - struct FormattedPath { - path: String, - result: std::io::Result, - } - enum Event { FormattedPath(FormattedPath), FormattingFinished, @@ -91,11 +102,12 @@ pub fn tui(paths: Vec) -> std::io::Result<()> { } let paths_to_format = paths.len(); + let mut formatted_paths = std::collections::LinkedList::new(); let stdout = std::io::stderr().into_raw_mode()?; - // let stdout = termion::screen::AlternateScreen::from(stdout); let backend = tui::backend::TermionBackend::new(stdout); let mut terminal = tui::Terminal::new(backend)?; + terminal.clear()?; let (sender, receiver) = std::sync::mpsc::channel(); @@ -128,10 +140,10 @@ pub fn tui(paths: Vec) -> std::io::Result<()> { let sender_finished = sender; std::thread::spawn(move || { paths.into_par_iter().for_each_with(sender_paths, |sender, path| { - let result = alejandra_engine::format::file(path.clone()); + let status = alejandra_engine::format::in_place(path.clone()); if let Err(error) = sender - .send(Event::FormattedPath(FormattedPath { path, result })) + .send(Event::FormattedPath(FormattedPath { path, status })) { eprintln!("{}", error); } @@ -142,36 +154,32 @@ pub fn tui(paths: Vec) -> std::io::Result<()> { } }); - terminal.clear()?; - let mut finished = false; let mut paths_with_errors: usize = 0; let mut paths_changed: usize = 0; let mut paths_unchanged: usize = 0; - let mut formatted_paths = std::collections::LinkedList::new(); while !finished { loop { if let Ok(event) = receiver.try_recv() { match event { Event::FormattedPath(formatted_path) => { - match formatted_path.result { - Ok(changed) => { - if changed { + match &formatted_path.status { + alejandra_engine::format::Status::Changed( + changed, + ) => { + if *changed { paths_changed += 1; } else { paths_unchanged += 1; } } - Err(_) => { + alejandra_engine::format::Status::Error(_) => { paths_with_errors += 1; } }; formatted_paths.push_back(formatted_path); - if formatted_paths.len() > 8 { - formatted_paths.pop_front(); - } } Event::FormattingFinished => { finished = true; @@ -253,23 +261,29 @@ pub fn tui(paths: Vec) -> std::io::Result<()> { let logger = tui::widgets::Paragraph::new( formatted_paths .iter() + .rev() + .take(8) .map(|formatted_path| { tui::text::Spans::from(vec![ - match &formatted_path.result { - Ok(changed) => tui::text::Span::styled( - if *changed { - "CHANGED " + match formatted_path.status { + alejandra_engine::format::Status::Changed( + changed, + ) => tui::text::Span::styled( + if changed { + "CHANGED " } else { - "UNCHANGED " + "OK " }, tui::style::Style::default() .fg(tui::style::Color::Green), ), - Err(_) => tui::text::Span::styled( - "ERROR ", - tui::style::Style::default() - .fg(tui::style::Color::Red), - ), + alejandra_engine::format::Status::Error(_) => { + tui::text::Span::styled( + "ERROR ", + tui::style::Style::default() + .fg(tui::style::Color::Red), + ) + } }, tui::text::Span::raw(formatted_path.path.clone()), ]) @@ -289,5 +303,5 @@ pub fn tui(paths: Vec) -> std::io::Result<()> { })?; } - Ok(()) + Ok(formatted_paths.iter().cloned().collect()) } diff --git a/src/alejandra_cli/src/main.rs b/src/alejandra_cli/src/main.rs index 384b22f..a9ace90 100644 --- a/src/alejandra_cli/src/main.rs +++ b/src/alejandra_cli/src/main.rs @@ -1,7 +1,9 @@ fn main() -> std::io::Result<()> { let matches = alejandra_cli::cli::parse(std::env::args().collect()); - match matches.values_of("include") { + let check = matches.is_present("check"); + + let formatted_paths = match matches.values_of("include") { Some(include) => { let include = include.collect(); let exclude = match matches.values_of("exclude") { @@ -16,15 +18,65 @@ fn main() -> std::io::Result<()> { && atty::is(atty::Stream::Stdin) && atty::is(atty::Stream::Stdout) { - alejandra_cli::cli::tui(paths)?; + alejandra_cli::cli::tui(paths)? } else { - alejandra_cli::cli::simple(paths)?; + alejandra_cli::cli::simple(paths) } } - None => { - alejandra_cli::cli::stdin()?; + None => vec![alejandra_cli::cli::stdin()], + }; + + let errors = formatted_paths + .iter() + .filter(|formatted_path| { + matches!( + formatted_path.status, + alejandra_engine::format::Status::Error(_) + ) + }) + .count(); + + if errors > 0 { + eprintln!(); + eprintln!( + "Failed! We encountered {} error{} at:", + errors, + if errors > 0 { "s" } else { "" } + ); + for formatted_path in formatted_paths { + if let alejandra_engine::format::Status::Error(error) = + formatted_path.status + { + eprintln!(" {}: {}", formatted_path.path, &error); + } + } + std::process::exit(1); + } + + let changed = formatted_paths + .iter() + .filter(|formatted_path| match formatted_path.status { + alejandra_engine::format::Status::Changed(changed) => changed, + _ => false, + }) + .count(); + + if changed > 0 { + eprintln!(); + eprintln!( + "Success! {} file{} {} changed", + changed, + if changed > 0 { "s" } else { "" }, + if changed > 0 { "were" } else { "was" }, + ); + if check { + std::process::exit(2); + } else { + std::process::exit(0); } } + eprintln!(); + eprintln!("Success! Your code complies the Alejandra style"); std::process::exit(0); } diff --git a/src/alejandra_engine/src/format.rs b/src/alejandra_engine/src/format.rs index e43b81a..594c15e 100644 --- a/src/alejandra_engine/src/format.rs +++ b/src/alejandra_engine/src/format.rs @@ -1,42 +1,56 @@ -pub fn string(path: String, string: String) -> std::io::Result { - let tokens = rnix::tokenizer::Tokenizer::new(&string); +#[derive(Clone)] +pub enum Status { + Error(String), + Changed(bool), +} + +impl From for Status { + fn from(error: std::io::Error) -> Status { + Status::Error(error.to_string()) + } +} + +pub fn in_memory(path: String, before: String) -> (Status, String) { + let tokens = rnix::tokenizer::Tokenizer::new(&before); let ast = rnix::parser::parse(tokens); let errors = ast.errors(); if !errors.is_empty() { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - errors[0].to_string(), - )); + return (Status::Error(errors[0].to_string()), before); } - let green_node = - crate::builder::build(ast.node().into(), false, path, true).unwrap(); + let after = crate::builder::build(ast.node().into(), false, path, true) + .unwrap() + .to_string(); - Ok(green_node.to_string()) -} - -pub fn string_or_passthrough(path: String, before: String) -> String { - match crate::format::string(path, before.clone()) { - Ok(after) => after, - Err(_) => before, + if before == after { + (Status::Changed(false), after) + } else { + (Status::Changed(true), after) } } -pub fn file(path: String) -> std::io::Result { +pub fn in_place(path: String) -> Status { use std::io::Write; - let input = std::fs::read_to_string(&path)?; - let input_clone = input.clone(); - let input_bytes = input_clone.as_bytes(); + match std::fs::read_to_string(&path) { + Ok(before) => { + let (status, data) = crate::format::in_memory(path.clone(), before); - let output = crate::format::string(path.clone(), input)?; - let output_bytes = output.as_bytes(); + if let Status::Changed(changed) = status { + if changed { + return match std::fs::File::create(path) { + Ok(mut file) => match file.write_all(data.as_bytes()) { + Ok(_) => status, + Err(error) => Status::from(error), + }, + Err(error) => Status::from(error), + }; + } + } - let changed = input_bytes != output_bytes; - if changed { - std::fs::File::create(path)?.write_all(output_bytes)?; + status + } + Err(error) => Status::from(error), } - - Ok(changed) } diff --git a/src/alejandra_engine/tests/fmt.rs b/src/alejandra_engine/tests/fmt.rs index a03e53a..3ab6778 100644 --- a/src/alejandra_engine/tests/fmt.rs +++ b/src/alejandra_engine/tests/fmt.rs @@ -14,10 +14,8 @@ fn cases() { let path_in = format!("tests/cases/{}/in", case); let path_out = format!("tests/cases/{}/out", case); let content_in = std::fs::read_to_string(path_in.clone()).unwrap(); - let content_got = alejandra_engine::format::string_or_passthrough( - path_in, - content_in.clone(), - ); + let content_got = + alejandra_engine::format::in_memory(path_in, content_in.clone()).1; if should_update { std::fs::File::create(&path_out)