From 8677492d56de68ef69d54dc0315eecb771636da9 Mon Sep 17 00:00:00 2001 From: Oleksii Dorozhkin Date: Tue, 8 Jun 2021 11:01:27 +0300 Subject: [PATCH] Add `--fmt` subcommand (#837) --- completions/just.bash | 2 +- completions/just.elvish | 1 + completions/just.fish | 1 + completions/just.powershell | 1 + completions/just.zsh | 1 + src/alias.rs | 2 +- src/analyzer.rs | 1 + src/assignment.rs | 9 + src/binding.rs | 2 +- src/common.rs | 33 +- src/config.rs | 56 ++- src/expression.rs | 6 +- src/fragment.rs | 2 +- src/item.rs | 15 +- src/justfile.rs | 12 +- src/line.rs | 2 +- src/module.rs | 22 +- src/node.rs | 7 + src/parameter.rs | 5 +- src/parser.rs | 44 +- src/recipe.rs | 6 +- src/set.rs | 8 +- src/setting.rs | 27 +- src/string_literal.rs | 2 +- src/subcommand.rs | 1 + src/testing.rs | 1 + src/thunk.rs | 2 +- src/unresolved_dependency.rs | 18 +- src/warning.rs | 2 +- tests/command.rs | 2 +- tests/conditional.rs | 8 +- tests/fmt.rs | 891 +++++++++++++++++++++++++++++++++++ tests/lib.rs | 1 + tests/misc.rs | 2 +- tests/test.rs | 2 +- 35 files changed, 1133 insertions(+), 64 deletions(-) create mode 100644 tests/fmt.rs diff --git a/completions/just.bash b/completions/just.bash index 4de4bcf..1e152b8 100644 --- a/completions/just.bash +++ b/completions/just.bash @@ -20,7 +20,7 @@ _just() { case "${cmd}" in just) - opts=" -q -u -v -e -l -h -V -f -d -c -s --dry-run --highlight --no-dotenv --no-highlight --quiet --shell-command --clear-shell-args --unsorted --verbose --choose --dump --edit --evaluate --init --list --summary --variables --help --version --chooser --color --list-heading --list-prefix --justfile --set --shell --shell-arg --working-directory --command --completions --show ... " + opts=" -q -u -v -e -l -h -V -f -d -c -s --dry-run --highlight --no-dotenv --no-highlight --quiet --shell-command --clear-shell-args --unsorted --verbose --choose --dump --edit --evaluate --fmt --init --list --summary --variables --help --version --chooser --color --list-heading --list-prefix --justfile --set --shell --shell-arg --working-directory --command --completions --show ... " if [[ ${cur} == -* ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 diff --git a/completions/just.elvish b/completions/just.elvish index a2c3fe1..05ebf70 100644 --- a/completions/just.elvish +++ b/completions/just.elvish @@ -47,6 +47,7 @@ edit:completion:arg-completer[just] = [@words]{ cand -e 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`' cand --edit 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`' cand --evaluate 'Evaluate and print all variables. If a variable name is given as an argument, only print that variable''s value.' + cand --fmt 'Format and overwrite justfile' cand --init 'Initialize new justfile in project root' cand -l 'List available recipes and their arguments' cand --list 'List available recipes and their arguments' diff --git a/completions/just.fish b/completions/just.fish index 83f2267..887910c 100644 --- a/completions/just.fish +++ b/completions/just.fish @@ -34,6 +34,7 @@ complete -c just -n "__fish_use_subcommand" -l choose -d 'Select one or more rec complete -c just -n "__fish_use_subcommand" -l dump -d 'Print entire justfile' complete -c just -n "__fish_use_subcommand" -s e -l edit -d 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`' complete -c just -n "__fish_use_subcommand" -l evaluate -d 'Evaluate and print all variables. If a variable name is given as an argument, only print that variable\'s value.' +complete -c just -n "__fish_use_subcommand" -l fmt -d 'Format and overwrite justfile' complete -c just -n "__fish_use_subcommand" -l init -d 'Initialize new justfile in project root' complete -c just -n "__fish_use_subcommand" -s l -l list -d 'List available recipes and their arguments' complete -c just -n "__fish_use_subcommand" -l summary -d 'List names of available recipes' diff --git a/completions/just.powershell b/completions/just.powershell index 64d8990..ed91762 100644 --- a/completions/just.powershell +++ b/completions/just.powershell @@ -52,6 +52,7 @@ Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock { [CompletionResult]::new('-e', 'e', [CompletionResultType]::ParameterName, 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`') [CompletionResult]::new('--edit', 'edit', [CompletionResultType]::ParameterName, 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`') [CompletionResult]::new('--evaluate', 'evaluate', [CompletionResultType]::ParameterName, 'Evaluate and print all variables. If a variable name is given as an argument, only print that variable''s value.') + [CompletionResult]::new('--fmt', 'fmt', [CompletionResultType]::ParameterName, 'Format and overwrite justfile') [CompletionResult]::new('--init', 'init', [CompletionResultType]::ParameterName, 'Initialize new justfile in project root') [CompletionResult]::new('-l', 'l', [CompletionResultType]::ParameterName, 'List available recipes and their arguments') [CompletionResult]::new('--list', 'list', [CompletionResultType]::ParameterName, 'List available recipes and their arguments') diff --git a/completions/just.zsh b/completions/just.zsh index d31d1a5..5d83246 100644 --- a/completions/just.zsh +++ b/completions/just.zsh @@ -48,6 +48,7 @@ _just() { '-e[Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`]' \ '--edit[Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`]' \ '--evaluate[Evaluate and print all variables. If a variable name is given as an argument, only print that variable'\''s value.]' \ +'--fmt[Format and overwrite justfile]' \ '--init[Initialize new justfile in project root]' \ '-l[List available recipes and their arguments]' \ '--list[List available recipes and their arguments]' \ diff --git a/src/alias.rs b/src/alias.rs index 37eef0c..5dafe12 100644 --- a/src/alias.rs +++ b/src/alias.rs @@ -1,7 +1,7 @@ use crate::common::*; /// An alias, e.g. `name := target` -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub(crate) struct Alias<'src, T = Rc>> { pub(crate) name: Name<'src>, pub(crate) target: T, diff --git a/src/analyzer.rs b/src/analyzer.rs index 55cf648..fbf32d9 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -29,6 +29,7 @@ impl<'src> Analyzer<'src> { self.analyze_assignment(&assignment)?; self.assignments.insert(assignment); }, + Item::Comment(_) => (), Item::Recipe(recipe) => { self.analyze_recipe(&recipe)?; self.recipes.insert(recipe); diff --git a/src/assignment.rs b/src/assignment.rs index 52c2e9c..5ea7bc1 100644 --- a/src/assignment.rs +++ b/src/assignment.rs @@ -2,3 +2,12 @@ use crate::common::*; /// An assignment, e.g `foo := bar` pub(crate) type Assignment<'src> = Binding<'src, Expression<'src>>; + +impl<'src> Display for Assignment<'src> { + fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { + if self.export { + write!(f, "export ")?; + } + write!(f, "{} := {}", self.name, self.value) + } +} diff --git a/src/binding.rs b/src/binding.rs index 85dbd4a..185c4ad 100644 --- a/src/binding.rs +++ b/src/binding.rs @@ -1,7 +1,7 @@ use crate::common::*; /// A binding of `name` to `value` -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub(crate) struct Binding<'src, V = String> { /// Export binding as an environment variable to child processes pub(crate) export: bool, diff --git a/src/common.rs b/src/common.rs index d7d0db1..95c13c7 100644 --- a/src/common.rs +++ b/src/common.rs @@ -5,9 +5,10 @@ pub(crate) use std::{ env, ffi::{OsStr, OsString}, fmt::{self, Debug, Display, Formatter}, - fs, + fs::{self, File}, io::{self, Cursor, Write}, iter::{self, FromIterator}, + mem, ops::{Index, Range, RangeInclusive}, path::{Path, PathBuf}, process::{self, Command, Stdio}, @@ -45,21 +46,21 @@ pub(crate) use crate::{ alias::Alias, analyzer::Analyzer, assignment::Assignment, assignment_resolver::AssignmentResolver, binding::Binding, color::Color, compilation_error::CompilationError, compilation_error_kind::CompilationErrorKind, - compiler::Compiler, config::Config, config_error::ConfigError, count::Count, - delimiter::Delimiter, dependency::Dependency, enclosure::Enclosure, evaluator::Evaluator, - expression::Expression, fragment::Fragment, function::Function, - function_context::FunctionContext, interrupt_guard::InterruptGuard, - interrupt_handler::InterruptHandler, item::Item, justfile::Justfile, keyword::Keyword, - lexer::Lexer, line::Line, list::List, load_error::LoadError, module::Module, name::Name, - output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind, parser::Parser, - platform::Platform, position::Position, positional::Positional, recipe::Recipe, - recipe_context::RecipeContext, recipe_resolver::RecipeResolver, runtime_error::RuntimeError, - scope::Scope, search::Search, search_config::SearchConfig, search_error::SearchError, set::Set, - setting::Setting, settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace, - string_kind::StringKind, string_literal::StringLiteral, subcommand::Subcommand, - suggestion::Suggestion, table::Table, thunk::Thunk, token::Token, token_kind::TokenKind, - unresolved_dependency::UnresolvedDependency, unresolved_recipe::UnresolvedRecipe, - use_color::UseColor, variables::Variables, verbosity::Verbosity, warning::Warning, + config::Config, config_error::ConfigError, count::Count, delimiter::Delimiter, + dependency::Dependency, enclosure::Enclosure, evaluator::Evaluator, expression::Expression, + fragment::Fragment, function::Function, function_context::FunctionContext, + interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item, + justfile::Justfile, keyword::Keyword, lexer::Lexer, line::Line, list::List, + load_error::LoadError, module::Module, name::Name, output_error::OutputError, + parameter::Parameter, parameter_kind::ParameterKind, parser::Parser, platform::Platform, + position::Position, positional::Positional, recipe::Recipe, recipe_context::RecipeContext, + recipe_resolver::RecipeResolver, runtime_error::RuntimeError, scope::Scope, search::Search, + search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting, + settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace, string_kind::StringKind, + string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table, + thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency, + unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables, + verbosity::Verbosity, warning::Warning, }; // type aliases diff --git a/src/config.rs b/src/config.rs index 883350f..79533b5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -38,6 +38,7 @@ mod cmd { pub(crate) const DUMP: &str = "DUMP"; pub(crate) const EDIT: &str = "EDIT"; pub(crate) const EVALUATE: &str = "EVALUATE"; + pub(crate) const FORMAT: &str = "FORMAT"; pub(crate) const INIT: &str = "INIT"; pub(crate) const LIST: &str = "LIST"; pub(crate) const SHOW: &str = "SHOW"; @@ -52,6 +53,7 @@ mod cmd { DUMP, EDIT, EVALUATE, + FORMAT, INIT, LIST, SHOW, @@ -63,6 +65,7 @@ mod cmd { COMPLETIONS, DUMP, EDIT, + FORMAT, INIT, LIST, SHOW, @@ -266,6 +269,11 @@ impl Config { "Evaluate and print all variables. If a variable name is given as an argument, only print \ that variable's value.", )) + .arg( + Arg::with_name(cmd::FORMAT) + .long("fmt") + .help("Format and overwrite justfile"), + ) .arg( Arg::with_name(cmd::INIT) .long("init") @@ -442,6 +450,8 @@ impl Config { Subcommand::Summary } else if matches.is_present(cmd::DUMP) { Subcommand::Dump + } else if matches.is_present(cmd::FORMAT) { + Subcommand::Format } else if matches.is_present(cmd::INIT) { Subcommand::Init } else if matches.is_present(cmd::LIST) { @@ -539,7 +549,9 @@ impl Config { }) .eprint(self.color)?; - let justfile = Compiler::compile(&src).eprint(self.color)?; + let tokens = Lexer::lex(&src).eprint(self.color)?; + let ast = Parser::parse(&tokens).eprint(self.color)?; + let justfile = Analyzer::analyze(ast.clone()).eprint(self.color)?; if self.verbosity.loud() { for warning in &justfile.warnings { @@ -555,8 +567,9 @@ impl Config { Choose { overrides, chooser } => self.choose(justfile, &search, overrides, chooser.as_deref())?, Command { overrides, .. } => self.run(justfile, &search, overrides, &[])?, - Dump => Self::dump(justfile), + Dump => Self::dump(ast)?, Evaluate { overrides, .. } => self.run(justfile, &search, overrides, &[])?, + Format => self.format(ast, &search)?, List => self.list(justfile), Run { arguments, @@ -676,8 +689,9 @@ impl Config { self.run(justfile, search, overrides, &recipes) } - fn dump(justfile: Justfile) { - println!("{}", justfile); + fn dump(ast: Module) -> Result<(), i32> { + print!("{}", ast); + Ok(()) } pub(crate) fn edit(&self, search: &Search) -> Result<(), i32> { @@ -713,6 +727,24 @@ impl Config { } } + fn format(&self, ast: Module, search: &Search) -> Result<(), i32> { + if let Err(error) = File::open(&search.justfile).and_then(|mut file| write!(file, "{}", ast)) { + if self.verbosity.loud() { + eprintln!( + "Failed to write justfile to `{}`: {}", + search.justfile.display(), + error + ); + } + Err(EXIT_FAILURE) + } else { + if self.verbosity.loud() { + eprintln!("Wrote justfile to `{}`", search.justfile.display()); + } + Ok(()) + } + } + pub(crate) fn init(&self) -> Result<(), i32> { let search = Search::init(&self.search_config, &self.invocation_directory).eprint(self.color)?; @@ -920,6 +952,7 @@ FLAGS: --evaluate Evaluate and print all variables. If a variable name is given as an \ argument, only print that variable's value. + --fmt Format and overwrite justfile --highlight Highlight echoed recipe lines in bold --init Initialize new justfile in project root -l, --list List available recipes and their arguments @@ -1315,6 +1348,11 @@ ARGS: args: ["--list", "--dump"], } + error! { + name: subcommand_conflict_fmt, + args: ["--list", "--fmt"], + } + error! { name: subcommand_conflict_init, args: ["--list", "--init"], @@ -1661,6 +1699,16 @@ ARGS: }, } + error! { + name: fmt_arguments, + args: ["--fmt", "bar"], + error: ConfigError::SubcommandArguments { subcommand, arguments }, + check: { + assert_eq!(subcommand, cmd::FORMAT); + assert_eq!(arguments, &["bar"]); + }, + } + error! { name: init_arguments, args: ["--init", "bar"], diff --git a/src/expression.rs b/src/expression.rs index fe8ff00..67a54e6 100644 --- a/src/expression.rs +++ b/src/expression.rs @@ -6,7 +6,7 @@ use crate::common::*; /// parenthetical groups). /// /// The parser parses both values and expressions into `Expression`s. -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone)] pub(crate) enum Expression<'src> { /// `contents` Backtick { @@ -45,7 +45,7 @@ impl<'src> Expression<'src> { impl<'src> Display for Expression<'src> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { match self { - Expression::Backtick { contents, .. } => write!(f, "`{}`", contents), + Expression::Backtick { token, .. } => write!(f, "{}", token.lexeme()), Expression::Concatination { lhs, rhs } => write!(f, "{} + {}", lhs, rhs), Expression::Conditional { lhs, @@ -55,7 +55,7 @@ impl<'src> Display for Expression<'src> { inverted, } => write!( f, - "if {} {} {} {{ {} }} else {{ {} }} ", + "if {} {} {} {{ {} }} else {{ {} }}", lhs, if *inverted { "!=" } else { "==" }, rhs, diff --git a/src/fragment.rs b/src/fragment.rs index 48aec9f..e43dfa2 100644 --- a/src/fragment.rs +++ b/src/fragment.rs @@ -1,7 +1,7 @@ use crate::common::*; /// A line fragment consisting either of… -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone)] pub(crate) enum Fragment<'src> { /// …raw text… Text { token: Token<'src> }, diff --git a/src/item.rs b/src/item.rs index 0af1d0b..21fa54a 100644 --- a/src/item.rs +++ b/src/item.rs @@ -1,10 +1,23 @@ use crate::common::*; /// A single top-level item -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) enum Item<'src> { Alias(Alias<'src, Name<'src>>), Assignment(Assignment<'src>), + Comment(&'src str), Recipe(UnresolvedRecipe<'src>), Set(Set<'src>), } + +impl<'src> Display for Item<'src> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Item::Alias(alias) => write!(f, "{}", alias), + Item::Assignment(assignment) => write!(f, "{}", assignment), + Item::Comment(comment) => write!(f, "{}", comment), + Item::Recipe(recipe) => write!(f, "{}", recipe), + Item::Set(set) => write!(f, "{}", set), + } + } +} diff --git a/src/justfile.rs b/src/justfile.rs index cfd3cee..08a8bd2 100644 --- a/src/justfile.rs +++ b/src/justfile.rs @@ -802,7 +802,7 @@ goodbye := \"y\" hello a b c: x y z #! blah #blarg - {{foo + bar}}abc{{goodbye + \"x\"}}xyz + {{ foo + bar }}abc{{ goodbye + \"x\" }}xyz 1 2 3 @@ -828,7 +828,7 @@ install: install: #!/bin/sh - if [[ -f {{practicum}} ]]; then + if [[ -f {{ practicum }} ]]; then \treturn fi", } @@ -869,7 +869,7 @@ c := a + b + a + b", r#"a: echo {{ `echo hello` + "blarg" }} {{ `echo bob` }}"#, r#"a: - echo {{`echo hello` + "blarg"}} {{`echo bob`}}"#, + echo {{ `echo hello` + "blarg" }} {{ `echo bob` }}"#, } test! { @@ -895,7 +895,7 @@ c := a + b + a + b", "a b c: {{b}} {{c}}", "a b c: - {{b}} {{c}}", + {{ b }} {{ c }}", } test! { @@ -908,7 +908,7 @@ a: "x := arch() a: - {{os()}} {{os_family()}}", + {{ os() }} {{ os_family() }}", } test! { @@ -921,7 +921,7 @@ a: r#"x := env_var('foo') a: - {{env_var_or_default('foo' + 'bar', 'baz')}} {{env_var(env_var("baz"))}}"#, + {{ env_var_or_default('foo' + 'bar', 'baz') }} {{ env_var(env_var("baz")) }}"#, } test! { diff --git a/src/line.rs b/src/line.rs index f296627..7b4b4c8 100644 --- a/src/line.rs +++ b/src/line.rs @@ -1,7 +1,7 @@ use crate::common::*; /// A single line in a recipe body, consisting of any number of `Fragment`s. -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub(crate) struct Line<'src> { pub(crate) fragments: Vec>, } diff --git a/src/module.rs b/src/module.rs index a2f0640..d83a2b5 100644 --- a/src/module.rs +++ b/src/module.rs @@ -7,10 +7,30 @@ use crate::common::*; /// Not all successful parses result in valid justfiles, so additional /// consistency checks and name resolution are performed by the `Analyzer`, /// which produces a `Justfile` from a `Module`. -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct Module<'src> { /// Items in the justfile pub(crate) items: Vec>, /// Non-fatal warnings encountered during parsing pub(crate) warnings: Vec, } + +impl<'src> Display for Module<'src> { + fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { + let mut iter = self.items.iter().peekable(); + + while let Some(item) = iter.next() { + writeln!(f, "{}", item)?; + + if let Some(next_item) = iter.peek() { + if matches!(item, Item::Recipe(_)) + || mem::discriminant(item) != mem::discriminant(next_item) + { + writeln!(f)?; + } + } + } + + Ok(()) + } +} diff --git a/src/node.rs b/src/node.rs index 19be563..aebfbbf 100644 --- a/src/node.rs +++ b/src/node.rs @@ -20,6 +20,7 @@ impl<'src> Node<'src> for Item<'src> { match self { Item::Alias(alias) => alias.tree(), Item::Assignment(assignment) => assignment.tree(), + Item::Comment(comment) => comment.tree(), Item::Recipe(recipe) => recipe.tree(), Item::Set(set) => set.tree(), } @@ -210,3 +211,9 @@ impl<'src> Node<'src> for Warning { unreachable!() } } + +impl<'src> Node<'src> for str { + fn tree(&self) -> Tree<'src> { + Tree::atom("comment").push(["\"", self, "\""].concat()) + } +} diff --git a/src/parameter.rs b/src/parameter.rs index 1806eed..ee0d93b 100644 --- a/src/parameter.rs +++ b/src/parameter.rs @@ -1,7 +1,7 @@ use crate::common::*; /// A single function parameter -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone)] pub(crate) struct Parameter<'src> { /// The parameter name pub(crate) name: Name<'src>, @@ -16,6 +16,9 @@ pub(crate) struct Parameter<'src> { impl<'src> Display for Parameter<'src> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { let color = Color::fmt(f); + if self.export { + write!(f, "$")?; + } if let Some(prefix) = self.kind.prefix() { write!(f, "{}", color.annotation().paint(prefix))?; } diff --git a/src/parser.rs b/src/parser.rs index 6f1286b..45c4c80 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -296,17 +296,34 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { /// Parse a justfile, consumes self fn parse_justfile(mut self) -> CompilationResult<'src, Module<'src>> { + fn pop_doc_comment<'src>( + items: &mut Vec>, + eol_since_last_comment: bool, + ) -> Option<&'src str> { + if !eol_since_last_comment { + if let Some(Item::Comment(contents)) = items.last() { + let doc = Some(contents[1..].trim_start()); + items.pop(); + return doc; + } + } + + None + } + let mut items = Vec::new(); - let mut doc = None; + let mut eol_since_last_comment = false; loop { let next = self.next()?; if let Some(comment) = self.accept(Comment)? { - doc = Some(comment.lexeme()[1..].trim()); + items.push(Item::Comment(comment.lexeme().trim_end())); self.expect_eol()?; + eol_since_last_comment = false; } else if self.accepted(Eol)? { + eol_since_last_comment = true; } else if self.accepted(Eof)? { break; } else if self.next_is(Identifier) { @@ -317,6 +334,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } else if self.next_are(&[Identifier, Identifier, ColonEquals]) { items.push(Item::Alias(self.parse_alias()?)); } else { + let doc = pop_doc_comment(&mut items, eol_since_last_comment); items.push(Item::Recipe(self.parse_recipe(doc, false)?)); }, Some(Keyword::Export) => @@ -326,6 +344,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { self.presume_keyword(Keyword::Export)?; items.push(Item::Assignment(self.parse_assignment(true)?)); } else { + let doc = pop_doc_comment(&mut items, eol_since_last_comment); items.push(Item::Recipe(self.parse_recipe(doc, false)?)); }, Some(Keyword::Set) => @@ -335,6 +354,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { { items.push(Item::Set(self.parse_set()?)); } else { + let doc = pop_doc_comment(&mut items, eol_since_last_comment); items.push(Item::Recipe(self.parse_recipe(doc, false)?)); }, _ => @@ -343,18 +363,16 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } else if self.next_are(&[Identifier, ColonEquals]) { items.push(Item::Assignment(self.parse_assignment(false)?)); } else { + let doc = pop_doc_comment(&mut items, eol_since_last_comment); items.push(Item::Recipe(self.parse_recipe(doc, false)?)); }, } } else if self.accepted(At)? { + let doc = pop_doc_comment(&mut items, eol_since_last_comment); items.push(Item::Recipe(self.parse_recipe(doc, true)?)); } else { return Err(self.unexpected_token()?); } - - if next.kind != Comment { - doc = None; - } } if self.next != self.tokens.len() { @@ -1097,11 +1115,17 @@ mod tests { test! { name: comment, text: "# foo", - tree: (justfile), + tree: (justfile (comment "# foo")), } test! { - name: comment_alias, + name: comment_before_alias, + text: "# foo\nalias x := y", + tree: (justfile (comment "# foo") (alias x y)), + } + + test! { + name: comment_after_alias, text: "alias x := y # foo", tree: (justfile (alias x y)), } @@ -1166,7 +1190,7 @@ mod tests { x := y bar: ", - tree: (justfile (assignment x y) (recipe bar)), + tree: (justfile (comment "# foo") (assignment x y) (recipe bar)), } test! { @@ -1176,7 +1200,7 @@ mod tests { bar: ", - tree: (justfile (recipe bar)), + tree: (justfile (comment "# foo") (recipe bar)), } test! { diff --git a/src/recipe.rs b/src/recipe.rs index 993d2f8..c7b2d71 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -23,7 +23,7 @@ fn error_from_signal( } /// A recipe, e.g. `foo: bar baz` -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone)] pub(crate) struct Recipe<'src, D = Dependency<'src>> { pub(crate) dependencies: Vec, pub(crate) doc: Option<&'src str>, @@ -314,7 +314,7 @@ impl<'src, D> Keyed<'src> for Recipe<'src, D> { } } -impl<'src> Display for Recipe<'src> { +impl<'src, D: Display> Display for Recipe<'src, D> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { if let Some(doc) = self.doc { writeln!(f, "# {}", doc)?; @@ -344,7 +344,7 @@ impl<'src> Display for Recipe<'src> { } match fragment { Fragment::Text { token } => write!(f, "{}", token.lexeme())?, - Fragment::Interpolation { expression, .. } => write!(f, "{{{{{}}}}}", expression)?, + Fragment::Interpolation { expression, .. } => write!(f, "{{{{ {} }}}}", expression)?, } } if i + 1 < self.body.len() { diff --git a/src/set.rs b/src/set.rs index ade63fe..b72887a 100644 --- a/src/set.rs +++ b/src/set.rs @@ -1,6 +1,6 @@ use crate::common::*; -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct Set<'src> { pub(crate) name: Name<'src>, pub(crate) value: Setting<'src>, @@ -11,3 +11,9 @@ impl<'src> Keyed<'src> for Set<'src> { self.name.lexeme() } } + +impl<'src> Display for Set<'src> { + fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { + write!(f, "set {} := {}", self.name, self.value) + } +} diff --git a/src/setting.rs b/src/setting.rs index 011e3c0..8f6b5b0 100644 --- a/src/setting.rs +++ b/src/setting.rs @@ -1,6 +1,6 @@ use crate::common::*; -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) enum Setting<'src> { DotenvLoad(bool), Export(bool), @@ -8,8 +8,31 @@ pub(crate) enum Setting<'src> { Shell(Shell<'src>), } -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub(crate) struct Shell<'src> { pub(crate) command: StringLiteral<'src>, pub(crate) arguments: Vec>, } + +impl<'src> Display for Setting<'src> { + fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { + match self { + Setting::DotenvLoad(value) => write!(f, "{}", value), + Setting::Export(value) => write!(f, "{}", value), + Setting::PositionalArguments(value) => write!(f, "{}", value), + Setting::Shell(shell) => write!(f, "{}", shell), + } + } +} + +impl<'src> Display for Shell<'src> { + fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { + write!(f, "[{}", self.command)?; + + for argument in &self.arguments { + write!(f, ", {}", argument)?; + } + + write!(f, "]") + } +} diff --git a/src/string_literal.rs b/src/string_literal.rs index 7034366..115d3e2 100644 --- a/src/string_literal.rs +++ b/src/string_literal.rs @@ -1,6 +1,6 @@ use crate::common::*; -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone)] pub(crate) struct StringLiteral<'src> { pub(crate) kind: StringKind, pub(crate) raw: &'src str, diff --git a/src/subcommand.rs b/src/subcommand.rs index a3084c6..ee5bd1c 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -20,6 +20,7 @@ pub(crate) enum Subcommand { overrides: BTreeMap, variable: Option, }, + Format, Init, List, Run { diff --git a/src/testing.rs b/src/testing.rs index 1ac4c45..bbf7d29 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -1,5 +1,6 @@ use crate::common::*; +use crate::compiler::Compiler; use pretty_assertions::assert_eq; pub(crate) fn compile(text: &str) -> Justfile { diff --git a/src/thunk.rs b/src/thunk.rs index 991b62f..bbc45c7 100644 --- a/src/thunk.rs +++ b/src/thunk.rs @@ -1,7 +1,7 @@ use crate::common::*; #[derive(Derivative)] -#[derivative(Debug, PartialEq = "feature_allow_slow_enum")] +#[derivative(Debug, Clone, PartialEq = "feature_allow_slow_enum")] pub(crate) enum Thunk<'src> { Nullary { name: Name<'src>, diff --git a/src/unresolved_dependency.rs b/src/unresolved_dependency.rs index de35f12..47f0dea 100644 --- a/src/unresolved_dependency.rs +++ b/src/unresolved_dependency.rs @@ -1,7 +1,23 @@ use crate::common::*; -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone)] pub(crate) struct UnresolvedDependency<'src> { pub(crate) recipe: Name<'src>, pub(crate) arguments: Vec>, } + +impl<'src> Display for UnresolvedDependency<'src> { + fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { + if self.arguments.is_empty() { + write!(f, "{}", self.recipe) + } else { + write!(f, "({}", self.recipe)?; + + for argument in &self.arguments { + write!(f, " {}", argument)?; + } + + write!(f, ")") + } + } +} diff --git a/src/warning.rs b/src/warning.rs index 455bc98..da3c8b1 100644 --- a/src/warning.rs +++ b/src/warning.rs @@ -1,6 +1,6 @@ use crate::common::*; -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub(crate) enum Warning { // Remove this on 2021-07-01. #[allow(dead_code)] diff --git a/tests/command.rs b/tests/command.rs index f740135..f3caca9 100644 --- a/tests/command.rs +++ b/tests/command.rs @@ -33,7 +33,7 @@ test! { USAGE: just{} --color --shell --shell-arg ... \ <--choose|--command |--completions |--dump|--edit|\ - --evaluate|--init|--list|--show |--summary|--variables> + --evaluate|--fmt|--init|--list|--show |--summary|--variables> For more information try --help ", EXE_SUFFIX), diff --git a/tests/conditional.rs b/tests/conditional.rs index 7e02c63..f1b2e22 100644 --- a/tests/conditional.rs +++ b/tests/conditional.rs @@ -149,10 +149,10 @@ test! { echo {{ a }} ", args: ("--dump"), - stdout: format!(" - a := if '' == '' {{ '' }} else {{ '' }}{} + stdout: " + a := if '' == '' { '' } else { '' } foo: - echo {{{{a}}}} - ", " ").as_str(), + echo {{ a }} + ", } diff --git a/tests/fmt.rs b/tests/fmt.rs new file mode 100644 index 0000000..056f285 --- /dev/null +++ b/tests/fmt.rs @@ -0,0 +1,891 @@ +test! { + name: alias_good, + justfile: " + alias f := foo + + foo: + echo foo + ", + args: ("--dump"), + stdout: " + alias f := foo + + foo: + echo foo + ", +} + +test! { + name: alias_fix_indent, + justfile: " + alias f:= foo + + foo: + echo foo + ", + args: ("--dump"), + stdout: " + alias f := foo + + foo: + echo foo + ", +} + +test! { + name: assignment_singlequote, + justfile: " + foo := 'foo' + ", + args: ("--dump"), + stdout: " + foo := 'foo' + ", +} + +test! { + name: assignment_doublequote, + justfile: r#" + foo := "foo" + "#, + args: ("--dump"), + stdout: r#" + foo := "foo" + "#, +} + +test! { + name: assignment_indented_singlequote, + justfile: " + foo := ''' + foo + ''' + ", + args: ("--dump"), + stdout: r" + foo := ''' + foo + ''' + ", +} + +test! { + name: assignment_indented_doublequote, + justfile: r#" + foo := """ + foo + """ + "#, + args: ("--dump"), + stdout: r#" + foo := """ + foo + """ + "#, +} + +test! { + name: assignment_backtick, + justfile: " + foo := `foo` + ", + args: ("--dump"), + stdout: " + foo := `foo` + ", +} + +test! { + name: assignment_indented_backtick, + justfile: " + foo := ``` + foo + ``` + ", + args: ("--dump"), + stdout: " + foo := ``` + foo + ``` + ", +} + +test! { + name: assignment_name, + justfile: " + bar := 'bar' + foo := bar + ", + args: ("--dump"), + stdout: " + bar := 'bar' + foo := bar + ", +} + +test! { + name: assignment_parenthized_expression, + justfile: " + foo := ('foo') + ", + args: ("--dump"), + stdout: " + foo := ('foo') + ", +} + +test! { + name: assignment_export, + justfile: " + export foo := 'foo' + ", + args: ("--dump"), + stdout: " + export foo := 'foo' + ", +} + +test! { + name: assignment_concat_values, + justfile: " + foo := 'foo' + 'bar' + ", + args: ("--dump"), + stdout: " + foo := 'foo' + 'bar' + ", +} + +test! { + name: assignment_if_oneline, + justfile: " + foo := if 'foo' == 'foo' { 'foo' } else { 'bar' } + ", + args: ("--dump"), + stdout: " + foo := if 'foo' == 'foo' { 'foo' } else { 'bar' } + ", +} + +test! { + name: assignment_if_multiline, + justfile: " + foo := if 'foo' != 'foo' { + 'foo' + } else { + 'bar' + } + ", + args: ("--dump"), + stdout: " + foo := if 'foo' != 'foo' { 'foo' } else { 'bar' } + ", +} + +test! { + name: assignment_nullary_function, + justfile: " + foo := arch() + ", + args: ("--dump"), + stdout: " + foo := arch() + ", +} + +test! { + name: assignment_unary_function, + justfile: " + foo := env_var('foo') + ", + args: ("--dump"), + stdout: " + foo := env_var('foo') + ", +} + +test! { + name: assignment_binary_function, + justfile: " + foo := env_var_or_default('foo', 'bar') + ", + args: ("--dump"), + stdout: " + foo := env_var_or_default('foo', 'bar') + ", +} + +test! { + name: recipe_ordinary, + justfile: " + foo: + echo bar + ", + args: ("--dump"), + stdout: " + foo: + echo bar + ", +} + +test! { + name: recipe_with_docstring, + justfile: " + # bar + foo: + echo bar + ", + args: ("--dump"), + stdout: " + # bar + foo: + echo bar + ", +} + +test! { + name: recipe_with_comments_in_body, + justfile: " + foo: + # bar + echo bar + ", + args: ("--dump"), + stdout: " + foo: + # bar + echo bar + ", +} + +test! { + name: recipe_body_is_comment, + justfile: " + foo: + # bar + ", + args: ("--dump"), + stdout: " + foo: + # bar + ", +} + +test! { + name: recipe_several_commands, + justfile: " + foo: + echo bar + echo baz + ", + args: ("--dump"), + stdout: " + foo: + echo bar + echo baz + ", +} + +test! { + name: recipe_quiet, + justfile: " + @foo: + echo bar + ", + args: ("--dump"), + stdout: " + @foo: + echo bar + ", +} + +test! { + name: recipe_quiet_command, + justfile: " + foo: + @echo bar + ", + args: ("--dump"), + stdout: " + foo: + @echo bar + ", +} + +test! { + name: recipe_quiet_comment, + justfile: " + foo: + @# bar + ", + args: ("--dump"), + stdout: " + foo: + @# bar + ", +} + +test! { + name: recipe_ignore_errors, + justfile: " + foo: + -echo foo + ", + args: ("--dump"), + stdout: " + foo: + -echo foo + ", +} + +test! { + name: recipe_parameter, + justfile: " + foo BAR: + echo foo + ", + args: ("--dump"), + stdout: " + foo BAR: + echo foo + ", +} + +test! { + name: recipe_parameter_default, + justfile: " + foo BAR='bar': + echo foo + ", + args: ("--dump"), + stdout: " + foo BAR='bar': + echo foo + ", +} + +test! { + name: recipe_parameter_envar, + justfile: " + foo $BAR: + echo foo + ", + args: ("--dump"), + stdout: " + foo $BAR: + echo foo + ", +} + +test! { + name: recipe_parameter_default_envar, + justfile: " + foo $BAR='foo': + echo foo + ", + args: ("--dump"), + stdout: " + foo $BAR='foo': + echo foo + ", +} + +test! { + name: recipe_parameter_concat, + justfile: " + foo BAR=('bar' + 'baz'): + echo foo + ", + args: ("--dump"), + stdout: " + foo BAR=('bar' + 'baz'): + echo foo + ", +} + +test! { + name: recipe_parameters, + justfile: " + foo BAR BAZ: + echo foo + ", + args: ("--dump"), + stdout: " + foo BAR BAZ: + echo foo + ", +} + +test! { + name: recipe_parameters_envar, + justfile: " + foo $BAR $BAZ: + echo foo + ", + args: ("--dump"), + stdout: " + foo $BAR $BAZ: + echo foo + ", +} + +test! { + name: recipe_variadic_plus, + justfile: " + foo +BAR: + echo foo + ", + args: ("--dump"), + stdout: " + foo +BAR: + echo foo + ", +} + +test! { + name: recipe_variadic_star, + justfile: " + foo *BAR: + echo foo + ", + args: ("--dump"), + stdout: " + foo *BAR: + echo foo + ", +} + +test! { + name: recipe_positional_variadic, + justfile: " + foo BAR *BAZ: + echo foo + ", + args: ("--dump"), + stdout: " + foo BAR *BAZ: + echo foo + ", +} + +test! { + name: recipe_variadic_default, + justfile: " + foo +BAR='bar': + echo foo + ", + args: ("--dump"), + stdout: " + foo +BAR='bar': + echo foo + ", +} + +test! { + name: recipe_parameter_in_body, + justfile: " + foo BAR: + echo {{ BAR }} + ", + args: ("--dump"), + stdout: " + foo BAR: + echo {{ BAR }} + ", +} + +test! { + name: recipe_parameter_conditional, + justfile: " + foo BAR: + echo {{ if 'foo' == 'foo' { 'foo' } else { 'bar' } }} + ", + args: ("--dump"), + stdout: " + foo BAR: + echo {{ if 'foo' == 'foo' { 'foo' } else { 'bar' } }} + ", +} + +test! { + name: recipe_escaped_braces, + justfile: " + foo BAR: + echo '{{{{BAR}}}}' + ", + args: ("--dump"), + stdout: " + foo BAR: + echo '{{{{BAR}}}}' + ", +} + +test! { + name: recipe_assignment_in_body, + justfile: " + bar := 'bar' + + foo: + echo $bar + ", + args: ("--dump"), + stdout: " + bar := 'bar' + + foo: + echo $bar + ", +} + +test! { + name: recipe_dependency, + justfile: " + bar: + echo bar + + foo: bar + echo foo + ", + args: ("--dump"), + stdout: " + bar: + echo bar + + foo: bar + echo foo + ", +} + +test! { + name: recipe_dependency_param, + justfile: " + bar BAR: + echo bar + + foo: (bar 'bar') + echo foo + ", + args: ("--dump"), + stdout: " + bar BAR: + echo bar + + foo: (bar 'bar') + echo foo + ", +} + +test! { + name: recipe_dependency_params, + justfile: " + bar BAR BAZ: + echo bar + + foo: (bar 'bar' 'baz') + echo foo + ", + args: ("--dump"), + stdout: " + bar BAR BAZ: + echo bar + + foo: (bar 'bar' 'baz') + echo foo + ", +} + +test! { + name: recipe_dependencies, + justfile: " + bar: + echo bar + + baz: + echo baz + + foo: baz bar + echo foo + ", + args: ("--dump"), + stdout: " + bar: + echo bar + + baz: + echo baz + + foo: baz bar + echo foo + ", +} + +test! { + name: recipe_dependencies_params, + justfile: " + bar BAR: + echo bar + + baz BAZ: + echo baz + + foo: (baz 'baz') (bar 'bar') + echo foo + ", + args: ("--dump"), + stdout: " + bar BAR: + echo bar + + baz BAZ: + echo baz + + foo: (baz 'baz') (bar 'bar') + echo foo + ", +} + +test! { + name: set_true_explicit, + justfile: " + set export := true + ", + args: ("--dump"), + stdout: " + set export := true + ", +} + +test! { + name: set_true_implicit, + justfile: " + set export + ", + args: ("--dump"), + stdout: " + set export := true + ", +} + +test! { + name: set_false, + justfile: " + set export := false + ", + args: ("--dump"), + stdout: " + set export := false + ", +} + +test! { + name: set_shell, + justfile: r#" + set shell := ['sh', "-c"] + "#, + args: ("--dump"), + stdout: r#" + set shell := ['sh', "-c"] + "#, +} + +test! { + name: comment, + justfile: " + # foo + ", + args: ("--dump"), + stdout: " + # foo + ", +} + +test! { + name: comment_multiline, + justfile: " + # foo + # bar + ", + args: ("--dump"), + stdout: " + # foo + # bar + ", +} + +test! { + name: comment_leading, + justfile: " + # foo + + foo := 'bar' + ", + args: ("--dump"), + stdout: " + # foo + + foo := 'bar' + ", +} + +test! { + name: comment_trailing, + justfile: " + foo := 'bar' + + # foo + ", + args: ("--dump"), + stdout: " + foo := 'bar' + + # foo + ", +} + +test! { + name: comment_before_recipe, + justfile: " + # foo + + foo: + echo foo + ", + args: ("--dump"), + stdout: " + # foo + + foo: + echo foo + ", +} + +test! { + name: comment_before_docstring_recipe, + justfile: " + # bar + + # foo + foo: + echo foo + ", + args: ("--dump"), + stdout: " + # bar + + # foo + foo: + echo foo + ", +} + +test! { + name: group_recipies, + justfile: " + foo: + echo foo + bar: + echo bar + ", + args: ("--dump"), + stdout: " + foo: + echo foo + + bar: + echo bar + ", +} + +test! { + name: group_aliases, + justfile: " + alias f := foo + + alias b := bar + + foo: + echo foo + + bar: + echo bar + ", + args: ("--dump"), + stdout: " + alias f := foo + alias b := bar + + foo: + echo foo + + bar: + echo bar + ", +} + +test! { + name: group_assignments, + justfile: " + foo := 'foo' + bar := 'bar' + ", + args: ("--dump"), + stdout: " + foo := 'foo' + bar := 'bar' + ", +} + +test! { + name: group_sets, + justfile: " + set export := true + set positional-arguments := true + ", + args: ("--dump"), + stdout: " + set export := true + set positional-arguments := true + ", +} + +test! { + name: group_comments, + justfile: " + # foo + + # bar + ", + args: ("--dump"), + stdout: " + # foo + # bar + ", +} + +test! { + name: separate_recipes_aliases, + justfile: " + alias f := foo + foo: + echo foo + ", + args: ("--dump"), + stdout: " + alias f := foo + + foo: + echo foo + ", +} + +test! { + name: no_trailing_newline, + justfile: " + foo: + echo foo", + args: ("--dump"), + stdout: " + foo: + echo foo + ", +} diff --git a/tests/lib.rs b/tests/lib.rs index d4b62a4..c4eb592 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -14,6 +14,7 @@ mod error_messages; mod evaluate; mod examples; mod export; +mod fmt; mod init; mod interrupts; mod invocation_directory; diff --git a/tests/misc.rs b/tests/misc.rs index 7bbe6d0..e3cea42 100644 --- a/tests/misc.rs +++ b/tests/misc.rs @@ -265,7 +265,7 @@ recipe: args: ("--show", "recipe"), stdout: r#" recipe: - echo {{hello + "bar" + bar}} + echo {{ hello + "bar" + bar }} "#, } diff --git a/tests/test.rs b/tests/test.rs index d617c2c..4b70897 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -75,7 +75,7 @@ impl<'a> Test<'a> { let mut justfile_path = tmp.path().to_path_buf(); justfile_path.push("justfile"); - fs::write(justfile_path, justfile).unwrap(); + fs::write(&justfile_path, justfile).unwrap(); let mut dotenv_path = tmp.path().to_path_buf(); dotenv_path.push(".env");