diff --git a/Cargo.lock b/Cargo.lock index 0d6b68b..07888d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,6 +77,15 @@ dependencies = [ "vec_map", ] +[[package]] +name = "cradle" +version = "0.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2352f0ca05779da0791a0ea204cc7bfddf83ee6e6277c919d8c0a5801d27f0e4" +dependencies = [ + "rustversion", +] + [[package]] name = "ctor" version = "0.1.20" @@ -200,6 +209,7 @@ dependencies = [ "atty", "camino", "clap", + "cradle", "ctrlc", "derivative", "dotenv", @@ -211,12 +221,14 @@ dependencies = [ "libc", "log", "pretty_assertions", + "regex", "snafu", "strum", "strum_macros", "target", "tempfile", "temptree", + "typed-arena", "unicode-width", "which", "yaml-rust", @@ -426,6 +438,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "rustversion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" + [[package]] name = "snafu" version = "0.6.10" @@ -556,6 +574,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "typed-arena" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0685c84d5d54d1c26f7d3eb96cd41550adb97baed141a761cf335d3d33bcd0ae" + [[package]] name = "unicode-segmentation" version = "1.8.0" diff --git a/Cargo.toml b/Cargo.toml index 98404a2..8cb1b36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ snafu = "0.6.0" strum_macros = "0.21.1" target = "1.0.0" tempfile = "3.0.0" +typed-arena = "2.0.1" unicode-width = "0.1.0" [dependencies.ctrlc] @@ -44,8 +45,10 @@ version = "0.21.0" features = ["derive"] [dev-dependencies] +cradle = "0.0.13" executable-path = "1.0.0" pretty_assertions = "0.7.0" +regex = "1.5.4" temptree = "0.1.0" which = "4.0.0" yaml-rust = "0.4.5" diff --git a/justfile b/justfile index dbdab50..1359d2f 100755 --- a/justfile +++ b/justfile @@ -40,7 +40,7 @@ build: fmt: cargo +nightly fmt --all -watch +COMMAND='test': +watch +COMMAND='ltest': cargo watch --clear --exec "{{COMMAND}}" man: @@ -61,7 +61,7 @@ version := `sed -En 's/version[[:space:]]*=[[:space:]]*"([^"]+)"/\1/p' Cargo.tom changes: git log --pretty=format:%s >> CHANGELOG.md -check: clippy test forbid +check: clippy fmt test forbid git diff --no-ext-diff --quiet --exit-code grep '^\[{{ version }}\]' CHANGELOG.md cargo +nightly generate-lockfile -Z minimal-versions diff --git a/src/analyzer.rs b/src/analyzer.rs index 001412b..83eba48 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -1,6 +1,6 @@ use crate::common::*; -use CompilationErrorKind::*; +use CompileErrorKind::*; #[derive(Default)] pub(crate) struct Analyzer<'src> { @@ -11,11 +11,11 @@ pub(crate) struct Analyzer<'src> { } impl<'src> Analyzer<'src> { - pub(crate) fn analyze(ast: Ast<'src>) -> CompilationResult<'src, Justfile> { + pub(crate) fn analyze(ast: Ast<'src>) -> CompileResult<'src, Justfile> { Analyzer::default().justfile(ast) } - pub(crate) fn justfile(mut self, ast: Ast<'src>) -> CompilationResult<'src, Justfile<'src>> { + pub(crate) fn justfile(mut self, ast: Ast<'src>) -> CompileResult<'src, Justfile<'src>> { for item in ast.items { match item { Item::Alias(alias) => { @@ -88,7 +88,7 @@ impl<'src> Analyzer<'src> { }) } - fn analyze_recipe(&self, recipe: &UnresolvedRecipe<'src>) -> CompilationResult<'src, ()> { + fn analyze_recipe(&self, recipe: &UnresolvedRecipe<'src>) -> CompileResult<'src, ()> { if let Some(original) = self.recipes.get(recipe.name.lexeme()) { return Err(recipe.name.token().error(DuplicateRecipe { recipe: original.name(), @@ -140,7 +140,7 @@ impl<'src> Analyzer<'src> { Ok(()) } - fn analyze_assignment(&self, assignment: &Assignment<'src>) -> CompilationResult<'src, ()> { + fn analyze_assignment(&self, assignment: &Assignment<'src>) -> CompileResult<'src, ()> { if self.assignments.contains_key(assignment.name.lexeme()) { return Err(assignment.name.token().error(DuplicateVariable { variable: assignment.name.lexeme(), @@ -149,7 +149,7 @@ impl<'src> Analyzer<'src> { Ok(()) } - fn analyze_alias(&self, alias: &Alias<'src, Name<'src>>) -> CompilationResult<'src, ()> { + fn analyze_alias(&self, alias: &Alias<'src, Name<'src>>) -> CompileResult<'src, ()> { let name = alias.name.lexeme(); if let Some(original) = self.aliases.get(name) { @@ -162,7 +162,7 @@ impl<'src> Analyzer<'src> { Ok(()) } - fn analyze_set(&self, set: &Set<'src>) -> CompilationResult<'src, ()> { + fn analyze_set(&self, set: &Set<'src>) -> CompileResult<'src, ()> { if let Some(original) = self.sets.get(set.name.lexeme()) { return Err(set.name.error(DuplicateSet { setting: original.name.lexeme(), @@ -176,7 +176,7 @@ impl<'src> Analyzer<'src> { fn resolve_alias( recipes: &Table<'src, Rc>>, alias: Alias<'src, Name<'src>>, - ) -> CompilationResult<'src, Alias<'src>> { + ) -> CompileResult<'src, Alias<'src>> { let token = alias.name.token(); // Make sure the alias doesn't conflict with any recipe if let Some(recipe) = recipes.get(alias.name.lexeme()) { diff --git a/src/assignment_resolver.rs b/src/assignment_resolver.rs index 4e0b8b3..c802e39 100644 --- a/src/assignment_resolver.rs +++ b/src/assignment_resolver.rs @@ -1,6 +1,6 @@ use crate::common::*; -use CompilationErrorKind::*; +use CompileErrorKind::*; pub(crate) struct AssignmentResolver<'src: 'run, 'run> { assignments: &'run Table<'src, Assignment<'src>>, @@ -11,7 +11,7 @@ pub(crate) struct AssignmentResolver<'src: 'run, 'run> { impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> { pub(crate) fn resolve_assignments( assignments: &Table<'src, Assignment<'src>>, - ) -> CompilationResult<'src, ()> { + ) -> CompileResult<'src, ()> { let mut resolver = AssignmentResolver { stack: Vec::new(), evaluated: BTreeSet::new(), @@ -25,7 +25,7 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> { Ok(()) } - fn resolve_assignment(&mut self, name: &'src str) -> CompilationResult<'src, ()> { + fn resolve_assignment(&mut self, name: &'src str) -> CompileResult<'src, ()> { if self.evaluated.contains(name) { return Ok(()); } @@ -45,7 +45,7 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> { length: 0, kind: TokenKind::Unspecified, }; - return Err(CompilationError { + return Err(CompileError { kind: Internal { message }, token, }); @@ -56,7 +56,7 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> { Ok(()) } - fn resolve_expression(&mut self, expression: &Expression<'src>) -> CompilationResult<'src, ()> { + fn resolve_expression(&mut self, expression: &Expression<'src>) -> CompileResult<'src, ()> { match expression { Expression::Variable { name } => { let variable = name.lexeme(); diff --git a/src/ast.rs b/src/ast.rs index 632110a..cff7a63 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1,9 +1,8 @@ use crate::common::*; -/// The top-level type produced by the parser.Not all successful parses result +/// The top-level type produced by the parser. 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 an -/// `Ast`. +/// are performed by the `Analyzer`, which produces a `Justfile` from an `Ast`. #[derive(Debug, Clone)] pub(crate) struct Ast<'src> { /// Items in the justfile diff --git a/src/common.rs b/src/common.rs index 6609eb8..a37d91f 100644 --- a/src/common.rs +++ b/src/common.rs @@ -11,7 +11,7 @@ pub(crate) use std::{ mem, ops::{Index, Range, RangeInclusive}, path::{Path, PathBuf}, - process::{self, Command, Stdio}, + process::{self, Command, ExitStatus, Stdio}, rc::Rc, str::{self, Chars}, sync::{Mutex, MutexGuard}, @@ -27,6 +27,7 @@ pub(crate) use libc::EXIT_FAILURE; pub(crate) use log::{info, warn}; pub(crate) use snafu::{ResultExt, Snafu}; pub(crate) use strum::{Display, EnumString, IntoStaticStr}; +pub(crate) use typed_arena::Arena; pub(crate) use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; // modules @@ -37,24 +38,23 @@ pub(crate) use crate::{load_dotenv::load_dotenv, output::output, unindent::unind // traits pub(crate) use crate::{ - command_ext::CommandExt, error::Error, error_result_ext::ErrorResultExt, keyed::Keyed, - ordinal::Ordinal, platform_interface::PlatformInterface, range_ext::RangeExt, + command_ext::CommandExt, keyed::Keyed, ordinal::Ordinal, platform_interface::PlatformInterface, + range_ext::RangeExt, }; // structs and enums pub(crate) use crate::{ alias::Alias, analyzer::Analyzer, assignment::Assignment, assignment_resolver::AssignmentResolver, ast::Ast, binding::Binding, color::Color, - compilation_error::CompilationError, compilation_error_kind::CompilationErrorKind, - config::Config, config_error::ConfigError, count::Count, delimiter::Delimiter, - dependency::Dependency, enclosure::Enclosure, evaluator::Evaluator, expression::Expression, + compile_error::CompileError, compile_error_kind::CompileErrorKind, config::Config, + config_error::ConfigError, count::Count, delimiter::Delimiter, dependency::Dependency, + enclosure::Enclosure, error::Error, 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, 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, + justfile::Justfile, keyword::Keyword, lexer::Lexer, line::Line, list::List, loader::Loader, + 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, 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, @@ -64,9 +64,9 @@ pub(crate) use crate::{ }; // type aliases -pub(crate) type CompilationResult<'a, T> = Result>; +pub(crate) type CompileResult<'a, T> = Result>; pub(crate) type ConfigResult = Result; -pub(crate) type RunResult<'a, T> = Result>; +pub(crate) type RunResult<'a, T> = Result>; pub(crate) type SearchResult = Result; // modules used in tests diff --git a/src/compilation_error.rs b/src/compile_error.rs similarity index 71% rename from src/compilation_error.rs rename to src/compile_error.rs index 56d23b1..d3d9d89 100644 --- a/src/compilation_error.rs +++ b/src/compile_error.rs @@ -1,23 +1,24 @@ use crate::common::*; #[derive(Debug, PartialEq)] -pub(crate) struct CompilationError<'src> { +pub(crate) struct CompileError<'src> { pub(crate) token: Token<'src>, - pub(crate) kind: CompilationErrorKind<'src>, + pub(crate) kind: CompileErrorKind<'src>, } -impl Error for CompilationError<'_> {} +impl<'src> CompileError<'src> { + pub(crate) fn context(&self) -> Token<'src> { + self.token + } +} -impl Display for CompilationError<'_> { +impl Display for CompileError<'_> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { - use CompilationErrorKind::*; - let message = Color::fmt(f).message(); - - write!(f, "{}", message.prefix())?; + use CompileErrorKind::*; match &self.kind { AliasShadowsRecipe { alias, recipe_line } => { - writeln!( + write!( f, "Alias `{}` defined on line {} shadows recipe `{}` defined on line {}", alias, @@ -27,13 +28,13 @@ impl Display for CompilationError<'_> { )?; }, BacktickShebang => { - writeln!(f, "Backticks may not start with `#!`")?; + write!(f, "Backticks may not start with `#!`")?; }, CircularRecipeDependency { recipe, ref circle } => if circle.len() == 2 { - writeln!(f, "Recipe `{}` depends on itself", recipe)?; + write!(f, "Recipe `{}` depends on itself", recipe)?; } else { - writeln!( + write!( f, "Recipe `{}` has circular dependency `{}`", recipe, @@ -45,79 +46,15 @@ impl Display for CompilationError<'_> { ref circle, } => if circle.len() == 2 { - writeln!(f, "Variable `{}` is defined in terms of itself", variable)?; + write!(f, "Variable `{}` is defined in terms of itself", variable)?; } else { - writeln!( + write!( f, "Variable `{}` depends on its own value: `{}`", variable, circle.join(" -> ") )?; }, - - InvalidEscapeSequence { character } => { - let representation = match character { - '`' => r"\`".to_owned(), - '\\' => r"\".to_owned(), - '\'' => r"'".to_owned(), - '"' => r#"""#.to_owned(), - _ => character.escape_default().collect(), - }; - writeln!(f, "`\\{}` is not a valid escape sequence", representation)?; - }, - DeprecatedEquals => { - writeln!( - f, - "`=` in assignments, exports, and aliases has been phased out on favor of `:=`" - )?; - writeln!( - f, - "Please see this issue for more details: https://github.com/casey/just/issues/379" - )?; - }, - DuplicateParameter { recipe, parameter } => { - writeln!( - f, - "Recipe `{}` has duplicate parameter `{}`", - recipe, parameter - )?; - }, - DuplicateVariable { variable } => { - writeln!(f, "Variable `{}` has multiple definitions", variable)?; - }, - UnexpectedToken { - ref expected, - found, - } => { - writeln!(f, "Expected {}, but found {}", List::or(expected), found)?; - }, - DuplicateAlias { alias, first } => { - writeln!( - f, - "Alias `{}` first defined on line {} is redefined on line {}", - alias, - first.ordinal(), - self.token.line.ordinal(), - )?; - }, - DuplicateRecipe { recipe, first } => { - writeln!( - f, - "Recipe `{}` first defined on line {} is redefined on line {}", - recipe, - first.ordinal(), - self.token.line.ordinal() - )?; - }, - DuplicateSet { setting, first } => { - writeln!( - f, - "Setting `{}` first set on line {} is redefined on line {}", - setting, - first.ordinal(), - self.token.line.ordinal(), - )?; - }, DependencyArgumentCountMismatch { dependency, found, @@ -134,53 +71,75 @@ impl Display for CompilationError<'_> { if min == max { let expected = min; - writeln!(f, "{} {}", expected, Count("argument", *expected))?; + write!(f, "{} {}", expected, Count("argument", *expected))?; } else if found < min { - writeln!(f, "at least {} {}", min, Count("argument", *min))?; + write!(f, "at least {} {}", min, Count("argument", *min))?; } else { - writeln!(f, "at most {} {}", max, Count("argument", *max))?; + write!(f, "at most {} {}", max, Count("argument", *max))?; } }, - ExpectedKeyword { expected, found } => writeln!( + DeprecatedEquals => { + writeln!( + f, + "`=` in assignments, exports, and aliases has been phased out on favor of `:=`" + )?; + write!( + f, + "Please see this issue for more details: https://github.com/casey/just/issues/379" + )?; + }, + DuplicateAlias { alias, first } => { + write!( + f, + "Alias `{}` first defined on line {} is redefined on line {}", + alias, + first.ordinal(), + self.token.line.ordinal(), + )?; + }, + DuplicateParameter { recipe, parameter } => { + write!( + f, + "Recipe `{}` has duplicate parameter `{}`", + recipe, parameter + )?; + }, + DuplicateRecipe { recipe, first } => { + write!( + f, + "Recipe `{}` first defined on line {} is redefined on line {}", + recipe, + first.ordinal(), + self.token.line.ordinal() + )?; + }, + DuplicateSet { setting, first } => { + write!( + f, + "Setting `{}` first set on line {} is redefined on line {}", + setting, + first.ordinal(), + self.token.line.ordinal(), + )?; + }, + DuplicateVariable { variable } => { + write!(f, "Variable `{}` has multiple definitions", variable)?; + }, + ExpectedKeyword { expected, found } => write!( f, "Expected keyword {} but found identifier `{}`", List::or_ticked(expected), found )?, - ParameterShadowsVariable { parameter } => { - writeln!( - f, - "Parameter `{}` shadows variable of the same name", - parameter - )?; - }, - RequiredParameterFollowsDefaultParameter { parameter } => { - writeln!( - f, - "Non-default parameter `{}` follows default parameter", - parameter - )?; - }, - ParameterFollowsVariadicParameter { parameter } => { - writeln!(f, "Parameter `{}` follows variadic parameter", parameter)?; - }, - MixedLeadingWhitespace { whitespace } => { - writeln!( - f, - "Found a mix of tabs and spaces in leading whitespace: `{}`\nLeading whitespace may \ - consist of tabs or spaces, but not both", - ShowWhitespace(whitespace) - )?; - }, ExtraLeadingWhitespace => { - writeln!(f, "Recipe line has extra leading whitespace")?; + write!(f, "Recipe line has extra leading whitespace")?; }, FunctionArgumentCountMismatch { function, found, expected, } => { - writeln!( + write!( f, "Function `{}` called with {} {} but takes {}", function, @@ -190,7 +149,7 @@ impl Display for CompilationError<'_> { )?; }, InconsistentLeadingWhitespace { expected, found } => { - writeln!( + write!( f, "Recipe line has inconsistent leading whitespace. Recipe started with `{}` but found \ line with `{}`", @@ -198,40 +157,30 @@ impl Display for CompilationError<'_> { ShowWhitespace(found) )?; }, - UnknownAliasTarget { alias, target } => { - writeln!(f, "Alias `{}` has an unknown target `{}`", alias, target)?; - }, - UnknownDependency { recipe, unknown } => { - writeln!( + Internal { ref message } => { + write!( f, - "Recipe `{}` has unknown dependency `{}`", - recipe, unknown + "Internal error, this may indicate a bug in just: {}\n\ + consider filing an issue: https://github.com/casey/just/issues/new", + message )?; }, - UndefinedVariable { variable } => { - writeln!(f, "Variable `{}` not defined", variable)?; - }, - UnknownFunction { function } => { - writeln!(f, "Call to unknown function `{}`", function)?; - }, - UnknownSetting { setting } => { - writeln!(f, "Unknown setting `{}`", setting)?; - }, - UnexpectedCharacter { expected } => { - writeln!(f, "Expected character `{}`", expected)?; - }, - UnknownStartOfToken => { - writeln!(f, "Unknown start of token:")?; - }, - UnexpectedEndOfToken { expected } => { - writeln!(f, "Expected character `{}` but found end-of-file", expected)?; + InvalidEscapeSequence { character } => { + let representation = match character { + '`' => r"\`".to_owned(), + '\\' => r"\".to_owned(), + '\'' => r"'".to_owned(), + '"' => r#"""#.to_owned(), + _ => character.escape_default().collect(), + }; + write!(f, "`\\{}` is not a valid escape sequence", representation)?; }, MismatchedClosingDelimiter { open, open_line, close, } => { - writeln!( + write!( f, "Mismatched closing delimiter `{}`. (Did you mean to close the `{}` on line {}?)", close.close(), @@ -239,33 +188,82 @@ impl Display for CompilationError<'_> { open_line.ordinal(), )?; }, + MixedLeadingWhitespace { whitespace } => { + write!( + f, + "Found a mix of tabs and spaces in leading whitespace: `{}`\nLeading whitespace may \ + consist of tabs or spaces, but not both", + ShowWhitespace(whitespace) + )?; + }, + ParameterFollowsVariadicParameter { parameter } => { + write!(f, "Parameter `{}` follows variadic parameter", parameter)?; + }, + ParameterShadowsVariable { parameter } => { + write!( + f, + "Parameter `{}` shadows variable of the same name", + parameter + )?; + }, + RequiredParameterFollowsDefaultParameter { parameter } => { + write!( + f, + "Non-default parameter `{}` follows default parameter", + parameter + )?; + }, + UndefinedVariable { variable } => { + write!(f, "Variable `{}` not defined", variable)?; + }, + UnexpectedCharacter { expected } => { + write!(f, "Expected character `{}`", expected)?; + }, UnexpectedClosingDelimiter { close } => { - writeln!(f, "Unexpected closing delimiter `{}`", close.close())?; + write!(f, "Unexpected closing delimiter `{}`", close.close())?; + }, + UnexpectedEndOfToken { expected } => { + write!(f, "Expected character `{}` but found end-of-file", expected)?; + }, + UnexpectedToken { + ref expected, + found, + } => { + write!(f, "Expected {}, but found {}", List::or(expected), found)?; + }, + UnknownAliasTarget { alias, target } => { + write!(f, "Alias `{}` has an unknown target `{}`", alias, target)?; + }, + UnknownDependency { recipe, unknown } => { + write!( + f, + "Recipe `{}` has unknown dependency `{}`", + recipe, unknown + )?; + }, + UnknownFunction { function } => { + write!(f, "Call to unknown function `{}`", function)?; + }, + UnknownSetting { setting } => { + write!(f, "Unknown setting `{}`", setting)?; + }, + UnknownStartOfToken => { + write!(f, "Unknown start of token:")?; }, UnpairedCarriageReturn => { - writeln!(f, "Unpaired carriage return")?; - }, - UnterminatedInterpolation => { - writeln!(f, "Unterminated interpolation")?; - }, - UnterminatedString => { - writeln!(f, "Unterminated string")?; + write!(f, "Unpaired carriage return")?; }, UnterminatedBacktick => { - writeln!(f, "Unterminated backtick")?; + write!(f, "Unterminated backtick")?; }, - Internal { ref message } => { - writeln!( - f, - "Internal error, this may indicate a bug in just: {}\n\ - consider filing an issue: https://github.com/casey/just/issues/new", - message - )?; + UnterminatedInterpolation => { + write!(f, "Unterminated interpolation")?; + }, + UnterminatedString => { + write!(f, "Unterminated string")?; }, } - write!(f, "{}", message.suffix())?; - - self.token.write_context(f, Color::fmt(f).error()) + Ok(()) } } diff --git a/src/compilation_error_kind.rs b/src/compile_error_kind.rs similarity index 98% rename from src/compilation_error_kind.rs rename to src/compile_error_kind.rs index b15c858..5909480 100644 --- a/src/compilation_error_kind.rs +++ b/src/compile_error_kind.rs @@ -1,7 +1,7 @@ use crate::common::*; #[derive(Debug, PartialEq)] -pub(crate) enum CompilationErrorKind<'src> { +pub(crate) enum CompileErrorKind<'src> { AliasShadowsRecipe { alias: &'src str, recipe_line: usize, @@ -34,13 +34,13 @@ pub(crate) enum CompilationErrorKind<'src> { recipe: &'src str, first: usize, }, - DuplicateVariable { - variable: &'src str, - }, DuplicateSet { setting: &'src str, first: usize, }, + DuplicateVariable { + variable: &'src str, + }, ExpectedKeyword { expected: Vec, found: &'src str, @@ -61,6 +61,11 @@ pub(crate) enum CompilationErrorKind<'src> { InvalidEscapeSequence { character: char, }, + MismatchedClosingDelimiter { + close: Delimiter, + open: Delimiter, + open_line: usize, + }, MixedLeadingWhitespace { whitespace: &'src str, }, @@ -76,6 +81,15 @@ pub(crate) enum CompilationErrorKind<'src> { UndefinedVariable { variable: &'src str, }, + UnexpectedCharacter { + expected: char, + }, + UnexpectedClosingDelimiter { + close: Delimiter, + }, + UnexpectedEndOfToken { + expected: char, + }, UnexpectedToken { expected: Vec, found: TokenKind, @@ -91,26 +105,12 @@ pub(crate) enum CompilationErrorKind<'src> { UnknownFunction { function: &'src str, }, - UnknownStartOfToken, - UnexpectedCharacter { - expected: char, - }, - UnexpectedEndOfToken { - expected: char, - }, UnknownSetting { setting: &'src str, }, + UnknownStartOfToken, UnpairedCarriageReturn, - UnexpectedClosingDelimiter { - close: Delimiter, - }, - MismatchedClosingDelimiter { - close: Delimiter, - open: Delimiter, - open_line: usize, - }, + UnterminatedBacktick, UnterminatedInterpolation, UnterminatedString, - UnterminatedBacktick, } diff --git a/src/compiler.rs b/src/compiler.rs index acba9ae..f19536e 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -3,7 +3,7 @@ use crate::common::*; pub(crate) struct Compiler; impl Compiler { - pub(crate) fn compile(src: &str) -> CompilationResult { + pub(crate) fn compile(src: &str) -> CompileResult { let tokens = Lexer::lex(src)?; let ast = Parser::parse(&tokens)?; diff --git a/src/config.rs b/src/config.rs index e68d981..3e1d028 100644 --- a/src/config.rs +++ b/src/config.rs @@ -532,7 +532,7 @@ impl Config { }) } - pub(crate) fn run_subcommand(self) -> Result<(), i32> { + pub(crate) fn run_subcommand<'src>(self, loader: &'src Loader) -> Result<(), Error<'src>> { use Subcommand::*; if self.subcommand == Init { @@ -540,34 +540,24 @@ impl Config { } if let Completions { shell } = self.subcommand { - return Subcommand::completions(self.verbosity, &shell); + return Subcommand::completions(&shell); } - let search = - Search::find(&self.search_config, &self.invocation_directory).eprint(self.color)?; + let search = Search::find(&self.search_config, &self.invocation_directory)?; if self.subcommand == Edit { - return self.edit(&search); + return Self::edit(&search); } - let src = fs::read_to_string(&search.justfile) - .map_err(|io_error| LoadError { - io_error, - path: &search.justfile, - }) - .eprint(self.color)?; + let src = loader.load(&search.justfile)?; - 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)?; + let tokens = Lexer::lex(&src)?; + let ast = Parser::parse(&tokens)?; + let justfile = Analyzer::analyze(ast.clone())?; if self.verbosity.loud() { for warning in &justfile.warnings { - if self.color.stderr().active() { - eprintln!("{:#}", warning); - } else { - eprintln!("{}", warning); - } + warning.write(&mut io::stderr(), self.color.stderr()).ok(); } } @@ -575,7 +565,7 @@ impl Config { Choose { overrides, chooser } => self.choose(justfile, &search, overrides, chooser.as_deref())?, Command { overrides, .. } => self.run(justfile, &search, overrides, &[])?, - Dump => Self::dump(ast)?, + Dump => Self::dump(ast), Evaluate { overrides, .. } => self.run(justfile, &search, overrides, &[])?, Format => self.format(ast, &search)?, List => self.list(justfile), @@ -583,7 +573,7 @@ impl Config { arguments, overrides, } => self.run(justfile, &search, overrides, arguments)?, - Show { ref name } => self.show(&name, justfile)?, + Show { ref name } => Self::show(&name, justfile)?, Summary => self.summary(justfile), Variables => Self::variables(justfile), Completions { .. } | Edit | Init => unreachable!(), @@ -592,13 +582,13 @@ impl Config { Ok(()) } - fn choose( + fn choose<'src>( &self, - justfile: Justfile, + justfile: Justfile<'src>, search: &Search, overrides: &BTreeMap, chooser: Option<&str>, - ) -> Result<(), i32> { + ) -> Result<(), Error<'src>> { let recipes = justfile .public_recipes(self.unsorted) .iter() @@ -607,10 +597,7 @@ impl Config { .collect::>>(); if recipes.is_empty() { - if self.verbosity.loud() { - eprintln!("Justfile contains no choosable recipes."); - } - return Err(EXIT_FAILURE); + return Err(Error::NoChoosableRecipes); } let chooser = chooser @@ -629,61 +616,39 @@ impl Config { let mut child = match result { Ok(child) => child, - Err(error) => { - if self.verbosity.loud() { - eprintln!( - "Chooser `{} {} {}` invocation failed: {}", - justfile.settings.shell_binary(self), - justfile.settings.shell_arguments(self).join(" "), - chooser.to_string_lossy(), - error - ); - } - return Err(EXIT_FAILURE); + Err(io_error) => { + return Err(Error::ChooserInvoke { + shell_binary: justfile.settings.shell_binary(self).to_owned(), + shell_arguments: justfile.settings.shell_arguments(self).join(" "), + chooser, + io_error, + }); }, }; for recipe in recipes { - if let Err(error) = child + if let Err(io_error) = child .stdin .as_mut() .expect("Child was created with piped stdio") .write_all(format!("{}\n", recipe.name).as_bytes()) { - if self.verbosity.loud() { - eprintln!( - "Failed to write to chooser `{}`: {}", - chooser.to_string_lossy(), - error - ); - } - return Err(EXIT_FAILURE); + return Err(Error::ChooserWrite { io_error, chooser }); } } let output = match child.wait_with_output() { Ok(output) => output, - Err(error) => { - if self.verbosity.loud() { - eprintln!( - "Failed to read output from chooser `{}`: {}", - chooser.to_string_lossy(), - error - ); - } - return Err(EXIT_FAILURE); + Err(io_error) => { + return Err(Error::ChooserRead { io_error, chooser }); }, }; if !output.status.success() { - if self.verbosity.loud() { - eprintln!( - "Chooser `{}` returned error: {}", - chooser.to_string_lossy(), - output.status - ); - } - return Err(output.status.code().unwrap_or(EXIT_FAILURE)); + return Err(Error::ChooserStatus { + status: output.status, + chooser, + }); } let stdout = String::from_utf8_lossy(&output.stdout); @@ -697,12 +662,11 @@ impl Config { self.run(justfile, search, overrides, &recipes) } - fn dump(ast: Ast) -> Result<(), i32> { + fn dump(ast: Ast) { print!("{}", ast); - Ok(()) } - pub(crate) fn edit(&self, search: &Search) -> Result<(), i32> { + pub(crate) fn edit(search: &Search) -> Result<(), Error<'static>> { let editor = env::var_os("VISUAL") .or_else(|| env::var_os("EDITOR")) .unwrap_or_else(|| "vim".into()); @@ -712,47 +676,38 @@ impl Config { .arg(&search.justfile) .status(); - match error { - Ok(status) => - if status.success() { - Ok(()) - } else { - if self.verbosity.loud() { - eprintln!("Editor `{}` failed: {}", editor.to_string_lossy(), status); - } - Err(status.code().unwrap_or(EXIT_FAILURE)) - }, - Err(error) => { - if self.verbosity.loud() { - eprintln!( - "Editor `{}` invocation failed: {}", - editor.to_string_lossy(), - error - ); - } - Err(EXIT_FAILURE) - }, + let status = match error { + Err(io_error) => return Err(Error::EditorInvoke { editor, io_error }), + Ok(status) => status, + }; + + if !status.success() { + return Err(Error::EditorStatus { editor, status }); + } + + Ok(()) + } + + fn require_unstable(&self, message: &str) -> Result<(), Error<'static>> { + if self.unstable { + Ok(()) + } else { + Err(Error::Unstable { + message: message.to_owned(), + }) } } - fn format(&self, ast: Ast, search: &Search) -> Result<(), i32> { - if !self.unstable { - eprintln!( - "The `--fmt` command is currently unstable. Pass the `--unstable` flag to enable it." - ); - return Err(EXIT_FAILURE); - } + fn format(&self, ast: Ast, search: &Search) -> Result<(), Error<'static>> { + self.require_unstable("The `--fmt` command is currently unstable.")?; - if let Err(error) = File::create(&search.justfile).and_then(|mut file| write!(file, "{}", ast)) + if let Err(io_error) = + File::create(&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) + Err(Error::WriteJustfile { + justfile: search.justfile.clone(), + io_error, + }) } else { if self.verbosity.loud() { eprintln!("Wrote justfile to `{}`", search.justfile.display()); @@ -761,24 +716,18 @@ impl Config { } } - pub(crate) fn init(&self) -> Result<(), i32> { - let search = - Search::init(&self.search_config, &self.invocation_directory).eprint(self.color)?; + pub(crate) fn init(&self) -> Result<(), Error<'static>> { + let search = Search::init(&self.search_config, &self.invocation_directory)?; - if search.justfile.exists() { - if self.verbosity.loud() { - eprintln!("Justfile `{}` already exists", search.justfile.display()); - } - Err(EXIT_FAILURE) - } else if let Err(err) = fs::write(&search.justfile, INIT_JUSTFILE) { - if self.verbosity.loud() { - eprintln!( - "Failed to write justfile to `{}`: {}", - search.justfile.display(), - err - ); - } - Err(EXIT_FAILURE) + if search.justfile.is_file() { + Err(Error::InitExists { + justfile: search.justfile, + }) + } else if let Err(io_error) = fs::write(&search.justfile, INIT_JUSTFILE) { + Err(Error::WriteJustfile { + justfile: search.justfile, + io_error, + }) } else { if self.verbosity.loud() { eprintln!("Wrote justfile to `{}`", search.justfile.display()); @@ -871,27 +820,21 @@ impl Config { } } - fn run( + fn run<'src>( &self, - justfile: Justfile, + justfile: Justfile<'src>, search: &Search, overrides: &BTreeMap, arguments: &[String], - ) -> Result<(), i32> { + ) -> Result<(), Error<'src>> { if let Err(error) = InterruptHandler::install(self.verbosity) { warn!("Failed to set CTRL-C handler: {}", error); } - let result = justfile.run(&self, search, overrides, arguments); - - if !self.verbosity.quiet() { - result.eprint(self.color) - } else { - result.map_err(|err| err.code()) - } + justfile.run(&self, search, overrides, arguments) } - fn show(&self, name: &str, justfile: Justfile) -> Result<(), i32> { + fn show<'src>(name: &str, justfile: Justfile<'src>) -> Result<(), Error<'src>> { if let Some(alias) = justfile.get_alias(name) { let recipe = justfile.get_recipe(alias.target.name.lexeme()).unwrap(); println!("{}", alias); @@ -901,13 +844,10 @@ impl Config { println!("{}", recipe); Ok(()) } else { - if self.verbosity.loud() { - eprintln!("Justfile does not contain recipe `{}`.", name); - if let Some(suggestion) = justfile.suggest_recipe(name) { - eprintln!("{}", suggestion); - } - } - Err(EXIT_FAILURE) + Err(Error::UnknownRecipes { + recipes: vec![name.to_owned()], + suggestion: justfile.suggest_recipe(name), + }) } } diff --git a/src/config_error.rs b/src/config_error.rs index 76ab066..82cc1bb 100644 --- a/src/config_error.rs +++ b/src/config_error.rs @@ -3,14 +3,14 @@ use crate::common::*; #[derive(Debug, Snafu)] #[snafu(visibility(pub(crate)))] pub(crate) enum ConfigError { + #[snafu(display("Failed to get current directory: {}", source))] + CurrentDir { source: io::Error }, #[snafu(display( "Internal config error, this may indicate a bug in just: {} \ consider filing an issue: https://github.com/casey/just/issues/new", message ))] Internal { message: String }, - #[snafu(display("Failed to get current directory: {}", source))] - CurrentDir { source: io::Error }, #[snafu(display( "Path-prefixed recipes may not be used with `--working-directory` or `--justfile`." ))] @@ -25,6 +25,15 @@ pub(crate) enum ConfigError { subcommand: &'static str, arguments: Vec, }, + #[snafu(display( + "`--{}` used with unexpected overrides: {}", + subcommand.to_lowercase(), + List::and_ticked(overrides.iter().map(|(key, value)| format!("{}={}", key, value))), + ))] + SubcommandOverrides { + subcommand: &'static str, + overrides: BTreeMap, + }, #[snafu(display( "`--{}` used with unexpected overrides: {}; and arguments: {}", subcommand.to_lowercase(), @@ -36,15 +45,6 @@ pub(crate) enum ConfigError { overrides: BTreeMap, arguments: Vec, }, - #[snafu(display( - "`--{}` used with unexpected overrides: {}", - subcommand.to_lowercase(), - List::and_ticked(overrides.iter().map(|(key, value)| format!("{}={}", key, value))), - ))] - SubcommandOverrides { - subcommand: &'static str, - overrides: BTreeMap, - }, } impl ConfigError { @@ -54,5 +54,3 @@ impl ConfigError { } } } - -impl Error for ConfigError {} diff --git a/src/error.rs b/src/error.rs index 98b7d51..b9437a0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,7 +1,624 @@ use crate::common::*; -pub(crate) trait Error: Display { - fn code(&self) -> i32 { - EXIT_FAILURE +#[derive(Debug)] +pub(crate) enum Error<'src> { + ArgumentCountMismatch { + recipe: &'src str, + parameters: Vec>, + found: usize, + min: usize, + max: usize, + }, + Backtick { + token: Token<'src>, + output_error: OutputError, + }, + ChooserInvoke { + shell_binary: String, + shell_arguments: String, + chooser: OsString, + io_error: io::Error, + }, + ChooserRead { + chooser: OsString, + io_error: io::Error, + }, + ChooserStatus { + chooser: OsString, + status: ExitStatus, + }, + ChooserWrite { + chooser: OsString, + io_error: io::Error, + }, + Code { + recipe: &'src str, + line_number: Option, + code: i32, + }, + CommandInvoke { + binary: OsString, + arguments: Vec, + io_error: io::Error, + }, + CommandStatus { + binary: OsString, + arguments: Vec, + status: ExitStatus, + }, + Compile { + compile_error: CompileError<'src>, + }, + Config { + config_error: ConfigError, + }, + Cygpath { + recipe: &'src str, + output_error: OutputError, + }, + DefaultRecipeRequiresArguments { + recipe: &'src str, + min_arguments: usize, + }, + Dotenv { + dotenv_error: dotenv::Error, + }, + EditorInvoke { + editor: OsString, + io_error: io::Error, + }, + EditorStatus { + editor: OsString, + status: ExitStatus, + }, + EvalUnknownVariable { + variable: String, + suggestion: Option>, + }, + FunctionCall { + function: Name<'src>, + message: String, + }, + InitExists { + justfile: PathBuf, + }, + Internal { + message: String, + }, + Io { + recipe: &'src str, + io_error: io::Error, + }, + Load { + path: PathBuf, + io_error: io::Error, + }, + NoChoosableRecipes, + NoRecipes, + Search { + search_error: SearchError, + }, + Shebang { + recipe: &'src str, + command: String, + argument: Option, + io_error: io::Error, + }, + Signal { + recipe: &'src str, + line_number: Option, + signal: i32, + }, + TmpdirIo { + recipe: &'src str, + io_error: io::Error, + }, + Unknown { + recipe: &'src str, + line_number: Option, + }, + UnknownOverrides { + overrides: Vec, + }, + UnknownRecipes { + recipes: Vec, + suggestion: Option>, + }, + Unstable { + message: String, + }, + WriteJustfile { + justfile: PathBuf, + io_error: io::Error, + }, +} + +impl<'src> Error<'src> { + pub(crate) fn code(&self) -> Option { + match self { + Self::Code { code, .. } + | Self::Backtick { + output_error: OutputError::Code(code), + .. + } => Some(*code), + Self::ChooserStatus { status, .. } | Self::EditorStatus { status, .. } => status.code(), + _ => None, + } + } + + fn context(&self) -> Option> { + match self { + Self::Backtick { token, .. } => Some(*token), + Self::Compile { compile_error } => Some(compile_error.context()), + Self::FunctionCall { function, .. } => Some(function.token()), + _ => None, + } + } + + pub(crate) fn internal(message: impl Into) -> Self { + Self::Internal { + message: message.into(), + } + } + + pub(crate) fn write(&self, w: &mut dyn Write, color: Color) -> io::Result<()> { + let color = color.stderr(); + + if color.active() { + writeln!( + w, + "{}: {}{:#}{}", + color.error().paint("error"), + color.message().prefix(), + self, + color.message().suffix() + )?; + } else { + writeln!(w, "error: {}", self)?; + } + + if let Some(token) = self.context() { + token.write_context(w, color.error())?; + writeln!(w)?; + } + + Ok(()) + } +} + +impl<'src> From> for Error<'src> { + fn from(compile_error: CompileError<'src>) -> Self { + Self::Compile { compile_error } + } +} + +impl<'src> From for Error<'src> { + fn from(config_error: ConfigError) -> Self { + Self::Config { config_error } + } +} + +impl<'src> From for Error<'src> { + fn from(dotenv_error: dotenv::Error) -> Error<'src> { + Self::Dotenv { dotenv_error } + } +} + +impl<'src> From for Error<'src> { + fn from(search_error: SearchError) -> Self { + Self::Search { search_error } + } +} + +impl<'src> Display for Error<'src> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + use Error::*; + + match self { + ArgumentCountMismatch { + recipe, + parameters, + found, + min, + max, + } => { + if min == max { + let expected = min; + write!( + f, + "Recipe `{}` got {} {} but {}takes {}", + recipe, + found, + Count("argument", *found), + if expected < found { "only " } else { "" }, + expected + )?; + } else if found < min { + write!( + f, + "Recipe `{}` got {} {} but takes at least {}", + recipe, + found, + Count("argument", *found), + min + )?; + } else if found > max { + write!( + f, + "Recipe `{}` got {} {} but takes at most {}", + recipe, + found, + Count("argument", *found), + max + )?; + } + write!(f, "\nusage:\n just {}", recipe)?; + for param in parameters { + write!(f, " {}", param)?; + } + }, + Backtick { output_error, .. } => match output_error { + OutputError::Code(code) => { + write!(f, "Backtick failed with exit code {}", code)?; + }, + OutputError::Signal(signal) => { + write!(f, "Backtick was terminated by signal {}", signal)?; + }, + OutputError::Unknown => { + write!(f, "Backtick failed for an unknown reason")?; + }, + OutputError::Io(io_error) => { + match io_error.kind() { + io::ErrorKind::NotFound => write!( + f, + "Backtick could not be run because just could not find `sh`:\n{}", + io_error + ), + io::ErrorKind::PermissionDenied => write!( + f, + "Backtick could not be run because just could not run `sh`:\n{}", + io_error + ), + _ => write!( + f, + "Backtick could not be run because of an IO error while launching `sh`:\n{}", + io_error + ), + }?; + }, + OutputError::Utf8(utf8_error) => { + write!( + f, + "Backtick succeeded but stdout was not utf8: {}", + utf8_error + )?; + }, + }, + ChooserInvoke { + shell_binary, + shell_arguments, + chooser, + io_error, + } => { + write!( + f, + "Chooser `{} {} {}` invocation failed: {}", + shell_binary, + shell_arguments, + chooser.to_string_lossy(), + io_error, + )?; + }, + ChooserRead { chooser, io_error } => { + write!( + f, + "Failed to read output from chooser `{}`: {}", + chooser.to_string_lossy(), + io_error + )?; + }, + ChooserStatus { chooser, status } => { + write!( + f, + "Chooser `{}` failed: {}", + chooser.to_string_lossy(), + status + )?; + }, + ChooserWrite { chooser, io_error } => { + write!( + f, + "Failed to write to chooser `{}`: {}", + chooser.to_string_lossy(), + io_error + )?; + }, + Code { + recipe, + line_number, + code, + } => + if let Some(n) = line_number { + write!( + f, + "Recipe `{}` failed on line {} with exit code {}", + recipe, n, code + )?; + } else { + write!(f, "Recipe `{}` failed with exit code {}", recipe, code)?; + }, + CommandInvoke { + binary, + arguments, + io_error, + } => { + write!( + f, + "Failed to invoke {}: {}", + iter::once(binary) + .chain(arguments) + .map(|value| Enclosure::tick(value.to_string_lossy()).to_string()) + .collect::>() + .join(" "), + io_error, + )?; + }, + CommandStatus { + binary, + arguments, + status, + } => { + write!( + f, + "Command {} failed: {}", + iter::once(binary) + .chain(arguments) + .map(|value| Enclosure::tick(value.to_string_lossy()).to_string()) + .collect::>() + .join(" "), + status, + )?; + }, + Compile { compile_error } => Display::fmt(compile_error, f)?, + Config { config_error } => Display::fmt(config_error, f)?, + Cygpath { + recipe, + output_error, + } => match output_error { + OutputError::Code(code) => { + write!( + f, + "Cygpath failed with exit code {} while translating recipe `{}` shebang interpreter \ + path", + code, recipe + )?; + }, + OutputError::Signal(signal) => { + write!( + f, + "Cygpath terminated by signal {} while translating recipe `{}` shebang interpreter \ + path", + signal, recipe + )?; + }, + OutputError::Unknown => { + write!( + f, + "Cygpath experienced an unknown failure while translating recipe `{}` shebang \ + interpreter path", + recipe + )?; + }, + OutputError::Io(io_error) => { + match io_error.kind() { + io::ErrorKind::NotFound => write!( + f, + "Could not find `cygpath` executable to translate recipe `{}` shebang interpreter \ + path:\n{}", + recipe, io_error + ), + io::ErrorKind::PermissionDenied => write!( + f, + "Could not run `cygpath` executable to translate recipe `{}` shebang interpreter \ + path:\n{}", + recipe, io_error + ), + _ => write!(f, "Could not run `cygpath` executable:\n{}", io_error), + }?; + }, + OutputError::Utf8(utf8_error) => { + write!( + f, + "Cygpath successfully translated recipe `{}` shebang interpreter path, but output was \ + not utf8: {}", + recipe, utf8_error + )?; + }, + }, + DefaultRecipeRequiresArguments { + recipe, + min_arguments, + } => { + write!( + f, + "Recipe `{}` cannot be used as default recipe since it requires at least {} {}.", + recipe, + min_arguments, + Count("argument", *min_arguments), + )?; + }, + Dotenv { dotenv_error } => { + write!(f, "Failed to load .env: {}", dotenv_error)?; + }, + EditorInvoke { editor, io_error } => { + write!( + f, + "Editor `{}` invocation failed: {}", + editor.to_string_lossy(), + io_error + )?; + }, + EditorStatus { editor, status } => { + write!( + f, + "Editor `{}` failed: {}", + editor.to_string_lossy(), + status + )?; + }, + EvalUnknownVariable { + variable, + suggestion, + } => { + write!(f, "Justfile does not contain variable `{}`.", variable,)?; + if let Some(suggestion) = *suggestion { + write!(f, "\n{}", suggestion)?; + } + }, + FunctionCall { function, message } => { + write!( + f, + "Call to function `{}` failed: {}", + function.lexeme(), + message + )?; + }, + InitExists { justfile } => { + write!(f, "Justfile `{}` already exists", justfile.display())?; + }, + Internal { message } => { + write!( + f, + "Internal runtime error, this may indicate a bug in just: {} \ + consider filing an issue: https://github.com/casey/just/issues/new", + message + )?; + }, + Io { recipe, io_error } => { + match io_error.kind() { + io::ErrorKind::NotFound => write!( + f, + "Recipe `{}` could not be run because just could not find `sh`: {}", + recipe, io_error + ), + io::ErrorKind::PermissionDenied => write!( + f, + "Recipe `{}` could not be run because just could not run `sh`: {}", + recipe, io_error + ), + _ => write!( + f, + "Recipe `{}` could not be run because of an IO error while launching `sh`: {}", + recipe, io_error + ), + }?; + }, + Load { io_error, path } => { + write!( + f, + "Failed to read justfile at `{}`: {}", + path.display(), + io_error + )?; + }, + NoChoosableRecipes => { + write!(f, "Justfile contains no choosable recipes.")?; + }, + NoRecipes => { + write!(f, "Justfile contains no recipes.")?; + }, + Search { search_error } => Display::fmt(search_error, f)?, + Shebang { + recipe, + command, + argument, + io_error, + } => + if let Some(argument) = argument { + write!( + f, + "Recipe `{}` with shebang `#!{} {}` execution error: {}", + recipe, command, argument, io_error + )?; + } else { + write!( + f, + "Recipe `{}` with shebang `#!{}` execution error: {}", + recipe, command, io_error + )?; + }, + Signal { + recipe, + line_number, + signal, + } => + if let Some(n) = line_number { + write!( + f, + "Recipe `{}` was terminated on line {} by signal {}", + recipe, n, signal + )?; + } else { + write!(f, "Recipe `{}` was terminated by signal {}", recipe, signal)?; + }, + TmpdirIo { recipe, io_error } => write!( + f, + "Recipe `{}` could not be run because of an IO error while trying to create a temporary \ + directory or write a file to that directory`:{}", + recipe, io_error + )?, + Unknown { + recipe, + line_number, + } => + if let Some(n) = line_number { + write!( + f, + "Recipe `{}` failed on line {} for an unknown reason", + recipe, n + )?; + } else { + write!(f, "Recipe `{}` failed for an unknown reason", recipe)?; + }, + UnknownOverrides { overrides } => { + write!( + f, + "{} {} overridden on the command line but not present in justfile", + Count("Variable", overrides.len()), + List::and_ticked(overrides), + )?; + }, + UnknownRecipes { + recipes, + suggestion, + } => { + write!( + f, + "Justfile does not contain {} {}.", + Count("recipe", recipes.len()), + List::or_ticked(recipes), + )?; + if let Some(suggestion) = *suggestion { + write!(f, "\n{}", suggestion)?; + } + }, + Unstable { message } => { + write!( + f, + "{} Invoke `just` with the `--unstable` flag to enable unstable features.", + message + )?; + }, + WriteJustfile { justfile, io_error } => { + write!( + f, + "Failed to write justfile to `{}`: {}", + justfile.display(), + io_error + )?; + }, + } + + Ok(()) } } diff --git a/src/error_result_ext.rs b/src/error_result_ext.rs deleted file mode 100644 index 5ed6237..0000000 --- a/src/error_result_ext.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::common::*; - -pub(crate) trait ErrorResultExt { - fn eprint(self, color: Color) -> Result; -} - -impl ErrorResultExt for Result { - fn eprint(self, color: Color) -> Result { - match self { - Ok(ok) => Ok(ok), - Err(error) => { - if color.stderr().active() { - eprintln!("{}: {:#}", color.stderr().error().paint("error"), error); - } else { - eprintln!("error: {}", error); - } - - Err(error.code()) - }, - } - } -} diff --git a/src/evaluator.rs b/src/evaluator.rs index e19a1cc..08fcfb5 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -60,7 +60,7 @@ impl<'src, 'run> Evaluator<'src, 'run> { { Ok(self.evaluate_assignment(assignment)?.to_owned()) } else { - Err(RuntimeError::Internal { + Err(Error::Internal { message: format!("attempted to evaluate undefined variable `{}`", variable), }) } @@ -76,7 +76,7 @@ impl<'src, 'run> Evaluator<'src, 'run> { match thunk { Nullary { name, function, .. } => - function(&context).map_err(|message| RuntimeError::FunctionCall { + function(&context).map_err(|message| Error::FunctionCall { function: *name, message, }), @@ -86,7 +86,7 @@ impl<'src, 'run> Evaluator<'src, 'run> { arg, .. } => function(&context, &self.evaluate_expression(arg)?).map_err(|message| { - RuntimeError::FunctionCall { + Error::FunctionCall { function: *name, message, } @@ -101,7 +101,7 @@ impl<'src, 'run> Evaluator<'src, 'run> { &self.evaluate_expression(a)?, &self.evaluate_expression(b)?, ) - .map_err(|message| RuntimeError::FunctionCall { + .map_err(|message| Error::FunctionCall { function: *name, message, }), @@ -116,7 +116,7 @@ impl<'src, 'run> Evaluator<'src, 'run> { &self.evaluate_expression(b)?, &self.evaluate_expression(c)?, ) - .map_err(|message| RuntimeError::FunctionCall { + .map_err(|message| Error::FunctionCall { function: *name, message, }), @@ -169,7 +169,7 @@ impl<'src, 'run> Evaluator<'src, 'run> { }); InterruptHandler::guard(|| { - output(cmd).map_err(|output_error| RuntimeError::Backtick { + output(cmd).map_err(|output_error| Error::Backtick { token: *token, output_error, }) @@ -233,7 +233,7 @@ impl<'src, 'run> Evaluator<'src, 'run> { } else if parameter.kind == ParameterKind::Star { String::new() } else { - return Err(RuntimeError::Internal { + return Err(Error::Internal { message: "missing parameter without default".to_owned(), }); } @@ -285,7 +285,7 @@ mod tests { echo {{`f() { return 100; }; f`}} ", args: ["a"], - error: RuntimeError::Backtick { + error: Error::Backtick { token, output_error: OutputError::Code(code), }, @@ -305,7 +305,7 @@ mod tests { echo {{b}} "#, args: ["--quiet", "recipe"], - error: RuntimeError::Backtick { + error: Error::Backtick { token, output_error: OutputError::Code(_), }, diff --git a/src/fuzzing.rs b/src/fuzzing.rs index b4da7d2..54bba26 100644 --- a/src/fuzzing.rs +++ b/src/fuzzing.rs @@ -2,7 +2,7 @@ use crate::common::*; pub(crate) fn compile(text: &str) { if let Err(error) = Parser::parse(text) { - if let CompilationErrorKind::Internal { .. } = error.kind { + if let CompileErrorKind::Internal { .. } = error.kind { panic!("{}", error) } } diff --git a/src/interrupt_handler.rs b/src/interrupt_handler.rs index a0d161e..a3f2dc7 100644 --- a/src/interrupt_handler.rs +++ b/src/interrupt_handler.rs @@ -21,7 +21,7 @@ impl InterruptHandler { match INSTANCE.lock() { Ok(guard) => guard, Err(poison_error) => { - eprintln!("{}", RuntimeError::Internal { + eprintln!("{}", Error::Internal { message: format!("interrupt handler mutex poisoned: {}", poison_error), }); std::process::exit(EXIT_FAILURE); @@ -58,7 +58,7 @@ impl InterruptHandler { pub(crate) fn unblock(&mut self) { if self.blocks == 0 { if self.verbosity.loud() { - eprintln!("{}", RuntimeError::Internal { + eprintln!("{}", Error::Internal { message: "attempted to unblock interrupt handler, but handler was not blocked".to_owned(), }); } diff --git a/src/justfile.rs b/src/justfile.rs index 709a29e..ee6cd7b 100644 --- a/src/justfile.rs +++ b/src/justfile.rs @@ -10,7 +10,7 @@ pub(crate) struct Justfile<'src> { } impl<'src> Justfile<'src> { - pub(crate) fn first(&self) -> Option<&Recipe> { + pub(crate) fn first(&self) -> Option<&Recipe<'src>> { let mut first: Option<&Recipe> = None; for recipe in self.recipes.values() { if let Some(first_recipe) = first { @@ -28,7 +28,7 @@ impl<'src> Justfile<'src> { self.recipes.len() } - pub(crate) fn suggest_recipe(&self, input: &str) -> Option { + pub(crate) fn suggest_recipe(&self, input: &str) -> Option> { let mut suggestions = self .recipes .keys() @@ -54,7 +54,7 @@ impl<'src> Justfile<'src> { .next() } - pub(crate) fn suggest_variable(&self, input: &str) -> Option { + pub(crate) fn suggest_variable(&self, input: &str) -> Option> { let mut suggestions = self .assignments .keys() @@ -74,21 +74,21 @@ impl<'src> Justfile<'src> { .next() } - pub(crate) fn run<'run>( - &'run self, - config: &'run Config, - search: &'run Search, - overrides: &'run BTreeMap, - arguments: &'run [String], - ) -> RunResult<'run, ()> { + pub(crate) fn run( + &self, + config: &Config, + search: &Search, + overrides: &BTreeMap, + arguments: &[String], + ) -> RunResult<'src, ()> { let unknown_overrides = overrides .keys() .filter(|name| !self.assignments.contains_key(name.as_str())) - .map(String::as_str) - .collect::>(); + .cloned() + .collect::>(); if !unknown_overrides.is_empty() { - return Err(RuntimeError::UnknownOverrides { + return Err(Error::UnknownOverrides { overrides: unknown_overrides, }); } @@ -107,12 +107,12 @@ impl<'src> Justfile<'src> { if let Some(assignment) = self.assignments.get(name) { scope.bind(assignment.export, assignment.name, value.clone()); } else { - unknown_overrides.push(name.as_ref()); + unknown_overrides.push(name.clone()); } } if !unknown_overrides.is_empty() { - return Err(RuntimeError::UnknownOverrides { + return Err(Error::UnknownOverrides { overrides: unknown_overrides, }); } @@ -148,7 +148,7 @@ impl<'src> Justfile<'src> { command.export(&self.settings, &dotenv, &scope); let status = InterruptHandler::guard(|| command.status()).map_err(|io_error| { - RuntimeError::CommandInvocation { + Error::CommandInvoke { binary: binary.clone(), arguments: arguments.clone(), io_error, @@ -156,7 +156,11 @@ impl<'src> Justfile<'src> { })?; if !status.success() { - process::exit(status.code().unwrap_or(EXIT_FAILURE)); + return Err(Error::CommandStatus { + binary: binary.clone(), + arguments: arguments.clone(), + status, + }); }; return Ok(()); @@ -166,7 +170,7 @@ impl<'src> Justfile<'src> { if let Some(value) = scope.value(variable) { print!("{}", value); } else { - return Err(RuntimeError::EvalUnknownVariable { + return Err(Error::EvalUnknownVariable { suggestion: self.suggest_variable(&variable), variable: variable.clone(), }); @@ -198,14 +202,14 @@ impl<'src> Justfile<'src> { } else if let Some(recipe) = self.first() { let min_arguments = recipe.min_arguments(); if min_arguments > 0 { - return Err(RuntimeError::DefaultRecipeRequiresArguments { + return Err(Error::DefaultRecipeRequiresArguments { recipe: recipe.name.lexeme(), min_arguments, }); } vec![recipe.name()] } else { - return Err(RuntimeError::NoRecipes); + return Err(Error::NoRecipes); }; let arguments = argvec.as_slice(); @@ -222,9 +226,9 @@ impl<'src> Justfile<'src> { let argument_range = recipe.argument_range(); let argument_count = cmp::min(tail.len(), recipe.max_arguments()); if !argument_range.range_contains(&argument_count) { - return Err(RuntimeError::ArgumentCountMismatch { + return Err(Error::ArgumentCountMismatch { recipe: recipe.name(), - parameters: recipe.parameters.iter().collect(), + parameters: recipe.parameters.clone(), found: tail.len(), min: recipe.min_arguments(), max: recipe.max_arguments(), @@ -234,7 +238,7 @@ impl<'src> Justfile<'src> { tail = &tail[argument_count..]; } } else { - missing.push(*argument); + missing.push((*argument).to_owned()); } rest = tail; } @@ -245,7 +249,7 @@ impl<'src> Justfile<'src> { } else { None }; - return Err(RuntimeError::UnknownRecipes { + return Err(Error::UnknownRecipes { recipes: missing, suggestion, }); @@ -266,7 +270,7 @@ impl<'src> Justfile<'src> { Ok(()) } - pub(crate) fn get_alias(&self, name: &str) -> Option<&Alias> { + pub(crate) fn get_alias(&self, name: &str) -> Option<&Alias<'src>> { self.aliases.get(name) } @@ -278,13 +282,13 @@ impl<'src> Justfile<'src> { .or_else(|| self.aliases.get(name).map(|alias| alias.target.as_ref())) } - fn run_recipe<'run>( + fn run_recipe( &self, - context: &'run RecipeContext<'src, 'run>, + context: &RecipeContext<'src, '_>, recipe: &Recipe<'src>, - arguments: &[&'run str], + arguments: &[&str], dotenv: &BTreeMap, - search: &'run Search, + search: &Search, ran: &mut BTreeSet>, ) -> RunResult<'src, ()> { let (outer, positional) = Evaluator::evaluate_parameters( @@ -351,7 +355,7 @@ impl<'src> Justfile<'src> { Ok(()) } - pub(crate) fn public_recipes(&self, source_order: bool) -> Vec<&Recipe> { + pub(crate) fn public_recipes(&self, source_order: bool) -> Vec<&Recipe<'src, Dependency>> { let mut recipes = self .recipes .values() @@ -403,7 +407,7 @@ mod tests { use super::*; use testing::compile; - use RuntimeError::*; + use Error::*; run_error! { name: unknown_recipes, diff --git a/src/lexer.rs b/src/lexer.rs index 04527d3..6e57cef 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -1,6 +1,6 @@ use crate::common::*; -use CompilationErrorKind::*; +use CompileErrorKind::*; use TokenKind::*; /// Just language lexer @@ -38,7 +38,7 @@ pub(crate) struct Lexer<'src> { impl<'src> Lexer<'src> { /// Lex `text` - pub(crate) fn lex(src: &str) -> CompilationResult> { + pub(crate) fn lex(src: &'src str) -> CompileResult>> { Lexer::new(src).tokenize() } @@ -70,7 +70,7 @@ impl<'src> Lexer<'src> { /// Advance over the character in `self.next`, updating `self.token_end` /// accordingly. - fn advance(&mut self) -> CompilationResult<'src, ()> { + fn advance(&mut self) -> CompileResult<'src, ()> { match self.next { Some(c) => { let len_utf8 = c.len_utf8(); @@ -92,7 +92,7 @@ impl<'src> Lexer<'src> { } /// Advance over N characters. - fn skip(&mut self, n: usize) -> CompilationResult<'src, ()> { + fn skip(&mut self, n: usize) -> CompileResult<'src, ()> { for _ in 0..n { self.advance()?; } @@ -110,7 +110,7 @@ impl<'src> Lexer<'src> { self.token_end.offset - self.token_start.offset } - fn accepted(&mut self, c: char) -> CompilationResult<'src, bool> { + fn accepted(&mut self, c: char) -> CompileResult<'src, bool> { if self.next_is(c) { self.advance()?; Ok(true) @@ -119,7 +119,7 @@ impl<'src> Lexer<'src> { } } - fn presume(&mut self, c: char) -> CompilationResult<'src, ()> { + fn presume(&mut self, c: char) -> CompileResult<'src, ()> { if !self.next_is(c) { return Err(self.internal_error(format!("Lexer presumed character `{}`", c))); } @@ -129,7 +129,7 @@ impl<'src> Lexer<'src> { Ok(()) } - fn presume_str(&mut self, s: &str) -> CompilationResult<'src, ()> { + fn presume_str(&mut self, s: &str) -> CompileResult<'src, ()> { for c in s.chars() { self.presume(c)?; } @@ -199,7 +199,7 @@ impl<'src> Lexer<'src> { } /// Create an internal error with `message` - fn internal_error(&self, message: impl Into) -> CompilationError<'src> { + fn internal_error(&self, message: impl Into) -> CompileError<'src> { // Use `self.token_end` as the location of the error let token = Token { src: self.src, @@ -209,8 +209,8 @@ impl<'src> Lexer<'src> { length: 0, kind: Unspecified, }; - CompilationError { - kind: CompilationErrorKind::Internal { + CompileError { + kind: CompileErrorKind::Internal { message: message.into(), }, token, @@ -218,7 +218,7 @@ impl<'src> Lexer<'src> { } /// Create a compilation error with `kind` - fn error(&self, kind: CompilationErrorKind<'src>) -> CompilationError<'src> { + fn error(&self, kind: CompileErrorKind<'src>) -> CompileError<'src> { // Use the in-progress token span as the location of the error. // The width of the error site to highlight depends on the kind of error: @@ -244,11 +244,11 @@ impl<'src> Lexer<'src> { length, }; - CompilationError { token, kind } + CompileError { token, kind } } - fn unterminated_interpolation_error(interpolation_start: Token<'src>) -> CompilationError<'src> { - CompilationError { + fn unterminated_interpolation_error(interpolation_start: Token<'src>) -> CompileError<'src> { + CompileError { token: interpolation_start, kind: UnterminatedInterpolation, } @@ -289,7 +289,7 @@ impl<'src> Lexer<'src> { } /// Consume the text and produce a series of tokens - fn tokenize(mut self) -> CompilationResult<'src, Vec>> { + fn tokenize(mut self) -> CompileResult<'src, Vec>> { loop { if self.token_start.column == 0 { self.lex_line_start()?; @@ -327,7 +327,7 @@ impl<'src> Lexer<'src> { } /// Handle blank lines and indentation - fn lex_line_start(&mut self) -> CompilationResult<'src, ()> { + fn lex_line_start(&mut self) -> CompileResult<'src, ()> { enum Indentation<'src> { // Line only contains whitespace Blank, @@ -477,7 +477,7 @@ impl<'src> Lexer<'src> { } /// Lex token beginning with `start` outside of a recipe body - fn lex_normal(&mut self, start: char) -> CompilationResult<'src, ()> { + fn lex_normal(&mut self, start: char) -> CompileResult<'src, ()> { match start { '&' => self.lex_digraph('&', '&', AmpersandAmpersand), '!' => self.lex_digraph('!', '=', BangEquals), @@ -513,7 +513,7 @@ impl<'src> Lexer<'src> { &mut self, interpolation_start: Token<'src>, start: char, - ) -> CompilationResult<'src, ()> { + ) -> CompileResult<'src, ()> { if self.rest_starts_with("}}") { // end current interpolation if self.interpolation_stack.pop().is_none() { @@ -536,7 +536,7 @@ impl<'src> Lexer<'src> { } /// Lex token while in recipe body - fn lex_body(&mut self) -> CompilationResult<'src, ()> { + fn lex_body(&mut self) -> CompileResult<'src, ()> { enum Terminator { Newline, NewlineCarriageReturn, @@ -599,14 +599,14 @@ impl<'src> Lexer<'src> { } /// Lex a single-character token - fn lex_single(&mut self, kind: TokenKind) -> CompilationResult<'src, ()> { + fn lex_single(&mut self, kind: TokenKind) -> CompileResult<'src, ()> { self.advance()?; self.token(kind); Ok(()) } /// Lex a double-character token - fn lex_double(&mut self, kind: TokenKind) -> CompilationResult<'src, ()> { + fn lex_double(&mut self, kind: TokenKind) -> CompileResult<'src, ()> { self.advance()?; self.advance()?; self.token(kind); @@ -621,7 +621,7 @@ impl<'src> Lexer<'src> { second: char, then: TokenKind, otherwise: TokenKind, - ) -> CompilationResult<'src, ()> { + ) -> CompileResult<'src, ()> { self.advance()?; if self.accepted(second)? { @@ -634,7 +634,7 @@ impl<'src> Lexer<'src> { } /// Lex an opening or closing delimiter - fn lex_delimiter(&mut self, kind: TokenKind) -> CompilationResult<'src, ()> { + fn lex_delimiter(&mut self, kind: TokenKind) -> CompileResult<'src, ()> { use Delimiter::*; match kind { @@ -663,7 +663,7 @@ impl<'src> Lexer<'src> { } /// Pop a delimiter from the open delimiter stack and error if incorrect type - fn close_delimiter(&mut self, close: Delimiter) -> CompilationResult<'src, ()> { + fn close_delimiter(&mut self, close: Delimiter) -> CompileResult<'src, ()> { match self.open_delimiters.pop() { Some((open, _)) if open == close => Ok(()), Some((open, open_line)) => Err(self.error(MismatchedClosingDelimiter { @@ -681,12 +681,7 @@ impl<'src> Lexer<'src> { } /// Lex a two-character digraph - fn lex_digraph( - &mut self, - left: char, - right: char, - token: TokenKind, - ) -> CompilationResult<'src, ()> { + fn lex_digraph(&mut self, left: char, right: char, token: TokenKind) -> CompileResult<'src, ()> { self.presume(left)?; if self.accepted(right)? { @@ -708,7 +703,7 @@ impl<'src> Lexer<'src> { } /// Lex a token starting with ':' - fn lex_colon(&mut self) -> CompilationResult<'src, ()> { + fn lex_colon(&mut self) -> CompileResult<'src, ()> { self.presume(':')?; if self.accepted('=')? { @@ -722,7 +717,7 @@ impl<'src> Lexer<'src> { } /// Lex a carriage return and line feed - fn lex_eol(&mut self) -> CompilationResult<'src, ()> { + fn lex_eol(&mut self) -> CompileResult<'src, ()> { if self.accepted('\r')? { if !self.accepted('\n')? { return Err(self.error(UnpairedCarriageReturn)); @@ -743,7 +738,7 @@ impl<'src> Lexer<'src> { } /// Lex name: [a-zA-Z_][a-zA-Z0-9_]* - fn lex_identifier(&mut self) -> CompilationResult<'src, ()> { + fn lex_identifier(&mut self) -> CompileResult<'src, ()> { self.advance()?; while let Some(c) = self.next { @@ -760,7 +755,7 @@ impl<'src> Lexer<'src> { } /// Lex comment: #[^\r\n] - fn lex_comment(&mut self) -> CompilationResult<'src, ()> { + fn lex_comment(&mut self) -> CompileResult<'src, ()> { self.presume('#')?; while !self.at_eol_or_eof() { @@ -773,7 +768,7 @@ impl<'src> Lexer<'src> { } /// Lex whitespace: [ \t]+ - fn lex_whitespace(&mut self) -> CompilationResult<'src, ()> { + fn lex_whitespace(&mut self) -> CompileResult<'src, ()> { while self.next_is_whitespace() { self.advance()?; } @@ -788,7 +783,7 @@ impl<'src> Lexer<'src> { /// Backtick: `[^`]*` /// Cooked string: "[^"]*" # also processes escape sequences /// Raw string: '[^']*' - fn lex_string(&mut self) -> CompilationResult<'src, ()> { + fn lex_string(&mut self) -> CompileResult<'src, ()> { let kind = if let Some(kind) = StringKind::from_token_start(self.rest()) { kind } else { @@ -980,12 +975,12 @@ mod tests { line: usize, column: usize, length: usize, - kind: CompilationErrorKind, + kind: CompileErrorKind, ) { match Lexer::lex(src) { Ok(_) => panic!("Lexing succeeded but expected"), Err(have) => { - let want = CompilationError { + let want = CompileError { token: Token { kind: have.token.kind, src, @@ -2285,9 +2280,10 @@ mod tests { #[test] fn presume_error() { + let compile_error = Lexer::new("!").presume('-').unwrap_err(); assert_matches!( - Lexer::new("!").presume('-').unwrap_err(), - CompilationError { + compile_error, + CompileError { token: Token { offset: 0, line: 0, @@ -2297,22 +2293,22 @@ mod tests { kind: Unspecified, }, kind: Internal { - message, + ref message, }, } if message == "Lexer presumed character `-`" ); + let mut cursor = Cursor::new(Vec::new()); + + Error::Compile { compile_error } + .write(&mut cursor, Color::never()) + .unwrap(); + assert_eq!( - Lexer::new("!").presume('-').unwrap_err().to_string(), - unindent( - " - Internal error, this may indicate a bug in just: Lexer presumed character `-` - \ - consider filing an issue: https://github.com/casey/just/issues/new - | - 1 | ! - | ^" - ), + str::from_utf8(&cursor.into_inner()).unwrap(), + "error: Internal error, this may indicate a bug in just: \ + Lexer presumed character `-`\nconsider filing an issue: \ + https://github.com/casey/just/issues/new\n |\n1 | !\n | ^\n" ); } } diff --git a/src/lib.rs b/src/lib.rs index b6bed73..8af492b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,7 @@ clippy::missing_docs_in_private_items, clippy::missing_errors_doc, clippy::missing_inline_in_public_items, + clippy::needless_lifetimes, clippy::needless_pass_by_value, clippy::non_ascii_literal, clippy::option_if_let_else, @@ -66,8 +67,8 @@ mod binding; mod color; mod command_ext; mod common; -mod compilation_error; -mod compilation_error_kind; +mod compile_error; +mod compile_error_kind; mod compiler; mod config; mod config_error; @@ -76,7 +77,6 @@ mod delimiter; mod dependency; mod enclosure; mod error; -mod error_result_ext; mod evaluator; mod expression; mod fragment; @@ -92,7 +92,7 @@ mod lexer; mod line; mod list; mod load_dotenv; -mod load_error; +mod loader; mod name; mod ordinal; mod output; @@ -109,7 +109,6 @@ mod recipe; mod recipe_context; mod recipe_resolver; mod run; -mod runtime_error; mod scope; mod search; mod search_config; diff --git a/src/load_error.rs b/src/load_error.rs deleted file mode 100644 index 2a4beda..0000000 --- a/src/load_error.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::common::*; - -pub(crate) struct LoadError<'path> { - pub(crate) path: &'path Path, - pub(crate) io_error: io::Error, -} - -impl Error for LoadError<'_> {} - -impl Display for LoadError<'_> { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!( - f, - "Failed to read justfile at `{}`: {}", - self.path.display(), - self.io_error - ) - } -} diff --git a/src/loader.rs b/src/loader.rs new file mode 100644 index 0000000..37bd1c8 --- /dev/null +++ b/src/loader.rs @@ -0,0 +1,21 @@ +use crate::common::*; + +pub(crate) struct Loader { + arena: Arena, +} + +impl Loader { + pub(crate) fn new() -> Self { + Loader { + arena: Arena::new(), + } + } + + pub(crate) fn load<'src>(&'src self, path: &Path) -> RunResult<&'src str> { + let src = fs::read_to_string(path).map_err(|io_error| Error::Load { + path: path.to_owned(), + io_error, + })?; + Ok(self.arena.alloc(src)) + } +} diff --git a/src/name.rs b/src/name.rs index f1030b4..935c132 100644 --- a/src/name.rs +++ b/src/name.rs @@ -40,7 +40,7 @@ impl<'src> Name<'src> { } } - pub(crate) fn error(&self, kind: CompilationErrorKind<'src>) -> CompilationError<'src> { + pub(crate) fn error(&self, kind: CompileErrorKind<'src>) -> CompileError<'src> { self.token().error(kind) } } diff --git a/src/parser.rs b/src/parser.rs index ac3264b..16446ec 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -36,7 +36,7 @@ pub(crate) struct Parser<'tokens, 'src> { impl<'tokens, 'src> Parser<'tokens, 'src> { /// Parse `tokens` into an `Ast` - pub(crate) fn parse(tokens: &'tokens [Token<'src>]) -> CompilationResult<'src, Ast<'src>> { + pub(crate) fn parse(tokens: &'tokens [Token<'src>]) -> CompileResult<'src, Ast<'src>> { Self::new(tokens).parse_ast() } @@ -49,27 +49,21 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } } - fn error( - &self, - kind: CompilationErrorKind<'src>, - ) -> CompilationResult<'src, CompilationError<'src>> { + fn error(&self, kind: CompileErrorKind<'src>) -> CompileResult<'src, CompileError<'src>> { Ok(self.next()?.error(kind)) } /// Construct an unexpected token error with the token returned by /// `Parser::next` - fn unexpected_token(&self) -> CompilationResult<'src, CompilationError<'src>> { - self.error(CompilationErrorKind::UnexpectedToken { + fn unexpected_token(&self) -> CompileResult<'src, CompileError<'src>> { + self.error(CompileErrorKind::UnexpectedToken { expected: self.expected.iter().cloned().collect::>(), found: self.next()?.kind, }) } - fn internal_error( - &self, - message: impl Into, - ) -> CompilationResult<'src, CompilationError<'src>> { - self.error(CompilationErrorKind::Internal { + fn internal_error(&self, message: impl Into) -> CompileResult<'src, CompileError<'src>> { + self.error(CompileErrorKind::Internal { message: message.into(), }) } @@ -83,7 +77,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// The next significant token - fn next(&self) -> CompilationResult<'src, Token<'src>> { + fn next(&self) -> CompileResult<'src, Token<'src>> { if let Some(token) = self.rest().next() { Ok(token) } else { @@ -118,7 +112,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// Get the `n`th next significant token - fn get(&self, n: usize) -> CompilationResult<'src, Token<'src>> { + fn get(&self, n: usize) -> CompileResult<'src, Token<'src>> { match self.rest().nth(n) { Some(token) => Ok(token), None => Err(self.internal_error("`Parser::get()` advanced past end of token stream")?), @@ -126,7 +120,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// Advance past one significant token, clearing the expected token set. - fn advance(&mut self) -> CompilationResult<'src, Token<'src>> { + fn advance(&mut self) -> CompileResult<'src, Token<'src>> { self.expected.clear(); for skipped in &self.tokens[self.next..] { @@ -142,7 +136,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { /// Return the next token if it is of kind `expected`, otherwise, return an /// unexpected token error - fn expect(&mut self, expected: TokenKind) -> CompilationResult<'src, Token<'src>> { + fn expect(&mut self, expected: TokenKind) -> CompileResult<'src, Token<'src>> { if let Some(token) = self.accept(expected)? { Ok(token) } else { @@ -151,7 +145,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// Return an unexpected token error if the next token is not an EOL - fn expect_eol(&mut self) -> CompilationResult<'src, ()> { + fn expect_eol(&mut self) -> CompileResult<'src, ()> { self.accept(Comment)?; if self.next_is(Eof) { @@ -161,14 +155,14 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { self.expect(Eol).map(|_| ()) } - fn expect_keyword(&mut self, expected: Keyword) -> CompilationResult<'src, ()> { + fn expect_keyword(&mut self, expected: Keyword) -> CompileResult<'src, ()> { let identifier = self.expect(Identifier)?; let found = identifier.lexeme(); if expected == found { Ok(()) } else { - Err(identifier.error(CompilationErrorKind::ExpectedKeyword { + Err(identifier.error(CompileErrorKind::ExpectedKeyword { expected: vec![expected], found, })) @@ -177,7 +171,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { /// Return an internal error if the next token is not of kind `Identifier` /// with lexeme `lexeme`. - fn presume_keyword(&mut self, keyword: Keyword) -> CompilationResult<'src, ()> { + fn presume_keyword(&mut self, keyword: Keyword) -> CompileResult<'src, ()> { let next = self.advance()?; if next.kind != Identifier { @@ -197,7 +191,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// Return an internal error if the next token is not of kind `kind`. - fn presume(&mut self, kind: TokenKind) -> CompilationResult<'src, Token<'src>> { + fn presume(&mut self, kind: TokenKind) -> CompileResult<'src, Token<'src>> { let next = self.advance()?; if next.kind != kind { @@ -211,7 +205,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// Return an internal error if the next token is not one of kinds `kinds`. - fn presume_any(&mut self, kinds: &[TokenKind]) -> CompilationResult<'src, Token<'src>> { + fn presume_any(&mut self, kinds: &[TokenKind]) -> CompileResult<'src, Token<'src>> { let next = self.advance()?; if !kinds.contains(&next.kind) { Err(self.internal_error(format!( @@ -225,7 +219,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// Accept and return a token of kind `kind` - fn accept(&mut self, kind: TokenKind) -> CompilationResult<'src, Option>> { + fn accept(&mut self, kind: TokenKind) -> CompileResult<'src, Option>> { if self.next_is(kind) { Ok(Some(self.advance()?)) } else { @@ -234,9 +228,9 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// Return an error if the next token is of kind `forbidden` - fn forbid(&self, forbidden: TokenKind, error: F) -> CompilationResult<'src, ()> + fn forbid(&self, forbidden: TokenKind, error: F) -> CompileResult<'src, ()> where - F: FnOnce(Token) -> CompilationError, + F: FnOnce(Token) -> CompileError, { let next = self.next()?; @@ -248,7 +242,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// Accept a token of kind `Identifier` and parse into a `Name` - fn accept_name(&mut self) -> CompilationResult<'src, Option>> { + fn accept_name(&mut self) -> CompileResult<'src, Option>> { if self.next_is(Identifier) { Ok(Some(self.parse_name()?)) } else { @@ -256,7 +250,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } } - fn accepted_keyword(&mut self, keyword: Keyword) -> CompilationResult<'src, bool> { + fn accepted_keyword(&mut self, keyword: Keyword) -> CompileResult<'src, bool> { let next = self.next()?; if next.kind == Identifier && next.lexeme() == keyword.lexeme() { @@ -268,7 +262,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// Accept a dependency - fn accept_dependency(&mut self) -> CompilationResult<'src, Option>> { + fn accept_dependency(&mut self) -> CompileResult<'src, Option>> { if let Some(recipe) = self.accept_name()? { Ok(Some(UnresolvedDependency { arguments: Vec::new(), @@ -290,12 +284,12 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// Accept and return `true` if next token is of kind `kind` - fn accepted(&mut self, kind: TokenKind) -> CompilationResult<'src, bool> { + fn accepted(&mut self, kind: TokenKind) -> CompileResult<'src, bool> { Ok(self.accept(kind)?.is_some()) } /// Parse a justfile, consumes self - fn parse_ast(mut self) -> CompilationResult<'src, Ast<'src>> { + fn parse_ast(mut self) -> CompileResult<'src, Ast<'src>> { fn pop_doc_comment<'src>( items: &mut Vec>, eol_since_last_comment: bool, @@ -330,7 +324,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { match Keyword::from_lexeme(next.lexeme()) { Some(Keyword::Alias) => if self.next_are(&[Identifier, Identifier, Equals]) { - return Err(self.get(2)?.error(CompilationErrorKind::DeprecatedEquals)); + return Err(self.get(2)?.error(CompileErrorKind::DeprecatedEquals)); } else if self.next_are(&[Identifier, Identifier, ColonEquals]) { items.push(Item::Alias(self.parse_alias()?)); } else { @@ -339,7 +333,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { }, Some(Keyword::Export) => if self.next_are(&[Identifier, Identifier, Equals]) { - return Err(self.get(2)?.error(CompilationErrorKind::DeprecatedEquals)); + return Err(self.get(2)?.error(CompileErrorKind::DeprecatedEquals)); } else if self.next_are(&[Identifier, Identifier, ColonEquals]) { self.presume_keyword(Keyword::Export)?; items.push(Item::Assignment(self.parse_assignment(true)?)); @@ -359,7 +353,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { }, _ => if self.next_are(&[Identifier, Equals]) { - return Err(self.get(1)?.error(CompilationErrorKind::DeprecatedEquals)); + return Err(self.get(1)?.error(CompileErrorKind::DeprecatedEquals)); } else if self.next_are(&[Identifier, ColonEquals]) { items.push(Item::Assignment(self.parse_assignment(false)?)); } else { @@ -389,7 +383,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// Parse an alias, e.g `alias name := target` - fn parse_alias(&mut self) -> CompilationResult<'src, Alias<'src, Name<'src>>> { + fn parse_alias(&mut self) -> CompileResult<'src, Alias<'src, Name<'src>>> { self.presume_keyword(Keyword::Alias)?; let name = self.parse_name()?; self.presume_any(&[Equals, ColonEquals])?; @@ -399,7 +393,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// Parse an assignment, e.g. `foo := bar` - fn parse_assignment(&mut self, export: bool) -> CompilationResult<'src, Assignment<'src>> { + fn parse_assignment(&mut self, export: bool) -> CompileResult<'src, Assignment<'src>> { let name = self.parse_name()?; self.presume_any(&[Equals, ColonEquals])?; let value = self.parse_expression()?; @@ -412,7 +406,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// Parse an expression, e.g. `1 + 2` - fn parse_expression(&mut self) -> CompilationResult<'src, Expression<'src>> { + fn parse_expression(&mut self) -> CompileResult<'src, Expression<'src>> { if self.accepted_keyword(Keyword::If)? { self.parse_conditional() } else { @@ -429,7 +423,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// Parse a conditional, e.g. `if a == b { "foo" } else { "bar" }` - fn parse_conditional(&mut self) -> CompilationResult<'src, Expression<'src>> { + fn parse_conditional(&mut self) -> CompileResult<'src, Expression<'src>> { let lhs = self.parse_expression()?; let inverted = self.accepted(BangEquals)?; @@ -467,7 +461,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// Parse a value, e.g. `(bar)` - fn parse_value(&mut self) -> CompilationResult<'src, Expression<'src>> { + fn parse_value(&mut self) -> CompileResult<'src, Expression<'src>> { if self.next_is(StringToken) { Ok(Expression::StringLiteral { string_literal: self.parse_string_literal()?, @@ -485,7 +479,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { }; if contents.starts_with("#!") { - return Err(next.error(CompilationErrorKind::BacktickShebang)); + return Err(next.error(CompileErrorKind::BacktickShebang)); } Ok(Expression::Backtick { contents, token }) @@ -511,7 +505,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// Parse a string literal, e.g. `"FOO"` - fn parse_string_literal(&mut self) -> CompilationResult<'src, StringLiteral<'src>> { + fn parse_string_literal(&mut self) -> CompileResult<'src, StringLiteral<'src>> { let token = self.expect(StringToken)?; let kind = StringKind::from_string_or_backtick(token)?; @@ -540,7 +534,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { '"' => cooked.push('"'), other => { return Err( - token.error(CompilationErrorKind::InvalidEscapeSequence { character: other }), + token.error(CompileErrorKind::InvalidEscapeSequence { character: other }), ); }, } @@ -560,12 +554,12 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// Parse a name from an identifier token - fn parse_name(&mut self) -> CompilationResult<'src, Name<'src>> { + fn parse_name(&mut self) -> CompileResult<'src, Name<'src>> { self.expect(Identifier).map(Name::from_identifier) } /// Parse sequence of comma-separated expressions - fn parse_sequence(&mut self) -> CompilationResult<'src, Vec>> { + fn parse_sequence(&mut self) -> CompileResult<'src, Vec>> { self.presume(ParenL)?; let mut elements = Vec::new(); @@ -588,7 +582,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { &mut self, doc: Option<&'src str>, quiet: bool, - ) -> CompilationResult<'src, UnresolvedRecipe<'src>> { + ) -> CompileResult<'src, UnresolvedRecipe<'src>> { let name = self.parse_name()?; let mut positional = Vec::new(); @@ -609,7 +603,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { let variadic = self.parse_parameter(kind)?; self.forbid(Identifier, |token| { - token.error(CompilationErrorKind::ParameterFollowsVariadicParameter { + token.error(CompileErrorKind::ParameterFollowsVariadicParameter { parameter: token.lexeme(), }) })?; @@ -661,7 +655,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// Parse a recipe parameter - fn parse_parameter(&mut self, kind: ParameterKind) -> CompilationResult<'src, Parameter<'src>> { + fn parse_parameter(&mut self, kind: ParameterKind) -> CompileResult<'src, Parameter<'src>> { let export = self.accepted(Dollar)?; let name = self.parse_name()?; @@ -681,7 +675,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// Parse the body of a recipe - fn parse_body(&mut self) -> CompilationResult<'src, Vec>> { + fn parse_body(&mut self) -> CompileResult<'src, Vec>> { let mut lines = Vec::new(); if self.accepted(Indent)? { @@ -721,7 +715,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// Parse a boolean setting value - fn parse_set_bool(&mut self) -> CompilationResult<'src, bool> { + fn parse_set_bool(&mut self) -> CompileResult<'src, bool> { if !self.accepted(ColonEquals)? { return Ok(true); } @@ -733,7 +727,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } else if Keyword::False == identifier.lexeme() { false } else { - return Err(identifier.error(CompilationErrorKind::ExpectedKeyword { + return Err(identifier.error(CompileErrorKind::ExpectedKeyword { expected: vec![Keyword::True, Keyword::False], found: identifier.lexeme(), })); @@ -743,7 +737,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } /// Parse a setting - fn parse_set(&mut self) -> CompilationResult<'src, Set<'src>> { + fn parse_set(&mut self) -> CompileResult<'src, Set<'src>> { self.presume_keyword(Keyword::Set)?; let name = Name::from_identifier(self.presume(Identifier)?); let lexeme = name.lexeme(); @@ -794,7 +788,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { name, }) } else { - Err(name.error(CompilationErrorKind::UnknownSetting { + Err(name.error(CompileErrorKind::UnknownSetting { setting: name.lexeme(), })) } @@ -806,7 +800,7 @@ mod tests { use super::*; use pretty_assertions::assert_eq; - use CompilationErrorKind::*; + use CompileErrorKind::*; macro_rules! test { { @@ -860,14 +854,14 @@ mod tests { line: usize, column: usize, length: usize, - kind: CompilationErrorKind, + kind: CompileErrorKind, ) { let tokens = Lexer::lex(src).expect("Lexing failed in parse test..."); match Parser::parse(&tokens) { Ok(_) => panic!("Parsing unexpectedly succeeded"), Err(have) => { - let want = CompilationError { + let want = CompileError { token: Token { kind: have.token.kind, src, diff --git a/src/recipe.rs b/src/recipe.rs index bc3832a..184f711 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -2,20 +2,16 @@ use crate::common::*; use std::process::{ExitStatus, Stdio}; -/// Return a `RuntimeError::Signal` if the process was terminated by a signal, -/// otherwise return an `RuntimeError::UnknownFailure` -fn error_from_signal( - recipe: &str, - line_number: Option, - exit_status: ExitStatus, -) -> RuntimeError { +/// Return a `Error::Signal` if the process was terminated by a signal, +/// otherwise return an `Error::UnknownFailure` +fn error_from_signal(recipe: &str, line_number: Option, exit_status: ExitStatus) -> Error { match Platform::signal_from_exit_status(exit_status) { - Some(signal) => RuntimeError::Signal { + Some(signal) => Error::Signal { recipe, line_number, signal, }, - None => RuntimeError::Unknown { + None => Error::Unknown { recipe, line_number, }, @@ -108,20 +104,18 @@ impl<'src, D> Recipe<'src, D> { return Ok(()); } - let shebang_line = evaluated_lines - .first() - .ok_or_else(|| RuntimeError::Internal { - message: "evaluated_lines was empty".to_owned(), - })?; + let shebang_line = evaluated_lines.first().ok_or_else(|| Error::Internal { + message: "evaluated_lines was empty".to_owned(), + })?; - let shebang = Shebang::new(shebang_line).ok_or_else(|| RuntimeError::Internal { + let shebang = Shebang::new(shebang_line).ok_or_else(|| Error::Internal { message: format!("bad shebang line: {}", shebang_line), })?; let tmp = tempfile::Builder::new() .prefix("just") .tempdir() - .map_err(|error| RuntimeError::TmpdirIoError { + .map_err(|error| Error::TmpdirIo { recipe: self.name(), io_error: error, })?; @@ -130,7 +124,7 @@ impl<'src, D> Recipe<'src, D> { path.push(shebang.script_filename(self.name())); { - let mut f = fs::File::create(&path).map_err(|error| RuntimeError::TmpdirIoError { + let mut f = fs::File::create(&path).map_err(|error| Error::TmpdirIo { recipe: self.name(), io_error: error, })?; @@ -158,14 +152,14 @@ impl<'src, D> Recipe<'src, D> { } f.write_all(text.as_bytes()) - .map_err(|error| RuntimeError::TmpdirIoError { + .map_err(|error| Error::TmpdirIo { recipe: self.name(), io_error: error, })?; } // make the script executable - Platform::set_execute_permission(&path).map_err(|error| RuntimeError::TmpdirIoError { + Platform::set_execute_permission(&path).map_err(|error| Error::TmpdirIo { recipe: self.name(), io_error: error, })?; @@ -173,7 +167,7 @@ impl<'src, D> Recipe<'src, D> { // create a command to run the script let mut command = Platform::make_shebang_command(&path, &context.search.working_directory, shebang).map_err( - |output_error| RuntimeError::Cygpath { + |output_error| Error::Cygpath { recipe: self.name(), output_error, }, @@ -190,7 +184,7 @@ impl<'src, D> Recipe<'src, D> { Ok(exit_status) => if let Some(code) = exit_status.code() { if code != 0 { - return Err(RuntimeError::Code { + return Err(Error::Code { recipe: self.name(), line_number: None, code, @@ -200,7 +194,7 @@ impl<'src, D> Recipe<'src, D> { return Err(error_from_signal(self.name(), None, exit_status)); }, Err(io_error) => { - return Err(RuntimeError::Shebang { + return Err(Error::Shebang { recipe: self.name(), command: shebang.interpreter.to_owned(), argument: shebang.argument.map(String::from), @@ -283,7 +277,7 @@ impl<'src, D> Recipe<'src, D> { Ok(exit_status) => if let Some(code) = exit_status.code() { if code != 0 && !infallable_command { - return Err(RuntimeError::Code { + return Err(Error::Code { recipe: self.name(), line_number: Some(line_number), code, @@ -297,7 +291,7 @@ impl<'src, D> Recipe<'src, D> { )); }, Err(io_error) => { - return Err(RuntimeError::IoError { + return Err(Error::Io { recipe: self.name(), io_error, }); diff --git a/src/recipe_resolver.rs b/src/recipe_resolver.rs index 0c5e6cb..bb8215b 100644 --- a/src/recipe_resolver.rs +++ b/src/recipe_resolver.rs @@ -1,6 +1,6 @@ use crate::common::*; -use CompilationErrorKind::*; +use CompileErrorKind::*; pub(crate) struct RecipeResolver<'src: 'run, 'run> { unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>, @@ -12,7 +12,7 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> { pub(crate) fn resolve_recipes( unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>, assignments: &Table<'src, Assignment<'src>>, - ) -> CompilationResult<'src, Table<'src, Rc>>> { + ) -> CompileResult<'src, Table<'src, Rc>>> { let mut resolver = RecipeResolver { resolved_recipes: Table::new(), unresolved_recipes, @@ -58,7 +58,7 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> { &self, variable: &Token<'src>, parameters: &[Parameter], - ) -> CompilationResult<'src, ()> { + ) -> CompileResult<'src, ()> { let name = variable.lexeme(); let undefined = !self.assignments.contains_key(name) && !parameters.iter().any(|p| p.name.lexeme() == name); @@ -74,7 +74,7 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> { &mut self, stack: &mut Vec<&'src str>, recipe: UnresolvedRecipe<'src>, - ) -> CompilationResult<'src, Rc>> { + ) -> CompileResult<'src, Rc>> { if let Some(resolved) = self.resolved_recipes.get(recipe.name()) { return Ok(Rc::clone(resolved)); } diff --git a/src/run.rs b/src/run.rs index 84e787b..f9162b0 100644 --- a/src/run.rs +++ b/src/run.rs @@ -16,7 +16,22 @@ pub fn run() -> Result<(), i32> { info!("Parsing command line arguments…"); let matches = app.get_matches(); - let config = Config::from_matches(&matches).eprint(Color::auto())?; + let loader = Loader::new(); - config.run_subcommand() + let mut color = Color::auto(); + let mut verbosity = Verbosity::default(); + + Config::from_matches(&matches) + .map_err(Error::from) + .and_then(|config| { + color = config.color; + verbosity = config.verbosity; + config.run_subcommand(&loader) + }) + .map_err(|error| { + if !verbosity.quiet() { + error.write(&mut io::stderr(), color).ok(); + } + error.code().unwrap_or(EXIT_FAILURE) + }) } diff --git a/src/runtime_error.rs b/src/runtime_error.rs deleted file mode 100644 index 7aab4c8..0000000 --- a/src/runtime_error.rs +++ /dev/null @@ -1,437 +0,0 @@ -use crate::common::*; - -#[derive(Debug)] -pub(crate) enum RuntimeError<'src> { - ArgumentCountMismatch { - recipe: &'src str, - parameters: Vec<&'src Parameter<'src>>, - found: usize, - min: usize, - max: usize, - }, - Backtick { - token: Token<'src>, - output_error: OutputError, - }, - Code { - recipe: &'src str, - line_number: Option, - code: i32, - }, - CommandInvocation { - binary: OsString, - arguments: Vec, - io_error: io::Error, - }, - Cygpath { - recipe: &'src str, - output_error: OutputError, - }, - Dotenv { - dotenv_error: dotenv::Error, - }, - EvalUnknownVariable { - variable: String, - suggestion: Option>, - }, - FunctionCall { - function: Name<'src>, - message: String, - }, - Internal { - message: String, - }, - IoError { - recipe: &'src str, - io_error: io::Error, - }, - Shebang { - recipe: &'src str, - command: String, - argument: Option, - io_error: io::Error, - }, - Signal { - recipe: &'src str, - line_number: Option, - signal: i32, - }, - TmpdirIoError { - recipe: &'src str, - io_error: io::Error, - }, - UnknownOverrides { - overrides: Vec<&'src str>, - }, - UnknownRecipes { - recipes: Vec<&'src str>, - suggestion: Option>, - }, - Unknown { - recipe: &'src str, - line_number: Option, - }, - NoRecipes, - DefaultRecipeRequiresArguments { - recipe: &'src str, - min_arguments: usize, - }, -} - -impl<'src> Error for RuntimeError<'src> { - fn code(&self) -> i32 { - match *self { - Self::Code { code, .. } - | Self::Backtick { - output_error: OutputError::Code(code), - .. - } => code, - _ => EXIT_FAILURE, - } - } -} - -impl<'src> RuntimeError<'src> { - fn context(&self) -> Option { - use RuntimeError::*; - match self { - FunctionCall { function, .. } => Some(function.token()), - Backtick { token, .. } => Some(*token), - _ => None, - } - } -} - -impl<'src> Display for RuntimeError<'src> { - fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { - use RuntimeError::*; - - let color = if f.alternate() { - Color::always() - } else { - Color::never() - }; - let message = color.message(); - write!(f, "{}", message.prefix())?; - - match self { - EvalUnknownVariable { - variable, - suggestion, - } => { - write!(f, "Justfile does not contain variable `{}`.", variable,)?; - if let Some(suggestion) = *suggestion { - write!(f, "\n{}", suggestion)?; - } - }, - UnknownRecipes { - recipes, - suggestion, - } => { - write!( - f, - "Justfile does not contain {} {}.", - Count("recipe", recipes.len()), - List::or_ticked(recipes), - )?; - if let Some(suggestion) = *suggestion { - write!(f, "\n{}", suggestion)?; - } - }, - UnknownOverrides { overrides } => { - write!( - f, - "{} {} overridden on the command line but not present in justfile", - Count("Variable", overrides.len()), - List::and_ticked(overrides), - )?; - }, - ArgumentCountMismatch { - recipe, - parameters, - found, - min, - max, - } => { - if min == max { - let expected = min; - write!( - f, - "Recipe `{}` got {} {} but {}takes {}", - recipe, - found, - Count("argument", *found), - if expected < found { "only " } else { "" }, - expected - )?; - } else if found < min { - write!( - f, - "Recipe `{}` got {} {} but takes at least {}", - recipe, - found, - Count("argument", *found), - min - )?; - } else if found > max { - write!( - f, - "Recipe `{}` got {} {} but takes at most {}", - recipe, - found, - Count("argument", *found), - max - )?; - } - write!(f, "\nusage:\n just {}", recipe)?; - for param in parameters { - if color.stderr().active() { - write!(f, " {:#}", param)?; - } else { - write!(f, " {}", param)?; - } - } - }, - Code { - recipe, - line_number, - code, - } => - if let Some(n) = line_number { - write!( - f, - "Recipe `{}` failed on line {} with exit code {}", - recipe, n, code - )?; - } else { - write!(f, "Recipe `{}` failed with exit code {}", recipe, code)?; - }, - CommandInvocation { - binary, - arguments, - io_error, - } => { - write!( - f, - "Failed to invoke {}: {}", - iter::once(binary) - .chain(arguments) - .map(|value| Enclosure::tick(value.to_string_lossy()).to_string()) - .collect::>() - .join(" "), - io_error, - )?; - }, - Cygpath { - recipe, - output_error, - } => match output_error { - OutputError::Code(code) => { - write!( - f, - "Cygpath failed with exit code {} while translating recipe `{}` shebang interpreter \ - path", - code, recipe - )?; - }, - OutputError::Signal(signal) => { - write!( - f, - "Cygpath terminated by signal {} while translating recipe `{}` shebang interpreter \ - path", - signal, recipe - )?; - }, - OutputError::Unknown => { - write!( - f, - "Cygpath experienced an unknown failure while translating recipe `{}` shebang \ - interpreter path", - recipe - )?; - }, - OutputError::Io(io_error) => { - match io_error.kind() { - io::ErrorKind::NotFound => write!( - f, - "Could not find `cygpath` executable to translate recipe `{}` shebang interpreter \ - path:\n{}", - recipe, io_error - ), - io::ErrorKind::PermissionDenied => write!( - f, - "Could not run `cygpath` executable to translate recipe `{}` shebang interpreter \ - path:\n{}", - recipe, io_error - ), - _ => write!(f, "Could not run `cygpath` executable:\n{}", io_error), - }?; - }, - OutputError::Utf8(utf8_error) => { - write!( - f, - "Cygpath successfully translated recipe `{}` shebang interpreter path, but output was \ - not utf8: {}", - recipe, utf8_error - )?; - }, - }, - Dotenv { dotenv_error } => { - writeln!(f, "Failed to load .env: {}", dotenv_error)?; - }, - FunctionCall { function, message } => { - writeln!( - f, - "Call to function `{}` failed: {}", - function.lexeme(), - message - )?; - }, - Shebang { - recipe, - command, - argument, - io_error, - } => - if let Some(argument) = argument { - write!( - f, - "Recipe `{}` with shebang `#!{} {}` execution error: {}", - recipe, command, argument, io_error - )?; - } else { - write!( - f, - "Recipe `{}` with shebang `#!{}` execution error: {}", - recipe, command, io_error - )?; - }, - Signal { - recipe, - line_number, - signal, - } => - if let Some(n) = line_number { - write!( - f, - "Recipe `{}` was terminated on line {} by signal {}", - recipe, n, signal - )?; - } else { - write!(f, "Recipe `{}` was terminated by signal {}", recipe, signal)?; - }, - Unknown { - recipe, - line_number, - } => - if let Some(n) = line_number { - write!( - f, - "Recipe `{}` failed on line {} for an unknown reason", - recipe, n - )?; - } else { - write!(f, "Recipe `{}` failed for an unknown reason", recipe)?; - }, - IoError { recipe, io_error } => { - match io_error.kind() { - io::ErrorKind::NotFound => writeln!( - f, - "Recipe `{}` could not be run because just could not find `sh`:{}", - recipe, io_error - ), - io::ErrorKind::PermissionDenied => writeln!( - f, - "Recipe `{}` could not be run because just could not run `sh`:{}", - recipe, io_error - ), - _ => writeln!( - f, - "Recipe `{}` could not be run because of an IO error while launching `sh`:{}", - recipe, io_error - ), - }?; - }, - TmpdirIoError { recipe, io_error } => writeln!( - f, - "Recipe `{}` could not be run because of an IO error while trying to create a temporary \ - directory or write a file to that directory`:{}", - recipe, io_error - )?, - Backtick { output_error, .. } => match output_error { - OutputError::Code(code) => { - writeln!(f, "Backtick failed with exit code {}", code)?; - }, - OutputError::Signal(signal) => { - writeln!(f, "Backtick was terminated by signal {}", signal)?; - }, - OutputError::Unknown => { - writeln!(f, "Backtick failed for an unknown reason")?; - }, - OutputError::Io(io_error) => { - match io_error.kind() { - io::ErrorKind::NotFound => write!( - f, - "Backtick could not be run because just could not find `sh`:\n{}", - io_error - ), - io::ErrorKind::PermissionDenied => write!( - f, - "Backtick could not be run because just could not run `sh`:\n{}", - io_error - ), - _ => write!( - f, - "Backtick could not be run because of an IO error while launching `sh`:\n{}", - io_error - ), - }?; - }, - OutputError::Utf8(utf8_error) => { - writeln!( - f, - "Backtick succeeded but stdout was not utf8: {}", - utf8_error - )?; - }, - }, - NoRecipes => { - writeln!(f, "Justfile contains no recipes.",)?; - }, - DefaultRecipeRequiresArguments { - recipe, - min_arguments, - } => { - writeln!( - f, - "Recipe `{}` cannot be used as default recipe since it requires at least {} {}.", - recipe, - min_arguments, - Count("argument", *min_arguments), - )?; - }, - Internal { message } => { - write!( - f, - "Internal runtime error, this may indicate a bug in just: {} \ - consider filing an issue: https://github.com/casey/just/issues/new", - message - )?; - }, - } - - write!(f, "{}", message.suffix())?; - - if let Some(token) = self.context() { - token.write_context(f, Color::fmt(f).error())?; - } - - Ok(()) - } -} - -impl<'src> From for RuntimeError<'src> { - fn from(dotenv_error: dotenv::Error) -> RuntimeError<'src> { - RuntimeError::Dotenv { dotenv_error } - } -} diff --git a/src/search_error.rs b/src/search_error.rs index c058f23..6a8e708 100644 --- a/src/search_error.rs +++ b/src/search_error.rs @@ -3,6 +3,17 @@ use crate::common::*; #[derive(Debug, Snafu)] #[snafu(visibility(pub(crate)))] pub(crate) enum SearchError { + #[snafu(display( + "I/O error reading directory `{}`: {}", + directory.display(), + io_error + ))] + Io { + directory: PathBuf, + io_error: io::Error, + }, + #[snafu(display("Justfile path had no parent: {}", path.display()))] + JustfileHadNoParent { path: PathBuf }, #[snafu(display( "Multiple candidate justfiles found in `{}`: {}", candidates[0].parent().unwrap().display(), @@ -13,23 +24,10 @@ pub(crate) enum SearchError { ), ))] MultipleCandidates { candidates: Vec }, - #[snafu(display( - "I/O error reading directory `{}`: {}", - directory.display(), - io_error - ))] - Io { - directory: PathBuf, - io_error: io::Error, - }, #[snafu(display("No justfile found"))] NotFound, - #[snafu(display("Justfile path had no parent: {}", path.display()))] - JustfileHadNoParent { path: PathBuf }, } -impl Error for SearchError {} - #[cfg(test)] mod tests { use super::*; diff --git a/src/string_kind.rs b/src/string_kind.rs index 2b1cca2..04aee7e 100644 --- a/src/string_kind.rs +++ b/src/string_kind.rs @@ -55,11 +55,11 @@ impl StringKind { } } - pub(crate) fn unterminated_error_kind(self) -> CompilationErrorKind<'static> { + pub(crate) fn unterminated_error_kind(self) -> CompileErrorKind<'static> { match self.delimiter { StringDelimiter::QuoteDouble | StringDelimiter::QuoteSingle => - CompilationErrorKind::UnterminatedString, - StringDelimiter::Backtick => CompilationErrorKind::UnterminatedBacktick, + CompileErrorKind::UnterminatedString, + StringDelimiter::Backtick => CompileErrorKind::UnterminatedBacktick, } } @@ -74,9 +74,9 @@ impl StringKind { self.indented } - pub(crate) fn from_string_or_backtick(token: Token) -> CompilationResult { + pub(crate) fn from_string_or_backtick(token: Token) -> CompileResult { Self::from_token_start(token.lexeme()).ok_or_else(|| { - token.error(CompilationErrorKind::Internal { + token.error(CompileErrorKind::Internal { message: "StringKind::from_token: Expected String or Backtick".to_owned(), }) }) diff --git a/src/subcommand.rs b/src/subcommand.rs index ee5bd1c..f03556e 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -210,26 +210,18 @@ const BASH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[( )]; impl Subcommand { - pub(crate) fn completions(verbosity: Verbosity, shell: &str) -> Result<(), i32> { + pub(crate) fn completions(shell: &str) -> RunResult<'static, ()> { use clap::Shell; - fn replace( - verbosity: Verbosity, - haystack: &mut String, - needle: &str, - replacement: &str, - ) -> Result<(), i32> { + fn replace(haystack: &mut String, needle: &str, replacement: &str) -> RunResult<'static, ()> { if let Some(index) = haystack.find(needle) { haystack.replace_range(index..index + needle.len(), replacement); Ok(()) } else { - if verbosity.loud() { - eprintln!("Failed to find text:"); - eprintln!("{}", needle); - eprintln!("…in completion script:"); - eprintln!("{}", haystack); - } - Err(EXIT_FAILURE) + Err(Error::internal(format!( + "Failed to find text:\n{}\n…in completion script:\n{}", + needle, haystack + ))) } } @@ -246,19 +238,19 @@ impl Subcommand { match shell { Shell::Bash => for (needle, replacement) in BASH_COMPLETION_REPLACEMENTS { - replace(verbosity, &mut script, needle, replacement)?; + replace(&mut script, needle, replacement)?; }, Shell::Fish => { script.insert_str(0, FISH_RECIPE_COMPLETIONS); }, Shell::PowerShell => for (needle, replacement) in POWERSHELL_COMPLETION_REPLACEMENTS { - replace(verbosity, &mut script, needle, replacement)?; + replace(&mut script, needle, replacement)?; }, Shell::Zsh => for (needle, replacement) in ZSH_COMPLETION_REPLACEMENTS { - replace(verbosity, &mut script, needle, replacement)?; + replace(&mut script, needle, replacement)?; }, Shell::Elvish => {}, } diff --git a/src/testing.rs b/src/testing.rs index 14d8c30..a69dcc4 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -61,7 +61,7 @@ pub(crate) fn analysis_error( line: usize, column: usize, length: usize, - kind: CompilationErrorKind, + kind: CompileErrorKind, ) { let tokens = Lexer::lex(src).expect("Lexing failed in parse test..."); @@ -70,7 +70,7 @@ pub(crate) fn analysis_error( match Analyzer::analyze(ast) { Ok(_) => panic!("Analysis unexpectedly succeeded"), Err(have) => { - let want = CompilationError { + let want = CompileError { token: Token { kind: have.token.kind, src, diff --git a/src/thunk.rs b/src/thunk.rs index 5a50483..3a039ca 100644 --- a/src/thunk.rs +++ b/src/thunk.rs @@ -32,7 +32,7 @@ impl<'src> Thunk<'src> { pub(crate) fn resolve( name: Name<'src>, mut arguments: Vec>, - ) -> CompilationResult<'src, Thunk<'src>> { + ) -> CompileResult<'src, Thunk<'src>> { if let Some(function) = crate::function::TABLE.get(&name.lexeme()) { match (function, arguments.len()) { (Function::Nullary(function), 0) => Ok(Thunk::Nullary { @@ -63,16 +63,14 @@ impl<'src> Thunk<'src> { name, }) }, - _ => Err( - name.error(CompilationErrorKind::FunctionArgumentCountMismatch { - function: name.lexeme(), - found: arguments.len(), - expected: function.argc(), - }), - ), + _ => Err(name.error(CompileErrorKind::FunctionArgumentCountMismatch { + function: name.lexeme(), + found: arguments.len(), + expected: function.argc(), + })), } } else { - Err(name.error(CompilationErrorKind::UnknownFunction { + Err(name.error(CompileErrorKind::UnknownFunction { function: name.lexeme(), })) } diff --git a/src/token.rs b/src/token.rs index 47a7b3d..8905db8 100644 --- a/src/token.rs +++ b/src/token.rs @@ -15,11 +15,11 @@ impl<'src> Token<'src> { &self.src[self.offset..self.offset + self.length] } - pub(crate) fn error(&self, kind: CompilationErrorKind<'src>) -> CompilationError<'src> { - CompilationError { token: *self, kind } + pub(crate) fn error(&self, kind: CompileErrorKind<'src>) -> CompileError<'src> { + CompileError { token: *self, kind } } - pub(crate) fn write_context(&self, f: &mut Formatter, color: Color) -> fmt::Result { + pub(crate) fn write_context(&self, w: &mut dyn Write, color: Color) -> io::Result<()> { let width = if self.length == 0 { 1 } else { self.length }; let line_number = self.line.ordinal(); @@ -50,11 +50,11 @@ impl<'src> Token<'src> { i += c.len_utf8(); } let line_number_width = line_number.to_string().len(); - writeln!(f, "{0:1$} |", "", line_number_width)?; - writeln!(f, "{} | {}", line_number, space_line)?; - write!(f, "{0:1$} |", "", line_number_width)?; + writeln!(w, "{0:1$} |", "", line_number_width)?; + writeln!(w, "{} | {}", line_number, space_line)?; + write!(w, "{0:1$} |", "", line_number_width)?; write!( - f, + w, " {0:1$}{2}{3:^<4$}{5}", "", space_column, @@ -67,12 +67,13 @@ impl<'src> Token<'src> { None => if self.offset != self.src.len() { write!( - f, + w, "internal error: Error has invalid line number: {}", line_number )?; }, } + Ok(()) } } diff --git a/src/unresolved_recipe.rs b/src/unresolved_recipe.rs index 125eb02..f08b538 100644 --- a/src/unresolved_recipe.rs +++ b/src/unresolved_recipe.rs @@ -6,7 +6,7 @@ impl<'src> UnresolvedRecipe<'src> { pub(crate) fn resolve( self, resolved: Vec>>, - ) -> CompilationResult<'src, Recipe<'src>> { + ) -> CompileResult<'src, Recipe<'src>> { assert_eq!( self.dependencies.len(), resolved.len(), @@ -21,14 +21,16 @@ impl<'src> UnresolvedRecipe<'src> { .argument_range() .contains(&unresolved.arguments.len()) { - return Err(unresolved.recipe.error( - CompilationErrorKind::DependencyArgumentCountMismatch { - dependency: unresolved.recipe.lexeme(), - found: unresolved.arguments.len(), - min: resolved.min_arguments(), - max: resolved.max_arguments(), - }, - )); + return Err( + unresolved + .recipe + .error(CompileErrorKind::DependencyArgumentCountMismatch { + dependency: unresolved.recipe.lexeme(), + found: unresolved.arguments.len(), + min: resolved.min_arguments(), + max: resolved.max_arguments(), + }), + ); } } diff --git a/src/warning.rs b/src/warning.rs index da3c8b1..fbba65b 100644 --- a/src/warning.rs +++ b/src/warning.rs @@ -13,19 +13,17 @@ impl Warning { Self::DotenvLoad => None, } } -} -impl Display for Warning { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let warning = Color::fmt(f).warning(); - let message = Color::fmt(f).message(); + pub(crate) fn write(&self, w: &mut dyn Write, color: Color) -> io::Result<()> { + let warning = color.warning(); + let message = color.message(); - write!(f, "{} {}", warning.paint("warning:"), message.prefix())?; + write!(w, "{} {}", warning.paint("warning:"), message.prefix())?; match self { Self::DotenvLoad => { #[rustfmt::skip] - write!(f, "\ + write!(w, "\ A `.env` file was found and loaded, but this behavior will change in the future. To silence this warning and continue loading `.env` files, add: @@ -39,11 +37,10 @@ See https://github.com/casey/just/issues/469 for more details.")?; }, } - write!(f, "{}", message.suffix())?; + writeln!(w, "{}", message.suffix())?; if let Some(token) = self.context() { - writeln!(f)?; - token.write_context(f, Color::fmt(f).warning())?; + token.write_context(w, warning)?; } Ok(()) diff --git a/tests/choose.rs b/tests/choose.rs index 0d99087..45954ea 100644 --- a/tests/choose.rs +++ b/tests/choose.rs @@ -95,7 +95,7 @@ test! { ", args: ("--choose"), stdout: "", - stderr: "Justfile contains no choosable recipes.\n", + stderr: "error: Justfile contains no choosable recipes.\n", status: EXIT_FAILURE, } @@ -113,6 +113,61 @@ test! { stderr: "echo foo\necho bar\n", } +#[test] +fn invoke_error_function() { + Test::new() + .justfile( + " + foo: + echo foo + + bar: + echo bar + ", + ) + .stderr(if cfg!(windows) { + "error: Chooser `/ -cu fzf` invocation failed: Access is denied. (os error 5)\n" + } else { + "error: Chooser `/ -cu fzf` invocation failed: Permission denied (os error 13)\n" + }) + .status(EXIT_FAILURE) + .shell(false) + .args(&["--shell", "/", "--choose"]) + .run(); +} + +#[test] +#[cfg(not(windows))] +fn status_error() { + let tmp = temptree! { + justfile: "foo:\n echo foo\nbar:\n echo bar\n", + "exit-2": "#!/usr/bin/env bash\nexit 2\n", + }; + + cmd_unit!(%"chmod +x", tmp.path().join("exit-2")); + + let path = env::join_paths( + iter::once(tmp.path().to_owned()).chain(env::split_paths(&env::var_os("PATH").unwrap())), + ) + .unwrap(); + + let output = Command::new(executable_path("just")) + .current_dir(tmp.path()) + .arg("--choose") + .arg("--chooser") + .arg("exit-2") + .env("PATH", path) + .output() + .unwrap(); + + assert_eq!( + String::from_utf8_lossy(&output.stderr), + "error: Chooser `exit-2` failed: exit code: 2\n", + ); + + assert_eq!(output.status.code().unwrap(), 2); +} + #[test] fn default() { let tmp = temptree! { diff --git a/tests/command.rs b/tests/command.rs index f3caca9..13019e9 100644 --- a/tests/command.rs +++ b/tests/command.rs @@ -91,6 +91,7 @@ test! { echo XYZ ", args: ("--command", "false"), + stderr: "error: Command `false` failed: exit code: 1\n", status: EXIT_FAILURE, } diff --git a/tests/common.rs b/tests/common.rs index 96b0d17..f874084 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -2,21 +2,26 @@ pub(crate) use std::{ collections::BTreeMap, env::{self, consts::EXE_SUFFIX}, error::Error, + fmt::Debug, fs, io::Write, iter, - path::Path, + path::{Path, PathBuf}, process::{Command, Output, Stdio}, str, }; +pub(crate) use cradle::cmd_unit; pub(crate) use executable_path::executable_path; pub(crate) use just::unindent; pub(crate) use libc::{EXIT_FAILURE, EXIT_SUCCESS}; +pub(crate) use pretty_assertions::Comparison; +pub(crate) use regex::Regex; +pub(crate) use tempfile::TempDir; pub(crate) use temptree::temptree; pub(crate) use which::which; pub(crate) use yaml_rust::YamlLoader; pub(crate) use crate::{ - assert_stdout::assert_stdout, assert_success::assert_success, tempdir::tempdir, + assert_stdout::assert_stdout, assert_success::assert_success, tempdir::tempdir, test::Test, }; diff --git a/tests/edit.rs b/tests/edit.rs index 78e77d3..c85bcb6 100644 --- a/tests/edit.rs +++ b/tests/edit.rs @@ -26,6 +26,67 @@ fn invalid_justfile() { assert_stdout(&output, JUSTFILE); } +#[test] +fn invoke_error() { + let tmp = temptree! { + justfile: JUSTFILE, + }; + + let output = Command::new(executable_path("just")) + .current_dir(tmp.path()) + .output() + .unwrap(); + + assert!(!output.status.success()); + + let output = Command::new(executable_path("just")) + .current_dir(tmp.path()) + .arg("--edit") + .env("VISUAL", "/") + .output() + .unwrap(); + + assert_eq!( + String::from_utf8_lossy(&output.stderr), + if cfg!(windows) { + "error: Editor `/` invocation failed: Access is denied. (os error 5)\n" + } else { + "error: Editor `/` invocation failed: Permission denied (os error 13)\n" + } + ); +} + +#[test] +#[cfg(not(windows))] +fn status_error() { + let tmp = temptree! { + justfile: JUSTFILE, + "exit-2": "#!/usr/bin/env bash\nexit 2\n", + }; + + cmd_unit!(%"chmod +x", tmp.path().join("exit-2")); + + let path = env::join_paths( + iter::once(tmp.path().to_owned()).chain(env::split_paths(&env::var_os("PATH").unwrap())), + ) + .unwrap(); + + let output = Command::new(executable_path("just")) + .current_dir(tmp.path()) + .arg("--edit") + .env("PATH", path) + .env("VISUAL", "exit-2") + .output() + .unwrap(); + + assert_eq!( + String::from_utf8_lossy(&output.stderr), + "error: Editor `exit-2` failed: exit code: 2\n" + ); + + assert_eq!(output.status.code().unwrap(), 2); +} + /// Test that editor is $VISUAL, $EDITOR, or "vim" in that order #[test] fn editor_precedence() { diff --git a/tests/fmt.rs b/tests/fmt.rs index 392bca2..779f381 100644 --- a/tests/fmt.rs +++ b/tests/fmt.rs @@ -5,7 +5,8 @@ test! { justfile: "", args: ("--fmt"), stderr: " - The `--fmt` command is currently unstable. Pass the `--unstable` flag to enable it. + error: The `--fmt` command is currently unstable. \ + Invoke `just` with the `--unstable` flag to enable unstable features. ", status: EXIT_FAILURE, } @@ -34,6 +35,34 @@ fn unstable_passed() { assert_eq!(fs::read_to_string(&justfile).unwrap(), "x := 'hello'\n",); } +#[test] +fn write_error() { + let tempdir = temptree! { + justfile: "x := 'hello' ", + }; + + let test = Test::with_tempdir(tempdir) + .no_justfile() + .args(&["--fmt", "--unstable"]) + .status(EXIT_FAILURE) + .stderr_regex(if cfg!(windows) { + r"error: Failed to write justfile to `.*`: Access is denied. \(os error 5\)\n" + } else { + r"error: Failed to write justfile to `.*`: Permission denied \(os error 13\)\n" + }); + + let justfile_path = test.justfile_path(); + + cmd_unit!(%"chmod 400", &justfile_path); + + let _tempdir = test.run(); + + assert_eq!( + fs::read_to_string(&justfile_path).unwrap(), + "x := 'hello' " + ); +} + test! { name: alias_good, justfile: " diff --git a/tests/init.rs b/tests/init.rs index efcbc6a..0fc8ce4 100644 --- a/tests/init.rs +++ b/tests/init.rs @@ -22,23 +22,38 @@ fn current_dir() { #[test] fn exists() { - let tmp = tempdir(); - - let output = Command::new(executable_path("just")) - .current_dir(tmp.path()) + let tempdir = Test::new() + .no_justfile() .arg("--init") - .output() - .unwrap(); + .stderr_regex("Wrote justfile to `.*`\n") + .run(); - assert!(output.status.success()); - - let output = Command::new(executable_path("just")) - .current_dir(tmp.path()) + Test::with_tempdir(tempdir) + .no_justfile() .arg("--init") - .output() - .unwrap(); + .status(EXIT_FAILURE) + .stderr_regex("error: Justfile `.*` already exists\n") + .run(); +} - assert!(!output.status.success()); +#[test] +fn write_error() { + let test = Test::new(); + + let justfile_path = test.justfile_path(); + + fs::create_dir(&justfile_path).unwrap(); + + test + .no_justfile() + .args(&["--init"]) + .status(EXIT_FAILURE) + .stderr_regex(if cfg!(windows) { + r"error: Failed to write justfile to `.*`: Access is denied. \(os error 5\)\n" + } else { + r"error: Failed to write justfile to `.*`: Is a directory \(os error 21\)\n" + }) + .run(); } #[test] @@ -47,18 +62,17 @@ fn invocation_directory() { ".git": {}, }; - let output = Command::new(executable_path("just")) - .current_dir(tmp.path()) + let test = Test::with_tempdir(tmp); + + let justfile_path = test.justfile_path(); + + let _tmp = test + .no_justfile() + .stderr_regex("Wrote justfile to `.*`\n") .arg("--init") - .output() - .unwrap(); + .run(); - assert!(output.status.success()); - - assert_eq!( - fs::read_to_string(tmp.path().join("justfile")).unwrap(), - EXPECTED - ); + assert_eq!(fs::read_to_string(justfile_path).unwrap(), EXPECTED); } #[test] diff --git a/tests/lib.rs b/tests/lib.rs index 46aacfb..fe77500 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -27,6 +27,7 @@ mod readme; mod search; mod shebang; mod shell; +mod show; mod string; mod sublime_syntax; mod subsequents; diff --git a/tests/misc.rs b/tests/misc.rs index cc4b75d..1a47880 100644 --- a/tests/misc.rs +++ b/tests/misc.rs @@ -123,30 +123,6 @@ test! { status: EXIT_FAILURE, } -test! { - name: alias_show, - justfile: "foo:\n bar\nalias f := foo", - args: ("--show", "f"), - stdout: " - alias f := foo - foo: - bar - ", -} - -test! { - name: alias_show_missing_target, - justfile: "alias f := foo", - args: ("--show", "f"), - stderr: " - error: Alias `f` has an unknown target `foo` - | - 1 | alias f := foo - | ^ - ", - status: EXIT_FAILURE, -} - test! { name: default, justfile: "default:\n echo hello\nother: \n echo bar", @@ -256,19 +232,6 @@ c: stderr: "echo d\necho c\n", } -test! { - name: show, - justfile: r#"hello := "foo" -bar := hello + hello -recipe: - echo {{hello + "bar" + bar}}"#, - args: ("--show", "recipe"), - stdout: r#" - recipe: - echo {{ hello + "bar" + bar }} - "#, -} - test! { name: status_passthrough, justfile: " @@ -700,8 +663,8 @@ test! { justfile: "b := a\na := `exit 100`\nbar:\n echo '{{`exit 200`}}'", args: ("--color", "always"), stdout: "", - stderr: "\u{1b}[1;31merror\u{1b}[0m: \u{1b}[1mBacktick failed with exit code 100 -\u{1b}[0m |\n2 | a := `exit 100`\n | \u{1b}[1;31m^^^^^^^^^^\u{1b}[0m\n", + stderr: "\u{1b}[1;31merror\u{1b}[0m: \u{1b}[1mBacktick failed with exit code 100\u{1b}[0m + |\n2 | a := `exit 100`\n | \u{1b}[1;31m^^^^^^^^^^\u{1b}[0m\n", status: 100, } @@ -1021,66 +984,6 @@ b: "#, } -test! { - name: show_suggestion, - justfile: r#" -hello a b='B ' c='C': - echo {{a}} {{b}} {{c}} - -a Z="\t z": -"#, - args: ("--show", "hell"), - stdout: "", - stderr: "Justfile does not contain recipe `hell`.\nDid you mean `hello`?\n", - status: EXIT_FAILURE, -} - -test! { - name: show_alias_suggestion, - justfile: r#" -hello a b='B ' c='C': - echo {{a}} {{b}} {{c}} - -alias foo := hello - -a Z="\t z": -"#, - args: ("--show", "fo"), - stdout: "", - stderr: "Justfile does not contain recipe `fo`.\nDid you mean `foo`, an alias for `hello`?\n", - status: EXIT_FAILURE, -} - -test! { - name: show_no_suggestion, - justfile: r#" -helloooooo a b='B ' c='C': - echo {{a}} {{b}} {{c}} - -a Z="\t z": -"#, - args: ("--show", "hell"), - stdout: "", - stderr: "Justfile does not contain recipe `hell`.\n", - status: EXIT_FAILURE, -} - -test! { - name: show_no_alias_suggestion, - justfile: r#" -hello a b='B ' c='C': - echo {{a}} {{b}} {{c}} - -alias foo := hello - -a Z="\t z": -"#, - args: ("--show", "fooooooo"), - stdout: "", - stderr: "Justfile does not contain recipe `fooooooo`.\n", - status: EXIT_FAILURE, -} - test! { name: run_suggestion, justfile: r#" diff --git a/tests/show.rs b/tests/show.rs new file mode 100644 index 0000000..309e53a --- /dev/null +++ b/tests/show.rs @@ -0,0 +1,101 @@ +use crate::common::*; + +test! { + name: show, + justfile: r#"hello := "foo" +bar := hello + hello +recipe: + echo {{hello + "bar" + bar}}"#, + args: ("--show", "recipe"), + stdout: r#" + recipe: + echo {{ hello + "bar" + bar }} + "#, +} + +test! { + name: alias_show, + justfile: "foo:\n bar\nalias f := foo", + args: ("--show", "f"), + stdout: " + alias f := foo + foo: + bar + ", +} + +test! { + name: alias_show_missing_target, + justfile: "alias f := foo", + args: ("--show", "f"), + stderr: " + error: Alias `f` has an unknown target `foo` + | + 1 | alias f := foo + | ^ + ", + status: EXIT_FAILURE, +} + +test! { + name: show_suggestion, + justfile: r#" +hello a b='B ' c='C': + echo {{a}} {{b}} {{c}} + +a Z="\t z": +"#, + args: ("--show", "hell"), + stdout: "", + stderr: "error: Justfile does not contain recipe `hell`.\nDid you mean `hello`?\n", + status: EXIT_FAILURE, +} + +test! { + name: show_alias_suggestion, + justfile: r#" +hello a b='B ' c='C': + echo {{a}} {{b}} {{c}} + +alias foo := hello + +a Z="\t z": +"#, + args: ("--show", "fo"), + stdout: "", + stderr: " + error: Justfile does not contain recipe `fo`. + Did you mean `foo`, an alias for `hello`? + ", + status: EXIT_FAILURE, +} + +test! { + name: show_no_suggestion, + justfile: r#" +helloooooo a b='B ' c='C': + echo {{a}} {{b}} {{c}} + +a Z="\t z": +"#, + args: ("--show", "hell"), + stdout: "", + stderr: "error: Justfile does not contain recipe `hell`.\n", + status: EXIT_FAILURE, +} + +test! { + name: show_no_alias_suggestion, + justfile: r#" +hello a b='B ' c='C': + echo {{a}} {{b}} {{c}} + +alias foo := hello + +a Z="\t z": +"#, + args: ("--show", "fooooooo"), + stdout: "", + stderr: "error: Justfile does not contain recipe `fooooooo`.\n", + status: EXIT_FAILURE, +} diff --git a/tests/test.rs b/tests/test.rs index 7d86de6..35cf9c8 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -1,13 +1,13 @@ use crate::common::*; +use pretty_assertions::assert_eq; + macro_rules! test { ( - name: $name:ident, - justfile: $justfile:expr, - $(args: ($($arg:tt)*),)? - $(env: { - $($env_key:literal : $env_value:literal,)* - },)? + name: $name:ident, + $(justfile: $justfile:expr,)? + $(args: ($($arg:tt),*),)? + $(env: { $($env_key:literal : $env_value:literal,)* },)? $(stdin: $stdin:expr,)? $(stdout: $stdout:expr,)? $(stderr: $stderr:expr,)? @@ -16,66 +16,128 @@ macro_rules! test { ) => { #[test] fn $name() { - #[allow(unused_mut)] - let mut env = std::collections::BTreeMap::new(); + let test = crate::test::Test::new(); - $($(env.insert($env_key.to_string(), $env_value.to_string());)*)? + $($(let test = test.arg($arg);)*)? + $($(let test = test.env($env_key, $env_value);)*)? + $(let test = test.justfile($justfile);)? + $(let test = test.shell($shell);)? + $(let test = test.status($status);)? + $(let test = test.stderr($stderr);)? + $(let test = test.stdin($stdin);)? + $(let test = test.stdout($stdout);)? - crate::test::Test { - justfile: $justfile, - $(args: &[$($arg)*],)? - $(stdin: $stdin,)? - $(stdout: $stdout,)? - $(stderr: $stderr,)? - $(status: $status,)? - $(shell: $shell,)? - env, - ..crate::test::Test::default() - }.run(); + test.run(); } } } -pub(crate) struct Test<'a> { - pub(crate) justfile: &'a str, - pub(crate) args: &'a [&'a str], - pub(crate) env: BTreeMap, - pub(crate) stdin: &'a str, - pub(crate) stdout: &'a str, - pub(crate) stderr: &'a str, - pub(crate) status: i32, - pub(crate) shell: bool, +pub(crate) struct Test { + pub(crate) tempdir: TempDir, + pub(crate) justfile: Option, + pub(crate) args: Vec, + pub(crate) env: BTreeMap, + pub(crate) stdin: String, + pub(crate) stdout: String, + pub(crate) stderr: String, + pub(crate) stderr_regex: Option, + pub(crate) status: i32, + pub(crate) shell: bool, } -impl<'a> Default for Test<'a> { - fn default() -> Test<'a> { - Test { - justfile: "", - args: &[], - env: BTreeMap::new(), - stdin: "", - stdout: "", - stderr: "", - status: EXIT_SUCCESS, - shell: true, +impl Test { + pub(crate) fn new() -> Self { + Self::with_tempdir(tempdir()) + } + + pub(crate) fn with_tempdir(tempdir: TempDir) -> Self { + Self { + args: Vec::new(), + env: BTreeMap::new(), + justfile: Some(String::new()), + stderr_regex: None, + shell: true, + status: EXIT_SUCCESS, + stderr: String::new(), + stdin: String::new(), + stdout: String::new(), + tempdir, } } + + pub(crate) fn arg(mut self, val: &str) -> Self { + self.args.push(val.to_owned()); + self + } + + pub(crate) fn args(mut self, args: &[&str]) -> Self { + for arg in args { + self = self.arg(arg); + } + self + } + + pub(crate) fn env(mut self, key: &str, val: &str) -> Self { + self.env.insert(key.to_string(), val.to_string()); + self + } + + pub(crate) fn justfile(mut self, justfile: impl Into) -> Self { + self.justfile = Some(justfile.into()); + self + } + + pub(crate) fn justfile_path(&self) -> PathBuf { + self.tempdir.path().join("justfile") + } + + pub(crate) fn no_justfile(mut self) -> Self { + self.justfile = None; + self + } + + pub(crate) fn shell(mut self, shell: bool) -> Self { + self.shell = shell; + self + } + + pub(crate) fn status(mut self, exit_status: i32) -> Self { + self.status = exit_status; + self + } + + pub(crate) fn stderr(mut self, stderr: impl Into) -> Self { + self.stderr = stderr.into(); + self + } + + pub(crate) fn stderr_regex(mut self, stderr_regex: impl AsRef) -> Self { + self.stderr_regex = Some(Regex::new(&format!("^{}$", stderr_regex.as_ref())).unwrap()); + self + } + + pub(crate) fn stdin(mut self, stdin: impl Into) -> Self { + self.stdin = stdin.into(); + self + } + + pub(crate) fn stdout(mut self, stdout: impl Into) -> Self { + self.stdout = stdout.into(); + self + } } -impl<'a> Test<'a> { - pub(crate) fn run(self) { - let tmp = tempdir(); +impl Test { + pub(crate) fn run(self) -> TempDir { + if let Some(justfile) = &self.justfile { + let justfile = unindent(justfile); + fs::write(self.justfile_path(), justfile).unwrap(); + } - let justfile = unindent(self.justfile); + let stdout = unindent(&self.stdout); + let stderr = unindent(&self.stderr); - let stdout = unindent(self.stdout); - let stderr = unindent(self.stderr); - - let mut justfile_path = tmp.path().to_path_buf(); - justfile_path.push("justfile"); - fs::write(&justfile_path, justfile).unwrap(); - - let mut dotenv_path = tmp.path().to_path_buf(); + let mut dotenv_path = self.tempdir.path().to_path_buf(); dotenv_path.push(".env"); fs::write(dotenv_path, "DOTENV_KEY=dotenv-value").unwrap(); @@ -87,8 +149,8 @@ impl<'a> Test<'a> { let mut child = command .args(self.args) - .envs(self.env) - .current_dir(tmp.path()) + .envs(&self.env) + .current_dir(self.tempdir.path()) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -107,23 +169,37 @@ impl<'a> Test<'a> { .wait_with_output() .expect("failed to wait for just process"); - let have = Output { - status: output.status.code().unwrap(), - stdout: str::from_utf8(&output.stdout).unwrap(), - stderr: str::from_utf8(&output.stderr).unwrap(), - }; + fn compare(name: &str, have: T, want: T) -> bool { + let equal = have == want; + if !equal { + eprintln!("Bad {}: {}", name, Comparison::new(&have, &want)); + } + equal + } - let want = Output { - status: self.status, - stdout: &stdout, - stderr: &stderr, - }; + let output_stderr = str::from_utf8(&output.stderr).unwrap(); - assert_eq!(have, want, "bad output"); + if let Some(ref stderr_regex) = self.stderr_regex { + if !stderr_regex.is_match(output_stderr) { + panic!( + "Stderr regex mismatch: {} !~= /{}/", + output_stderr, stderr_regex + ); + } + } + + if !compare("status", output.status.code().unwrap(), self.status) + | !compare("stdout", str::from_utf8(&output.stdout).unwrap(), &stdout) + | (self.stderr_regex.is_none() && !compare("stderr", output_stderr, &stderr)) + { + panic!("Output mismatch."); + } if self.status == EXIT_SUCCESS { - test_round_trip(tmp.path()); + test_round_trip(self.tempdir.path()); } + + self.tempdir } }