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:
parent
98457c05d7
commit
1b0fafea75
24
Cargo.lock
generated
24
Cargo.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
|
4
justfile
4
justfile
@ -40,7 +40,7 @@ build:
|
||||
fmt:
|
||||
cargo +nightly fmt --all
|
||||
|
||||
watch +COMMAND='test':
|
||||
watch +COMMAND='ltest':
|
||||
cargo watch --clear --exec "{{COMMAND}}"
|
||||
|
||||
man:
|
||||
@ -61,7 +61,7 @@ version := `sed -En 's/version[[:space:]]*=[[:space:]]*"([^"]+)"/\1/p' Cargo.tom
|
||||
changes:
|
||||
git log --pretty=format:%s >> CHANGELOG.md
|
||||
|
||||
check: clippy test forbid
|
||||
check: clippy fmt test forbid
|
||||
git diff --no-ext-diff --quiet --exit-code
|
||||
grep '^\[{{ version }}\]' CHANGELOG.md
|
||||
cargo +nightly generate-lockfile -Z minimal-versions
|
||||
|
@ -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()) {
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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(())
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
@ -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)?;
|
||||
|
214
src/config.rs
214
src/config.rs
@ -532,7 +532,7 @@ impl Config {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn run_subcommand(self) -> Result<(), i32> {
|
||||
pub(crate) fn run_subcommand<'src>(self, loader: &'src Loader) -> Result<(), Error<'src>> {
|
||||
use Subcommand::*;
|
||||
|
||||
if self.subcommand == Init {
|
||||
@ -540,34 +540,24 @@ impl Config {
|
||||
}
|
||||
|
||||
if let Completions { shell } = self.subcommand {
|
||||
return Subcommand::completions(self.verbosity, &shell);
|
||||
return Subcommand::completions(&shell);
|
||||
}
|
||||
|
||||
let search =
|
||||
Search::find(&self.search_config, &self.invocation_directory).eprint(self.color)?;
|
||||
let search = Search::find(&self.search_config, &self.invocation_directory)?;
|
||||
|
||||
if self.subcommand == Edit {
|
||||
return self.edit(&search);
|
||||
return Self::edit(&search);
|
||||
}
|
||||
|
||||
let src = fs::read_to_string(&search.justfile)
|
||||
.map_err(|io_error| LoadError {
|
||||
io_error,
|
||||
path: &search.justfile,
|
||||
})
|
||||
.eprint(self.color)?;
|
||||
let src = loader.load(&search.justfile)?;
|
||||
|
||||
let tokens = Lexer::lex(&src).eprint(self.color)?;
|
||||
let ast = Parser::parse(&tokens).eprint(self.color)?;
|
||||
let justfile = Analyzer::analyze(ast.clone()).eprint(self.color)?;
|
||||
let tokens = Lexer::lex(&src)?;
|
||||
let ast = Parser::parse(&tokens)?;
|
||||
let justfile = Analyzer::analyze(ast.clone())?;
|
||||
|
||||
if self.verbosity.loud() {
|
||||
for warning in &justfile.warnings {
|
||||
if self.color.stderr().active() {
|
||||
eprintln!("{:#}", warning);
|
||||
} else {
|
||||
eprintln!("{}", warning);
|
||||
}
|
||||
warning.write(&mut io::stderr(), self.color.stderr()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
@ -575,7 +565,7 @@ impl Config {
|
||||
Choose { overrides, chooser } =>
|
||||
self.choose(justfile, &search, overrides, chooser.as_deref())?,
|
||||
Command { overrides, .. } => self.run(justfile, &search, overrides, &[])?,
|
||||
Dump => Self::dump(ast)?,
|
||||
Dump => Self::dump(ast),
|
||||
Evaluate { overrides, .. } => self.run(justfile, &search, overrides, &[])?,
|
||||
Format => self.format(ast, &search)?,
|
||||
List => self.list(justfile),
|
||||
@ -583,7 +573,7 @@ impl Config {
|
||||
arguments,
|
||||
overrides,
|
||||
} => self.run(justfile, &search, overrides, arguments)?,
|
||||
Show { ref name } => self.show(&name, justfile)?,
|
||||
Show { ref name } => Self::show(&name, justfile)?,
|
||||
Summary => self.summary(justfile),
|
||||
Variables => Self::variables(justfile),
|
||||
Completions { .. } | Edit | Init => unreachable!(),
|
||||
@ -592,13 +582,13 @@ impl Config {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn choose(
|
||||
fn choose<'src>(
|
||||
&self,
|
||||
justfile: Justfile,
|
||||
justfile: Justfile<'src>,
|
||||
search: &Search,
|
||||
overrides: &BTreeMap<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),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {}
|
||||
|
623
src/error.rs
623
src/error.rs
@ -1,7 +1,624 @@
|
||||
use crate::common::*;
|
||||
|
||||
pub(crate) trait Error: Display {
|
||||
fn code(&self) -> i32 {
|
||||
EXIT_FAILURE
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Error<'src> {
|
||||
ArgumentCountMismatch {
|
||||
recipe: &'src str,
|
||||
parameters: Vec<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(())
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
@ -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(_),
|
||||
},
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
});
|
||||
}
|
||||
|
@ -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,
|
||||
|
98
src/lexer.rs
98
src/lexer.rs
@ -1,6 +1,6 @@
|
||||
use crate::common::*;
|
||||
|
||||
use CompilationErrorKind::*;
|
||||
use CompileErrorKind::*;
|
||||
use TokenKind::*;
|
||||
|
||||
/// Just language lexer
|
||||
@ -38,7 +38,7 @@ pub(crate) struct Lexer<'src> {
|
||||
|
||||
impl<'src> Lexer<'src> {
|
||||
/// Lex `text`
|
||||
pub(crate) fn lex(src: &str) -> CompilationResult<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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
21
src/loader.rs
Normal 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))
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
102
src/parser.rs
102
src/parser.rs
@ -36,7 +36,7 @@ pub(crate) struct Parser<'tokens, 'src> {
|
||||
|
||||
impl<'tokens, 'src> Parser<'tokens, 'src> {
|
||||
/// Parse `tokens` into an `Ast`
|
||||
pub(crate) fn parse(tokens: &'tokens [Token<'src>]) -> CompilationResult<'src, Ast<'src>> {
|
||||
pub(crate) fn parse(tokens: &'tokens [Token<'src>]) -> CompileResult<'src, Ast<'src>> {
|
||||
Self::new(tokens).parse_ast()
|
||||
}
|
||||
|
||||
@ -49,27 +49,21 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
||||
}
|
||||
}
|
||||
|
||||
fn error(
|
||||
&self,
|
||||
kind: CompilationErrorKind<'src>,
|
||||
) -> CompilationResult<'src, CompilationError<'src>> {
|
||||
fn error(&self, kind: CompileErrorKind<'src>) -> CompileResult<'src, CompileError<'src>> {
|
||||
Ok(self.next()?.error(kind))
|
||||
}
|
||||
|
||||
/// Construct an unexpected token error with the token returned by
|
||||
/// `Parser::next`
|
||||
fn unexpected_token(&self) -> CompilationResult<'src, CompilationError<'src>> {
|
||||
self.error(CompilationErrorKind::UnexpectedToken {
|
||||
fn unexpected_token(&self) -> CompileResult<'src, CompileError<'src>> {
|
||||
self.error(CompileErrorKind::UnexpectedToken {
|
||||
expected: self.expected.iter().cloned().collect::<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,
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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));
|
||||
}
|
||||
|
19
src/run.rs
19
src/run.rs
@ -16,7 +16,22 @@ pub fn run() -> Result<(), i32> {
|
||||
info!("Parsing command line arguments…");
|
||||
let matches = app.get_matches();
|
||||
|
||||
let config = Config::from_matches(&matches).eprint(Color::auto())?;
|
||||
let loader = Loader::new();
|
||||
|
||||
config.run_subcommand()
|
||||
let mut color = Color::auto();
|
||||
let mut verbosity = Verbosity::default();
|
||||
|
||||
Config::from_matches(&matches)
|
||||
.map_err(Error::from)
|
||||
.and_then(|config| {
|
||||
color = config.color;
|
||||
verbosity = config.verbosity;
|
||||
config.run_subcommand(&loader)
|
||||
})
|
||||
.map_err(|error| {
|
||||
if !verbosity.quiet() {
|
||||
error.write(&mut io::stderr(), color).ok();
|
||||
}
|
||||
error.code().unwrap_or(EXIT_FAILURE)
|
||||
})
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
@ -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::*;
|
||||
|
@ -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(),
|
||||
})
|
||||
})
|
||||
|
@ -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 => {},
|
||||
}
|
||||
|
@ -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,
|
||||
|
10
src/thunk.rs
10
src/thunk.rs
@ -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(),
|
||||
}))
|
||||
}
|
||||
|
17
src/token.rs
17
src/token.rs
@ -15,11 +15,11 @@ impl<'src> Token<'src> {
|
||||
&self.src[self.offset..self.offset + self.length]
|
||||
}
|
||||
|
||||
pub(crate) fn error(&self, kind: CompilationErrorKind<'src>) -> CompilationError<'src> {
|
||||
CompilationError { token: *self, kind }
|
||||
pub(crate) fn error(&self, kind: CompileErrorKind<'src>) -> CompileError<'src> {
|
||||
CompileError { token: *self, kind }
|
||||
}
|
||||
|
||||
pub(crate) fn write_context(&self, f: &mut Formatter, color: Color) -> fmt::Result {
|
||||
pub(crate) fn write_context(&self, w: &mut dyn Write, color: Color) -> io::Result<()> {
|
||||
let width = if self.length == 0 { 1 } else { self.length };
|
||||
|
||||
let line_number = self.line.ordinal();
|
||||
@ -50,11 +50,11 @@ impl<'src> Token<'src> {
|
||||
i += c.len_utf8();
|
||||
}
|
||||
let line_number_width = line_number.to_string().len();
|
||||
writeln!(f, "{0:1$} |", "", line_number_width)?;
|
||||
writeln!(f, "{} | {}", line_number, space_line)?;
|
||||
write!(f, "{0:1$} |", "", line_number_width)?;
|
||||
writeln!(w, "{0:1$} |", "", line_number_width)?;
|
||||
writeln!(w, "{} | {}", line_number, space_line)?;
|
||||
write!(w, "{0:1$} |", "", line_number_width)?;
|
||||
write!(
|
||||
f,
|
||||
w,
|
||||
" {0:1$}{2}{3:^<4$}{5}",
|
||||
"",
|
||||
space_column,
|
||||
@ -67,12 +67,13 @@ impl<'src> Token<'src> {
|
||||
None =>
|
||||
if self.offset != self.src.len() {
|
||||
write!(
|
||||
f,
|
||||
w,
|
||||
"internal error: Error has invalid line number: {}",
|
||||
line_number
|
||||
)?;
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
},
|
||||
));
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(())
|
||||
|
@ -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! {
|
||||
|
@ -91,6 +91,7 @@ test! {
|
||||
echo XYZ
|
||||
",
|
||||
args: ("--command", "false"),
|
||||
stderr: "error: Command `false` failed: exit code: 1\n",
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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() {
|
||||
|
31
tests/fmt.rs
31
tests/fmt.rs
@ -5,7 +5,8 @@ test! {
|
||||
justfile: "",
|
||||
args: ("--fmt"),
|
||||
stderr: "
|
||||
The `--fmt` command is currently unstable. Pass the `--unstable` flag to enable it.
|
||||
error: The `--fmt` command is currently unstable. \
|
||||
Invoke `just` with the `--unstable` flag to enable unstable features.
|
||||
",
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
@ -34,6 +35,34 @@ fn unstable_passed() {
|
||||
assert_eq!(fs::read_to_string(&justfile).unwrap(), "x := 'hello'\n",);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_error() {
|
||||
let tempdir = temptree! {
|
||||
justfile: "x := 'hello' ",
|
||||
};
|
||||
|
||||
let test = Test::with_tempdir(tempdir)
|
||||
.no_justfile()
|
||||
.args(&["--fmt", "--unstable"])
|
||||
.status(EXIT_FAILURE)
|
||||
.stderr_regex(if cfg!(windows) {
|
||||
r"error: Failed to write justfile to `.*`: Access is denied. \(os error 5\)\n"
|
||||
} else {
|
||||
r"error: Failed to write justfile to `.*`: Permission denied \(os error 13\)\n"
|
||||
});
|
||||
|
||||
let justfile_path = test.justfile_path();
|
||||
|
||||
cmd_unit!(%"chmod 400", &justfile_path);
|
||||
|
||||
let _tempdir = test.run();
|
||||
|
||||
assert_eq!(
|
||||
fs::read_to_string(&justfile_path).unwrap(),
|
||||
"x := 'hello' "
|
||||
);
|
||||
}
|
||||
|
||||
test! {
|
||||
name: alias_good,
|
||||
justfile: "
|
||||
|
@ -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]
|
||||
|
@ -27,6 +27,7 @@ mod readme;
|
||||
mod search;
|
||||
mod shebang;
|
||||
mod shell;
|
||||
mod show;
|
||||
mod string;
|
||||
mod sublime_syntax;
|
||||
mod subsequents;
|
||||
|
101
tests/misc.rs
101
tests/misc.rs
@ -123,30 +123,6 @@ test! {
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
test! {
|
||||
name: alias_show,
|
||||
justfile: "foo:\n bar\nalias f := foo",
|
||||
args: ("--show", "f"),
|
||||
stdout: "
|
||||
alias f := foo
|
||||
foo:
|
||||
bar
|
||||
",
|
||||
}
|
||||
|
||||
test! {
|
||||
name: alias_show_missing_target,
|
||||
justfile: "alias f := foo",
|
||||
args: ("--show", "f"),
|
||||
stderr: "
|
||||
error: Alias `f` has an unknown target `foo`
|
||||
|
|
||||
1 | alias f := foo
|
||||
| ^
|
||||
",
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
test! {
|
||||
name: default,
|
||||
justfile: "default:\n echo hello\nother: \n echo bar",
|
||||
@ -256,19 +232,6 @@ c:
|
||||
stderr: "echo d\necho c\n",
|
||||
}
|
||||
|
||||
test! {
|
||||
name: show,
|
||||
justfile: r#"hello := "foo"
|
||||
bar := hello + hello
|
||||
recipe:
|
||||
echo {{hello + "bar" + bar}}"#,
|
||||
args: ("--show", "recipe"),
|
||||
stdout: r#"
|
||||
recipe:
|
||||
echo {{ hello + "bar" + bar }}
|
||||
"#,
|
||||
}
|
||||
|
||||
test! {
|
||||
name: status_passthrough,
|
||||
justfile: "
|
||||
@ -700,8 +663,8 @@ test! {
|
||||
justfile: "b := a\na := `exit 100`\nbar:\n echo '{{`exit 200`}}'",
|
||||
args: ("--color", "always"),
|
||||
stdout: "",
|
||||
stderr: "\u{1b}[1;31merror\u{1b}[0m: \u{1b}[1mBacktick failed with exit code 100
|
||||
\u{1b}[0m |\n2 | a := `exit 100`\n | \u{1b}[1;31m^^^^^^^^^^\u{1b}[0m\n",
|
||||
stderr: "\u{1b}[1;31merror\u{1b}[0m: \u{1b}[1mBacktick failed with exit code 100\u{1b}[0m
|
||||
|\n2 | a := `exit 100`\n | \u{1b}[1;31m^^^^^^^^^^\u{1b}[0m\n",
|
||||
status: 100,
|
||||
}
|
||||
|
||||
@ -1021,66 +984,6 @@ b:
|
||||
"#,
|
||||
}
|
||||
|
||||
test! {
|
||||
name: show_suggestion,
|
||||
justfile: r#"
|
||||
hello a b='B ' c='C':
|
||||
echo {{a}} {{b}} {{c}}
|
||||
|
||||
a Z="\t z":
|
||||
"#,
|
||||
args: ("--show", "hell"),
|
||||
stdout: "",
|
||||
stderr: "Justfile does not contain recipe `hell`.\nDid you mean `hello`?\n",
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
test! {
|
||||
name: show_alias_suggestion,
|
||||
justfile: r#"
|
||||
hello a b='B ' c='C':
|
||||
echo {{a}} {{b}} {{c}}
|
||||
|
||||
alias foo := hello
|
||||
|
||||
a Z="\t z":
|
||||
"#,
|
||||
args: ("--show", "fo"),
|
||||
stdout: "",
|
||||
stderr: "Justfile does not contain recipe `fo`.\nDid you mean `foo`, an alias for `hello`?\n",
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
test! {
|
||||
name: show_no_suggestion,
|
||||
justfile: r#"
|
||||
helloooooo a b='B ' c='C':
|
||||
echo {{a}} {{b}} {{c}}
|
||||
|
||||
a Z="\t z":
|
||||
"#,
|
||||
args: ("--show", "hell"),
|
||||
stdout: "",
|
||||
stderr: "Justfile does not contain recipe `hell`.\n",
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
test! {
|
||||
name: show_no_alias_suggestion,
|
||||
justfile: r#"
|
||||
hello a b='B ' c='C':
|
||||
echo {{a}} {{b}} {{c}}
|
||||
|
||||
alias foo := hello
|
||||
|
||||
a Z="\t z":
|
||||
"#,
|
||||
args: ("--show", "fooooooo"),
|
||||
stdout: "",
|
||||
stderr: "Justfile does not contain recipe `fooooooo`.\n",
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
test! {
|
||||
name: run_suggestion,
|
||||
justfile: r#"
|
||||
|
101
tests/show.rs
Normal file
101
tests/show.rs
Normal 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,
|
||||
}
|
200
tests/test.rs
200
tests/test.rs
@ -1,13 +1,13 @@
|
||||
use crate::common::*;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
macro_rules! test {
|
||||
(
|
||||
name: $name:ident,
|
||||
justfile: $justfile:expr,
|
||||
$(args: ($($arg:tt)*),)?
|
||||
$(env: {
|
||||
$($env_key:literal : $env_value:literal,)*
|
||||
},)?
|
||||
$(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
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user