Add loader and refactor errors (#917)

This commit adds a `Loader` type, which can be used to load multiple
source strings. This was done to support the work on modules, but
coincidentally enabled consolidating errors, since now `Config::run`
can take a `&Loader`, and in the event of an error, return and `Error`
that borrows from loaded strings. Multiple error types have been
consolidated, and a bunch of ad-hoc error printing was removed.
This commit is contained in:
Casey Rodarmor 2021-07-26 01:26:06 -07:00 committed by GitHub
parent 98457c05d7
commit 1b0fafea75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1646 additions and 1288 deletions

24
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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<Recipe<'src>>>,
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()) {

View File

@ -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();

View File

@ -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

View File

@ -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<T, CompilationError<'a>>;
pub(crate) type CompileResult<'a, T> = Result<T, CompileError<'a>>;
pub(crate) type ConfigResult<T> = Result<T, ConfigError>;
pub(crate) type RunResult<'a, T> = Result<T, RuntimeError<'a>>;
pub(crate) type RunResult<'a, T> = Result<T, Error<'a>>;
pub(crate) type SearchResult<T> = Result<T, SearchError>;
// modules used in tests

View File

@ -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(),
)?;
},
UnexpectedClosingDelimiter { close } => {
writeln!(f, "Unexpected closing delimiter `{}`", close.close())?;
},
UnpairedCarriageReturn => {
writeln!(f, "Unpaired carriage return")?;
},
UnterminatedInterpolation => {
writeln!(f, "Unterminated interpolation")?;
},
UnterminatedString => {
writeln!(f, "Unterminated string")?;
},
UnterminatedBacktick => {
writeln!(f, "Unterminated backtick")?;
},
Internal { ref message } => {
writeln!(
MixedLeadingWhitespace { whitespace } => {
write!(
f,
"Internal error, this may indicate a bug in just: {}\n\
consider filing an issue: https://github.com/casey/just/issues/new",
message
"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 } => {
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 => {
write!(f, "Unpaired carriage return")?;
},
UnterminatedBacktick => {
write!(f, "Unterminated backtick")?;
},
UnterminatedInterpolation => {
write!(f, "Unterminated interpolation")?;
},
UnterminatedString => {
write!(f, "Unterminated string")?;
},
}
write!(f, "{}", message.suffix())?;
self.token.write_context(f, Color::fmt(f).error())
Ok(())
}
}

View File

@ -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<Keyword>,
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<TokenKind>,
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,
}

View File

@ -3,7 +3,7 @@ use crate::common::*;
pub(crate) struct Compiler;
impl Compiler {
pub(crate) fn compile(src: &str) -> CompilationResult<Justfile> {
pub(crate) fn compile(src: &str) -> CompileResult<Justfile> {
let tokens = Lexer::lex(src)?;
let ast = Parser::parse(&tokens)?;

View File

@ -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<String, String>,
chooser: Option<&str>,
) -> Result<(), i32> {
) -> Result<(), Error<'src>> {
let recipes = justfile
.public_recipes(self.unsorted)
.iter()
@ -607,10 +597,7 @@ impl Config {
.collect::<Vec<&Recipe<Dependency>>>();
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() {
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 {
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)
},
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<String, String>,
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),
})
}
}

View File

@ -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<String>,
},
#[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<String, String>,
},
#[snafu(display(
"`--{}` used with unexpected overrides: {}; and arguments: {}",
subcommand.to_lowercase(),
@ -36,15 +45,6 @@ pub(crate) enum ConfigError {
overrides: BTreeMap<String, String>,
arguments: Vec<String>,
},
#[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<String, String>,
},
}
impl ConfigError {
@ -54,5 +54,3 @@ impl ConfigError {
}
}
}
impl Error for ConfigError {}

View File

@ -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<Parameter<'src>>,
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<usize>,
code: i32,
},
CommandInvoke {
binary: OsString,
arguments: Vec<OsString>,
io_error: io::Error,
},
CommandStatus {
binary: OsString,
arguments: Vec<OsString>,
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<Suggestion<'src>>,
},
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<String>,
io_error: io::Error,
},
Signal {
recipe: &'src str,
line_number: Option<usize>,
signal: i32,
},
TmpdirIo {
recipe: &'src str,
io_error: io::Error,
},
Unknown {
recipe: &'src str,
line_number: Option<usize>,
},
UnknownOverrides {
overrides: Vec<String>,
},
UnknownRecipes {
recipes: Vec<String>,
suggestion: Option<Suggestion<'src>>,
},
Unstable {
message: String,
},
WriteJustfile {
justfile: PathBuf,
io_error: io::Error,
},
}
impl<'src> Error<'src> {
pub(crate) fn code(&self) -> Option<i32> {
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<Token<'src>> {
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<String>) -> 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<CompileError<'src>> for Error<'src> {
fn from(compile_error: CompileError<'src>) -> Self {
Self::Compile { compile_error }
}
}
impl<'src> From<ConfigError> for Error<'src> {
fn from(config_error: ConfigError) -> Self {
Self::Config { config_error }
}
}
impl<'src> From<dotenv::Error> for Error<'src> {
fn from(dotenv_error: dotenv::Error) -> Error<'src> {
Self::Dotenv { dotenv_error }
}
}
impl<'src> From<SearchError> 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::<Vec<String>>()
.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::<Vec<String>>()
.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(())
}
}

View File

@ -1,22 +0,0 @@
use crate::common::*;
pub(crate) trait ErrorResultExt<T> {
fn eprint(self, color: Color) -> Result<T, i32>;
}
impl<T, E: Error> ErrorResultExt<T> for Result<T, E> {
fn eprint(self, color: Color) -> Result<T, i32> {
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())
},
}
}
}

View File

@ -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(_),
},

View File

@ -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)
}
}

View File

@ -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(),
});
}

View File

@ -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<Dependency>> = 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<Suggestion> {
pub(crate) fn suggest_recipe(&self, input: &str) -> Option<Suggestion<'src>> {
let mut suggestions = self
.recipes
.keys()
@ -54,7 +54,7 @@ impl<'src> Justfile<'src> {
.next()
}
pub(crate) fn suggest_variable(&self, input: &str) -> Option<Suggestion> {
pub(crate) fn suggest_variable(&self, input: &str) -> Option<Suggestion<'src>> {
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<String, String>,
arguments: &'run [String],
) -> RunResult<'run, ()> {
pub(crate) fn run(
&self,
config: &Config,
search: &Search,
overrides: &BTreeMap<String, String>,
arguments: &[String],
) -> RunResult<'src, ()> {
let unknown_overrides = overrides
.keys()
.filter(|name| !self.assignments.contains_key(name.as_str()))
.map(String::as_str)
.collect::<Vec<&str>>();
.cloned()
.collect::<Vec<String>>();
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<String, String>,
search: &'run Search,
search: &Search,
ran: &mut BTreeSet<Vec<String>>,
) -> 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<Dependency>> {
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,

View File

@ -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<Vec<Token>> {
pub(crate) fn lex(src: &'src str) -> CompileResult<Vec<Token<'src>>> {
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<String>) -> CompilationError<'src> {
fn internal_error(&self, message: impl Into<String>) -> 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<Token<'src>>> {
fn tokenize(mut self) -> CompileResult<'src, Vec<Token<'src>>> {
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"
);
}
}

View File

@ -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;

View File

@ -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
)
}
}

21
src/loader.rs Normal file
View File

@ -0,0 +1,21 @@
use crate::common::*;
pub(crate) struct Loader {
arena: Arena<String>,
}
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))
}
}

View File

@ -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)
}
}

View File

@ -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::<Vec<TokenKind>>(),
found: self.next()?.kind,
})
}
fn internal_error(
&self,
message: impl Into<String>,
) -> CompilationResult<'src, CompilationError<'src>> {
self.error(CompilationErrorKind::Internal {
fn internal_error(&self, message: impl Into<String>) -> 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<Token<'src>>> {
fn accept(&mut self, kind: TokenKind) -> CompileResult<'src, Option<Token<'src>>> {
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<F>(&self, forbidden: TokenKind, error: F) -> CompilationResult<'src, ()>
fn forbid<F>(&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<Name<'src>>> {
fn accept_name(&mut self) -> CompileResult<'src, Option<Name<'src>>> {
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<UnresolvedDependency<'src>>> {
fn accept_dependency(&mut self) -> CompileResult<'src, Option<UnresolvedDependency<'src>>> {
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<Item<'src>>,
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<Expression<'src>>> {
fn parse_sequence(&mut self) -> CompileResult<'src, Vec<Expression<'src>>> {
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<Line<'src>>> {
fn parse_body(&mut self) -> CompileResult<'src, Vec<Line<'src>>> {
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,

View File

@ -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<usize>,
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<usize>, 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 {
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,
});

View File

@ -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<Recipe<'src>>>> {
) -> CompileResult<'src, Table<'src, Rc<Recipe<'src>>>> {
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<Recipe<'src>>> {
) -> CompileResult<'src, Rc<Recipe<'src>>> {
if let Some(resolved) = self.resolved_recipes.get(recipe.name()) {
return Ok(Rc::clone(resolved));
}

View File

@ -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)
})
}

View File

@ -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<usize>,
code: i32,
},
CommandInvocation {
binary: OsString,
arguments: Vec<OsString>,
io_error: io::Error,
},
Cygpath {
recipe: &'src str,
output_error: OutputError,
},
Dotenv {
dotenv_error: dotenv::Error,
},
EvalUnknownVariable {
variable: String,
suggestion: Option<Suggestion<'src>>,
},
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<String>,
io_error: io::Error,
},
Signal {
recipe: &'src str,
line_number: Option<usize>,
signal: i32,
},
TmpdirIoError {
recipe: &'src str,
io_error: io::Error,
},
UnknownOverrides {
overrides: Vec<&'src str>,
},
UnknownRecipes {
recipes: Vec<&'src str>,
suggestion: Option<Suggestion<'src>>,
},
Unknown {
recipe: &'src str,
line_number: Option<usize>,
},
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<Token> {
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::<Vec<String>>()
.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<dotenv::Error> for RuntimeError<'src> {
fn from(dotenv_error: dotenv::Error) -> RuntimeError<'src> {
RuntimeError::Dotenv { dotenv_error }
}
}

View File

@ -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<PathBuf> },
#[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::*;

View File

@ -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<Self> {
pub(crate) fn from_string_or_backtick(token: Token) -> CompileResult<Self> {
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(),
})
})

View File

@ -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 => {},
}

View File

@ -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,

View File

@ -32,7 +32,7 @@ impl<'src> Thunk<'src> {
pub(crate) fn resolve(
name: Name<'src>,
mut arguments: Vec<Expression<'src>>,
) -> 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 {
_ => 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(),
}))
}

View File

@ -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(())
}
}

View File

@ -6,7 +6,7 @@ impl<'src> UnresolvedRecipe<'src> {
pub(crate) fn resolve(
self,
resolved: Vec<Rc<Recipe<'src>>>,
) -> 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 {
return Err(
unresolved
.recipe
.error(CompileErrorKind::DependencyArgumentCountMismatch {
dependency: unresolved.recipe.lexeme(),
found: unresolved.arguments.len(),
min: resolved.min_arguments(),
max: resolved.max_arguments(),
},
));
}),
);
}
}

View File

@ -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(())

View File

@ -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! {

View File

@ -91,6 +91,7 @@ test! {
echo XYZ
",
args: ("--command", "false"),
stderr: "error: Command `false` failed: exit code: 1\n",
status: EXIT_FAILURE,
}

View File

@ -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,
};

View File

@ -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() {

View File

@ -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: "

View File

@ -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]

View File

@ -27,6 +27,7 @@ mod readme;
mod search;
mod shebang;
mod shell;
mod show;
mod string;
mod sublime_syntax;
mod subsequents;

View File

@ -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#"

101
tests/show.rs Normal file
View File

@ -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,
}

View File

@ -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,)*
},)?
$(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) struct Test {
pub(crate) tempdir: TempDir,
pub(crate) justfile: Option<String>,
pub(crate) args: Vec<String>,
pub(crate) env: BTreeMap<String, String>,
pub(crate) stdin: &'a str,
pub(crate) stdout: &'a str,
pub(crate) stderr: &'a str,
pub(crate) stdin: String,
pub(crate) stdout: String,
pub(crate) stderr: String,
pub(crate) stderr_regex: Option<Regex>,
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<String>) -> 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<String>) -> Self {
self.stderr = stderr.into();
self
}
pub(crate) fn stderr_regex(mut self, stderr_regex: impl AsRef<str>) -> Self {
self.stderr_regex = Some(Regex::new(&format!("^{}$", stderr_regex.as_ref())).unwrap());
self
}
pub(crate) fn stdin(mut self, stdin: impl Into<String>) -> Self {
self.stdin = stdin.into();
self
}
pub(crate) fn stdout(mut self, stdout: impl Into<String>) -> 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<T: PartialEq + Debug>(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
}
}