Improve argument parsing and error handling for submodules (#2154)
This commit is contained in:
parent
e1b17fe9cf
commit
18ec9796b9
403
src/argument_parser.rs
Normal file
403
src/argument_parser.rs
Normal file
@ -0,0 +1,403 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[allow(clippy::doc_markdown)]
|
||||||
|
/// The argument parser is responsible for grouping positional arguments into
|
||||||
|
/// argument groups, which consist of a path to a recipe and its arguments.
|
||||||
|
///
|
||||||
|
/// Argument parsing is substantially complicated by the fact that recipe paths
|
||||||
|
/// can be given on the command line as multiple arguments, i.e., "foo" "bar"
|
||||||
|
/// baz", or as a single "::"-separated argument.
|
||||||
|
///
|
||||||
|
/// Error messages produced by the argument parser should use the format of the
|
||||||
|
/// recipe path as passed on the command line.
|
||||||
|
///
|
||||||
|
/// Additionally, if a recipe is specified with a "::"-separated path, extra
|
||||||
|
/// components of that path after a valid recipe must not be used as arguments,
|
||||||
|
/// whereas arguments after multiple argument path may be used as arguments. As
|
||||||
|
/// an example, `foo bar baz` may refer to recipe `foo::bar` with argument
|
||||||
|
/// `baz`, but `foo::bar::baz` is an error, since `bar` is a recipe, not a
|
||||||
|
/// module.
|
||||||
|
pub(crate) struct ArgumentParser<'src: 'run, 'run> {
|
||||||
|
arguments: &'run [&'run str],
|
||||||
|
next: usize,
|
||||||
|
root: &'run Justfile<'src>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub(crate) struct ArgumentGroup<'run> {
|
||||||
|
pub(crate) arguments: Vec<&'run str>,
|
||||||
|
pub(crate) path: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'src: 'run, 'run> ArgumentParser<'src, 'run> {
|
||||||
|
pub(crate) fn parse_arguments(
|
||||||
|
root: &'run Justfile<'src>,
|
||||||
|
arguments: &'run [&'run str],
|
||||||
|
) -> RunResult<'src, Vec<ArgumentGroup<'run>>> {
|
||||||
|
let mut groups = Vec::new();
|
||||||
|
|
||||||
|
let mut invocation_parser = Self {
|
||||||
|
arguments,
|
||||||
|
next: 0,
|
||||||
|
root,
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
groups.push(invocation_parser.parse_group()?);
|
||||||
|
|
||||||
|
if invocation_parser.next == arguments.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_group(&mut self) -> RunResult<'src, ArgumentGroup<'run>> {
|
||||||
|
let (recipe, path) = if let Some(next) = self.next() {
|
||||||
|
if next.contains(':') {
|
||||||
|
let module_path =
|
||||||
|
ModulePath::try_from([next].as_slice()).map_err(|()| Error::UnknownRecipe {
|
||||||
|
recipe: next.into(),
|
||||||
|
suggestion: None,
|
||||||
|
})?;
|
||||||
|
let (recipe, path, _) = self.resolve_recipe(true, &module_path.path)?;
|
||||||
|
self.next += 1;
|
||||||
|
(recipe, path)
|
||||||
|
} else {
|
||||||
|
let (recipe, path, consumed) = self.resolve_recipe(false, self.rest())?;
|
||||||
|
self.next += consumed;
|
||||||
|
(recipe, path)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let (recipe, path, consumed) = self.resolve_recipe(false, self.rest())?;
|
||||||
|
assert_eq!(consumed, 0);
|
||||||
|
(recipe, path)
|
||||||
|
};
|
||||||
|
|
||||||
|
let rest = self.rest();
|
||||||
|
|
||||||
|
let argument_range = recipe.argument_range();
|
||||||
|
let argument_count = cmp::min(rest.len(), recipe.max_arguments());
|
||||||
|
if !argument_range.range_contains(&argument_count) {
|
||||||
|
return Err(Error::ArgumentCountMismatch {
|
||||||
|
recipe: recipe.name(),
|
||||||
|
parameters: recipe.parameters.clone(),
|
||||||
|
found: rest.len(),
|
||||||
|
min: recipe.min_arguments(),
|
||||||
|
max: recipe.max_arguments(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let arguments = rest[..argument_count].to_vec();
|
||||||
|
|
||||||
|
self.next += argument_count;
|
||||||
|
|
||||||
|
Ok(ArgumentGroup { arguments, path })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_recipe(
|
||||||
|
&self,
|
||||||
|
module_path: bool,
|
||||||
|
args: &[impl AsRef<str>],
|
||||||
|
) -> RunResult<'src, (&'run Recipe<'src>, Vec<String>, usize)> {
|
||||||
|
let mut current = self.root;
|
||||||
|
let mut path = Vec::new();
|
||||||
|
|
||||||
|
for (i, arg) in args.iter().enumerate() {
|
||||||
|
let arg = arg.as_ref();
|
||||||
|
|
||||||
|
path.push(arg.to_string());
|
||||||
|
|
||||||
|
if let Some(module) = current.modules.get(arg) {
|
||||||
|
current = module;
|
||||||
|
} else if let Some(recipe) = current.get_recipe(arg) {
|
||||||
|
if module_path && i + 1 < args.len() {
|
||||||
|
return Err(Error::ExpectedSubmoduleButFoundRecipe {
|
||||||
|
path: if module_path {
|
||||||
|
path.join("::")
|
||||||
|
} else {
|
||||||
|
path.join(" ")
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Ok((recipe, path, i + 1));
|
||||||
|
} else {
|
||||||
|
if module_path && i + 1 < args.len() {
|
||||||
|
return Err(Error::UnknownSubmodule {
|
||||||
|
path: path.join("::"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(Error::UnknownRecipe {
|
||||||
|
recipe: if module_path {
|
||||||
|
path.join("::")
|
||||||
|
} else {
|
||||||
|
path.join(" ")
|
||||||
|
},
|
||||||
|
suggestion: current.suggest_recipe(arg),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(recipe) = ¤t.default {
|
||||||
|
recipe.check_can_be_default_recipe()?;
|
||||||
|
path.push(recipe.name().into());
|
||||||
|
Ok((recipe, path, args.len()))
|
||||||
|
} else if current.recipes.is_empty() {
|
||||||
|
Err(Error::NoRecipes)
|
||||||
|
} else {
|
||||||
|
Err(Error::NoDefaultRecipe)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next(&self) -> Option<&'run str> {
|
||||||
|
self.arguments.get(self.next).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rest(&self) -> &[&'run str] {
|
||||||
|
&self.arguments[self.next..]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use {super::*, tempfile::TempDir};
|
||||||
|
|
||||||
|
trait TempDirExt {
|
||||||
|
fn write(&self, path: &str, content: &str);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TempDirExt for TempDir {
|
||||||
|
fn write(&self, path: &str, content: &str) {
|
||||||
|
let path = self.path().join(path);
|
||||||
|
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||||
|
fs::write(path, content).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_no_arguments() {
|
||||||
|
let justfile = testing::compile("foo:");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
ArgumentParser::parse_arguments(&justfile, &["foo"]).unwrap(),
|
||||||
|
vec![ArgumentGroup {
|
||||||
|
path: vec!["foo".into()],
|
||||||
|
arguments: Vec::new()
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_with_argument() {
|
||||||
|
let justfile = testing::compile("foo bar:");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
ArgumentParser::parse_arguments(&justfile, &["foo", "baz"]).unwrap(),
|
||||||
|
vec![ArgumentGroup {
|
||||||
|
path: vec!["foo".into()],
|
||||||
|
arguments: vec!["baz"],
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_argument_count_mismatch() {
|
||||||
|
let justfile = testing::compile("foo bar:");
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
ArgumentParser::parse_arguments(&justfile, &["foo"]).unwrap_err(),
|
||||||
|
Error::ArgumentCountMismatch {
|
||||||
|
recipe: "foo",
|
||||||
|
found: 0,
|
||||||
|
min: 1,
|
||||||
|
max: 1,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_unknown() {
|
||||||
|
let justfile = testing::compile("foo:");
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
ArgumentParser::parse_arguments(&justfile, &["bar"]).unwrap_err(),
|
||||||
|
Error::UnknownRecipe {
|
||||||
|
recipe,
|
||||||
|
suggestion: None
|
||||||
|
} if recipe == "bar",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiple_unknown() {
|
||||||
|
let justfile = testing::compile("foo:");
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
ArgumentParser::parse_arguments(&justfile, &["bar", "baz"]).unwrap_err(),
|
||||||
|
Error::UnknownRecipe {
|
||||||
|
recipe,
|
||||||
|
suggestion: None
|
||||||
|
} if recipe == "bar",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recipe_in_submodule() {
|
||||||
|
let loader = Loader::new();
|
||||||
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
|
let path = tempdir.path().join("justfile");
|
||||||
|
fs::write(&path, "mod foo").unwrap();
|
||||||
|
fs::create_dir(tempdir.path().join("foo")).unwrap();
|
||||||
|
fs::write(tempdir.path().join("foo/mod.just"), "bar:").unwrap();
|
||||||
|
let compilation = Compiler::compile(true, &loader, &path).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
ArgumentParser::parse_arguments(&compilation.justfile, &["foo", "bar"]).unwrap(),
|
||||||
|
vec![ArgumentGroup {
|
||||||
|
path: vec!["foo".into(), "bar".into()],
|
||||||
|
arguments: Vec::new()
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recipe_in_submodule_unknown() {
|
||||||
|
let loader = Loader::new();
|
||||||
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
|
let path = tempdir.path().join("justfile");
|
||||||
|
fs::write(&path, "mod foo").unwrap();
|
||||||
|
fs::create_dir(tempdir.path().join("foo")).unwrap();
|
||||||
|
fs::write(tempdir.path().join("foo/mod.just"), "bar:").unwrap();
|
||||||
|
let compilation = Compiler::compile(true, &loader, &path).unwrap();
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
ArgumentParser::parse_arguments(&compilation.justfile, &["foo", "zzz"]).unwrap_err(),
|
||||||
|
Error::UnknownRecipe {
|
||||||
|
recipe,
|
||||||
|
suggestion: None
|
||||||
|
} if recipe == "foo zzz",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recipe_in_submodule_path_unknown() {
|
||||||
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
|
tempdir.write("justfile", "mod foo");
|
||||||
|
tempdir.write("foo.just", "bar:");
|
||||||
|
|
||||||
|
let loader = Loader::new();
|
||||||
|
let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap();
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
ArgumentParser::parse_arguments(&compilation.justfile, &["foo::zzz"]).unwrap_err(),
|
||||||
|
Error::UnknownRecipe {
|
||||||
|
recipe,
|
||||||
|
suggestion: None
|
||||||
|
} if recipe == "foo::zzz",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn module_path_not_consumed() {
|
||||||
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
|
tempdir.write("justfile", "mod foo");
|
||||||
|
tempdir.write("foo.just", "bar:");
|
||||||
|
|
||||||
|
let loader = Loader::new();
|
||||||
|
let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap();
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
ArgumentParser::parse_arguments(&compilation.justfile, &["foo::bar::baz"]).unwrap_err(),
|
||||||
|
Error::ExpectedSubmoduleButFoundRecipe {
|
||||||
|
path,
|
||||||
|
} if path == "foo::bar",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_recipes() {
|
||||||
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
|
tempdir.write("justfile", "");
|
||||||
|
|
||||||
|
let loader = Loader::new();
|
||||||
|
let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap();
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
ArgumentParser::parse_arguments(&compilation.justfile, &[]).unwrap_err(),
|
||||||
|
Error::NoRecipes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_recipe_requires_arguments() {
|
||||||
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
|
tempdir.write("justfile", "foo bar:");
|
||||||
|
|
||||||
|
let loader = Loader::new();
|
||||||
|
let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap();
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
ArgumentParser::parse_arguments(&compilation.justfile, &[]).unwrap_err(),
|
||||||
|
Error::DefaultRecipeRequiresArguments {
|
||||||
|
recipe: "foo",
|
||||||
|
min_arguments: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_default_recipe() {
|
||||||
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
|
tempdir.write("justfile", "import 'foo.just'");
|
||||||
|
tempdir.write("foo.just", "bar:");
|
||||||
|
|
||||||
|
let loader = Loader::new();
|
||||||
|
let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap();
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
ArgumentParser::parse_arguments(&compilation.justfile, &[]).unwrap_err(),
|
||||||
|
Error::NoDefaultRecipe,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn complex_grouping() {
|
||||||
|
let justfile = testing::compile(
|
||||||
|
"
|
||||||
|
FOO A B='blarg':
|
||||||
|
echo foo: {{A}} {{B}}
|
||||||
|
|
||||||
|
BAR X:
|
||||||
|
echo bar: {{X}}
|
||||||
|
|
||||||
|
BAZ +Z:
|
||||||
|
echo baz: {{Z}}
|
||||||
|
",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
ArgumentParser::parse_arguments(
|
||||||
|
&justfile,
|
||||||
|
&["BAR", "0", "FOO", "1", "2", "BAZ", "3", "4", "5"]
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
vec![
|
||||||
|
ArgumentGroup {
|
||||||
|
path: vec!["BAR".into()],
|
||||||
|
arguments: vec!["0"],
|
||||||
|
},
|
||||||
|
ArgumentGroup {
|
||||||
|
path: vec!["FOO".into()],
|
||||||
|
arguments: vec!["1", "2"],
|
||||||
|
},
|
||||||
|
ArgumentGroup {
|
||||||
|
path: vec!["BAZ".into()],
|
||||||
|
arguments: vec!["3", "4", "5"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
18
src/error.rs
18
src/error.rs
@ -95,6 +95,9 @@ pub(crate) enum Error<'src> {
|
|||||||
variable: String,
|
variable: String,
|
||||||
suggestion: Option<Suggestion<'src>>,
|
suggestion: Option<Suggestion<'src>>,
|
||||||
},
|
},
|
||||||
|
ExpectedSubmoduleButFoundRecipe {
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
FormatCheckFoundDiff,
|
FormatCheckFoundDiff,
|
||||||
FunctionCall {
|
FunctionCall {
|
||||||
function: Name<'src>,
|
function: Name<'src>,
|
||||||
@ -162,13 +165,13 @@ pub(crate) enum Error<'src> {
|
|||||||
line_number: Option<usize>,
|
line_number: Option<usize>,
|
||||||
},
|
},
|
||||||
UnknownSubmodule {
|
UnknownSubmodule {
|
||||||
path: ModulePath,
|
path: String,
|
||||||
},
|
},
|
||||||
UnknownOverrides {
|
UnknownOverrides {
|
||||||
overrides: Vec<String>,
|
overrides: Vec<String>,
|
||||||
},
|
},
|
||||||
UnknownRecipes {
|
UnknownRecipe {
|
||||||
recipes: Vec<String>,
|
recipe: String,
|
||||||
suggestion: Option<Suggestion<'src>>,
|
suggestion: Option<Suggestion<'src>>,
|
||||||
},
|
},
|
||||||
Unstable {
|
Unstable {
|
||||||
@ -365,6 +368,9 @@ impl<'src> ColorDisplay for Error<'src> {
|
|||||||
write!(f, "\n{suggestion}")?;
|
write!(f, "\n{suggestion}")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ExpectedSubmoduleButFoundRecipe { path } => {
|
||||||
|
write!(f, "Expected submodule at `{path}` but found recipe.")?;
|
||||||
|
},
|
||||||
FormatCheckFoundDiff => {
|
FormatCheckFoundDiff => {
|
||||||
write!(f, "Formatted justfile differs from original.")?;
|
write!(f, "Formatted justfile differs from original.")?;
|
||||||
}
|
}
|
||||||
@ -447,10 +453,8 @@ impl<'src> ColorDisplay for Error<'src> {
|
|||||||
let overrides = List::and_ticked(overrides);
|
let overrides = List::and_ticked(overrides);
|
||||||
write!(f, "{count} {overrides} overridden on the command line but not present in justfile")?;
|
write!(f, "{count} {overrides} overridden on the command line but not present in justfile")?;
|
||||||
}
|
}
|
||||||
UnknownRecipes { recipes, suggestion } => {
|
UnknownRecipe { recipe, suggestion } => {
|
||||||
let count = Count("recipe", recipes.len());
|
write!(f, "Justfile does not contain recipe `{recipe}`.")?;
|
||||||
let recipes = List::or_ticked(recipes);
|
|
||||||
write!(f, "Justfile does not contain {count} {recipes}.")?;
|
|
||||||
if let Some(suggestion) = suggestion {
|
if let Some(suggestion) = suggestion {
|
||||||
write!(f, "\n{suggestion}")?;
|
write!(f, "\n{suggestion}")?;
|
||||||
}
|
}
|
||||||
|
208
src/justfile.rs
208
src/justfile.rs
@ -173,66 +173,26 @@ impl<'src> Justfile<'src> {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut remaining: Vec<&str> = if !arguments.is_empty() {
|
let arguments = arguments.iter().map(String::as_str).collect::<Vec<&str>>();
|
||||||
arguments.iter().map(String::as_str).collect()
|
|
||||||
} else if let Some(recipe) = &self.default {
|
let groups = ArgumentParser::parse_arguments(self, &arguments)?;
|
||||||
recipe.check_can_be_default_recipe()?;
|
|
||||||
vec![recipe.name()]
|
|
||||||
} else if self.recipes.is_empty() {
|
|
||||||
return Err(Error::NoRecipes);
|
|
||||||
} else {
|
|
||||||
return Err(Error::NoDefaultRecipe);
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut missing = Vec::new();
|
|
||||||
let mut invocations = Vec::new();
|
|
||||||
let mut scopes = BTreeMap::new();
|
|
||||||
let arena: Arena<Scope> = Arena::new();
|
let arena: Arena<Scope> = Arena::new();
|
||||||
|
let mut invocations = Vec::<Invocation>::new();
|
||||||
|
let mut scopes = BTreeMap::new();
|
||||||
|
|
||||||
while let Some(first) = remaining.first().copied() {
|
for group in &groups {
|
||||||
if first.contains("::")
|
invocations.push(self.invocation(
|
||||||
&& !(first.starts_with(':') || first.ends_with(':') || first.contains(":::"))
|
|
||||||
{
|
|
||||||
remaining = first
|
|
||||||
.split("::")
|
|
||||||
.chain(remaining[1..].iter().copied())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let rest = &remaining[1..];
|
|
||||||
|
|
||||||
if let Some((invocation, consumed)) = self.invocation(
|
|
||||||
0,
|
|
||||||
&mut Vec::new(),
|
|
||||||
&arena,
|
&arena,
|
||||||
&mut scopes,
|
&group.arguments,
|
||||||
config,
|
config,
|
||||||
&dotenv,
|
&dotenv,
|
||||||
search,
|
|
||||||
&scope,
|
&scope,
|
||||||
first,
|
&group.path,
|
||||||
rest,
|
0,
|
||||||
)? {
|
&mut scopes,
|
||||||
remaining = rest[consumed..].to_vec();
|
search,
|
||||||
invocations.push(invocation);
|
)?);
|
||||||
} else {
|
|
||||||
missing.push(first.to_string());
|
|
||||||
remaining = rest.to_vec();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !missing.is_empty() {
|
|
||||||
let suggestion = if missing.len() == 1 {
|
|
||||||
self.suggest_recipe(missing.first().unwrap())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
return Err(Error::UnknownRecipes {
|
|
||||||
recipes: missing,
|
|
||||||
suggestion,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut ran = Ran::default();
|
let mut ran = Ran::default();
|
||||||
@ -278,21 +238,29 @@ impl<'src> Justfile<'src> {
|
|||||||
|
|
||||||
fn invocation<'run>(
|
fn invocation<'run>(
|
||||||
&'run self,
|
&'run self,
|
||||||
depth: usize,
|
|
||||||
path: &mut Vec<&'run str>,
|
|
||||||
arena: &'run Arena<Scope<'src, 'run>>,
|
arena: &'run Arena<Scope<'src, 'run>>,
|
||||||
scopes: &mut BTreeMap<Vec<&'run str>, &'run Scope<'src, 'run>>,
|
arguments: &[&'run str],
|
||||||
config: &'run Config,
|
config: &'run Config,
|
||||||
dotenv: &'run BTreeMap<String, String>,
|
dotenv: &'run BTreeMap<String, String>,
|
||||||
search: &'run Search,
|
|
||||||
parent: &'run Scope<'src, 'run>,
|
parent: &'run Scope<'src, 'run>,
|
||||||
first: &'run str,
|
path: &'run [String],
|
||||||
rest: &[&'run str],
|
position: usize,
|
||||||
) -> RunResult<'src, Option<(Invocation<'src, 'run>, usize)>> {
|
scopes: &mut BTreeMap<&'run [String], &'run Scope<'src, 'run>>,
|
||||||
if let Some(module) = self.modules.get(first) {
|
search: &'run Search,
|
||||||
path.push(first);
|
) -> RunResult<'src, Invocation<'src, 'run>> {
|
||||||
|
if position + 1 == path.len() {
|
||||||
|
let recipe = self.get_recipe(&path[position]).unwrap();
|
||||||
|
Ok(Invocation {
|
||||||
|
recipe,
|
||||||
|
module_source: &self.source,
|
||||||
|
arguments: arguments.into(),
|
||||||
|
settings: &self.settings,
|
||||||
|
scope: parent,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let module = self.modules.get(&path[position]).unwrap();
|
||||||
|
|
||||||
let scope = if let Some(scope) = scopes.get(path) {
|
let scope = if let Some(scope) = scopes.get(&path[..position]) {
|
||||||
scope
|
scope
|
||||||
} else {
|
} else {
|
||||||
let scope = Evaluator::evaluate_assignments(
|
let scope = Evaluator::evaluate_assignments(
|
||||||
@ -304,76 +272,21 @@ impl<'src> Justfile<'src> {
|
|||||||
search,
|
search,
|
||||||
)?;
|
)?;
|
||||||
let scope = arena.alloc(scope);
|
let scope = arena.alloc(scope);
|
||||||
scopes.insert(path.clone(), scope);
|
scopes.insert(path, scope);
|
||||||
scopes.get(path).unwrap()
|
scopes.get(path).unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
if rest.is_empty() {
|
module.invocation(
|
||||||
if let Some(recipe) = &module.default {
|
arena,
|
||||||
recipe.check_can_be_default_recipe()?;
|
arguments,
|
||||||
return Ok(Some((
|
config,
|
||||||
Invocation {
|
dotenv,
|
||||||
settings: &module.settings,
|
scope,
|
||||||
recipe,
|
path,
|
||||||
arguments: Vec::new(),
|
position + 1,
|
||||||
scope,
|
scopes,
|
||||||
module_source: &self.source,
|
search,
|
||||||
},
|
)
|
||||||
depth,
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
Err(Error::NoDefaultRecipe)
|
|
||||||
} else {
|
|
||||||
module.invocation(
|
|
||||||
depth + 1,
|
|
||||||
path,
|
|
||||||
arena,
|
|
||||||
scopes,
|
|
||||||
config,
|
|
||||||
dotenv,
|
|
||||||
search,
|
|
||||||
scope,
|
|
||||||
rest[0],
|
|
||||||
&rest[1..],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if let Some(recipe) = self.get_recipe(first) {
|
|
||||||
if recipe.parameters.is_empty() {
|
|
||||||
Ok(Some((
|
|
||||||
Invocation {
|
|
||||||
arguments: Vec::new(),
|
|
||||||
recipe,
|
|
||||||
scope: parent,
|
|
||||||
settings: &self.settings,
|
|
||||||
module_source: &self.source,
|
|
||||||
},
|
|
||||||
depth,
|
|
||||||
)))
|
|
||||||
} else {
|
|
||||||
let argument_range = recipe.argument_range();
|
|
||||||
let argument_count = cmp::min(rest.len(), recipe.max_arguments());
|
|
||||||
if !argument_range.range_contains(&argument_count) {
|
|
||||||
return Err(Error::ArgumentCountMismatch {
|
|
||||||
recipe: recipe.name(),
|
|
||||||
parameters: recipe.parameters.clone(),
|
|
||||||
found: rest.len(),
|
|
||||||
min: recipe.min_arguments(),
|
|
||||||
max: recipe.max_arguments(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(Some((
|
|
||||||
Invocation {
|
|
||||||
arguments: rest[..argument_count].to_vec(),
|
|
||||||
recipe,
|
|
||||||
scope: parent,
|
|
||||||
settings: &self.settings,
|
|
||||||
module_source: &self.source,
|
|
||||||
},
|
|
||||||
depth + argument_count,
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -523,21 +436,38 @@ mod tests {
|
|||||||
use Error::*;
|
use Error::*;
|
||||||
|
|
||||||
run_error! {
|
run_error! {
|
||||||
name: unknown_recipes,
|
name: unknown_recipe_no_suggestion,
|
||||||
src: "a:\nb:\nc:",
|
src: "a:\nb:\nc:",
|
||||||
args: ["a", "x", "y", "z"],
|
args: ["a", "xyz", "y", "z"],
|
||||||
error: UnknownRecipes {
|
error: UnknownRecipe {
|
||||||
recipes,
|
recipe,
|
||||||
suggestion,
|
suggestion,
|
||||||
},
|
},
|
||||||
check: {
|
check: {
|
||||||
assert_eq!(recipes, &["x", "y", "z"]);
|
assert_eq!(recipe, "xyz");
|
||||||
assert_eq!(suggestion, None);
|
assert_eq!(suggestion, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
run_error! {
|
run_error! {
|
||||||
name: unknown_recipes_show_alias_suggestion,
|
name: unknown_recipe_with_suggestion,
|
||||||
|
src: "a:\nb:\nc:",
|
||||||
|
args: ["a", "x", "y", "z"],
|
||||||
|
error: UnknownRecipe {
|
||||||
|
recipe,
|
||||||
|
suggestion,
|
||||||
|
},
|
||||||
|
check: {
|
||||||
|
assert_eq!(recipe, "x");
|
||||||
|
assert_eq!(suggestion, Some(Suggestion {
|
||||||
|
name: "a",
|
||||||
|
target: None,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run_error! {
|
||||||
|
name: unknown_recipe_show_alias_suggestion,
|
||||||
src: "
|
src: "
|
||||||
foo:
|
foo:
|
||||||
echo foo
|
echo foo
|
||||||
@ -545,12 +475,12 @@ mod tests {
|
|||||||
alias z := foo
|
alias z := foo
|
||||||
",
|
",
|
||||||
args: ["zz"],
|
args: ["zz"],
|
||||||
error: UnknownRecipes {
|
error: UnknownRecipe {
|
||||||
recipes,
|
recipe,
|
||||||
suggestion,
|
suggestion,
|
||||||
},
|
},
|
||||||
check: {
|
check: {
|
||||||
assert_eq!(recipes, &["zz"]);
|
assert_eq!(recipe, "zz");
|
||||||
assert_eq!(suggestion, Some(Suggestion {
|
assert_eq!(suggestion, Some(Suggestion {
|
||||||
name: "z",
|
name: "z",
|
||||||
target: Some("foo"),
|
target: Some("foo"),
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
pub(crate) use {
|
pub(crate) use {
|
||||||
crate::{
|
crate::{
|
||||||
alias::Alias, analyzer::Analyzer, assignment::Assignment,
|
alias::Alias, analyzer::Analyzer, argument_parser::ArgumentParser, assignment::Assignment,
|
||||||
assignment_resolver::AssignmentResolver, ast::Ast, attribute::Attribute, binding::Binding,
|
assignment_resolver::AssignmentResolver, ast::Ast, attribute::Attribute, binding::Binding,
|
||||||
color::Color, color_display::ColorDisplay, command_ext::CommandExt, compilation::Compilation,
|
color::Color, color_display::ColorDisplay, command_ext::CommandExt, compilation::Compilation,
|
||||||
compile_error::CompileError, compile_error_kind::CompileErrorKind, compiler::Compiler,
|
compile_error::CompileError, compile_error_kind::CompileErrorKind, compiler::Compiler,
|
||||||
@ -113,6 +113,7 @@ pub mod summary;
|
|||||||
|
|
||||||
mod alias;
|
mod alias;
|
||||||
mod analyzer;
|
mod analyzer;
|
||||||
|
mod argument_parser;
|
||||||
mod assignment;
|
mod assignment;
|
||||||
mod assignment_resolver;
|
mod assignment_resolver;
|
||||||
mod ast;
|
mod ast;
|
||||||
|
@ -150,7 +150,7 @@ impl Subcommand {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match Self::run_inner(config, loader, arguments, overrides, &search) {
|
match Self::run_inner(config, loader, arguments, overrides, &search) {
|
||||||
Err((err @ Error::UnknownRecipes { .. }, true)) => {
|
Err((err @ Error::UnknownRecipe { .. }, true)) => {
|
||||||
match search.justfile.parent().unwrap().parent() {
|
match search.justfile.parent().unwrap().parent() {
|
||||||
Some(parent) => {
|
Some(parent) => {
|
||||||
unknown_recipes_errors.get_or_insert(err);
|
unknown_recipes_errors.get_or_insert(err);
|
||||||
@ -428,7 +428,9 @@ impl Subcommand {
|
|||||||
module = module
|
module = module
|
||||||
.modules
|
.modules
|
||||||
.get(name)
|
.get(name)
|
||||||
.ok_or_else(|| Error::UnknownSubmodule { path: path.clone() })?;
|
.ok_or_else(|| Error::UnknownSubmodule {
|
||||||
|
path: path.to_string(),
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::list_module(config, module, 0);
|
Self::list_module(config, module, 0);
|
||||||
@ -588,7 +590,9 @@ impl Subcommand {
|
|||||||
module = module
|
module = module
|
||||||
.modules
|
.modules
|
||||||
.get(name)
|
.get(name)
|
||||||
.ok_or_else(|| Error::UnknownSubmodule { path: path.clone() })?;
|
.ok_or_else(|| Error::UnknownSubmodule {
|
||||||
|
path: path.to_string(),
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = path.path.last().unwrap();
|
let name = path.path.last().unwrap();
|
||||||
@ -602,8 +606,8 @@ impl Subcommand {
|
|||||||
println!("{}", recipe.color_display(config.color.stdout()));
|
println!("{}", recipe.color_display(config.color.stdout()));
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(Error::UnknownRecipes {
|
Err(Error::UnknownRecipe {
|
||||||
recipes: vec![name.to_owned()],
|
recipe: name.to_owned(),
|
||||||
suggestion: module.suggest_recipe(name),
|
suggestion: module.suggest_recipe(name),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -131,7 +131,7 @@ macro_rules! run_error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! assert_matches {
|
macro_rules! assert_matches {
|
||||||
($expression:expr, $( $pattern:pat_param )|+ $( if $guard:expr )?) => {
|
($expression:expr, $( $pattern:pat_param )|+ $( if $guard:expr )? $(,)?) => {
|
||||||
match $expression {
|
match $expression {
|
||||||
$( $pattern )|+ $( if $guard )? => {}
|
$( $pattern )|+ $( if $guard )? => {}
|
||||||
left => panic!(
|
left => panic!(
|
||||||
|
@ -652,7 +652,7 @@ test! {
|
|||||||
justfile: "hello:",
|
justfile: "hello:",
|
||||||
args: ("foo", "bar"),
|
args: ("foo", "bar"),
|
||||||
stdout: "",
|
stdout: "",
|
||||||
stderr: "error: Justfile does not contain recipes `foo` or `bar`.\n",
|
stderr: "error: Justfile does not contain recipe `foo`.\n",
|
||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ fn missing_recipe_after_invalid_path() {
|
|||||||
.test_round_trip(false)
|
.test_round_trip(false)
|
||||||
.arg(":foo::foo")
|
.arg(":foo::foo")
|
||||||
.arg("bar")
|
.arg("bar")
|
||||||
.stderr("error: Justfile does not contain recipes `:foo::foo` or `bar`.\n")
|
.stderr("error: Justfile does not contain recipe `:foo::foo`.\n")
|
||||||
.status(EXIT_FAILURE)
|
.status(EXIT_FAILURE)
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
@ -690,3 +690,94 @@ fn recipes_with_same_name_are_both_run() {
|
|||||||
.stdout("MODULE\nROOT\n")
|
.stdout("MODULE\nROOT\n")
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn submodule_recipe_not_found_error_message() {
|
||||||
|
Test::new()
|
||||||
|
.args(["--unstable", "foo::bar"])
|
||||||
|
.stderr("error: Justfile does not contain submodule `foo`\n")
|
||||||
|
.status(1)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn submodule_recipe_not_found_spaced_error_message() {
|
||||||
|
Test::new()
|
||||||
|
.write("foo.just", "bar:\n @echo MODULE")
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.args(["--unstable", "foo", "baz"])
|
||||||
|
.stderr("error: Justfile does not contain recipe `foo baz`.\nDid you mean `bar`?\n")
|
||||||
|
.status(1)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn submodule_recipe_not_found_colon_separated_error_message() {
|
||||||
|
Test::new()
|
||||||
|
.write("foo.just", "bar:\n @echo MODULE")
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.args(["--unstable", "foo::baz"])
|
||||||
|
.stderr("error: Justfile does not contain recipe `foo::baz`.\nDid you mean `bar`?\n")
|
||||||
|
.status(1)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn colon_separated_path_does_not_run_recipes() {
|
||||||
|
Test::new()
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
foo:
|
||||||
|
@echo FOO
|
||||||
|
|
||||||
|
bar:
|
||||||
|
@echo BAR
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.args(["--unstable", "foo::bar"])
|
||||||
|
.stderr("error: Expected submodule at `foo` but found recipe.\n")
|
||||||
|
.status(1)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expected_submodule_but_found_recipe_in_root_error() {
|
||||||
|
Test::new()
|
||||||
|
.justfile("foo:")
|
||||||
|
.arg("foo::baz")
|
||||||
|
.stderr("error: Expected submodule at `foo` but found recipe.\n")
|
||||||
|
.status(1)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expected_submodule_but_found_recipe_in_submodule_error() {
|
||||||
|
Test::new()
|
||||||
|
.justfile("mod foo")
|
||||||
|
.write("foo.just", "bar:")
|
||||||
|
.test_round_trip(false)
|
||||||
|
.args(["--unstable", "foo::bar::baz"])
|
||||||
|
.stderr("error: Expected submodule at `foo::bar` but found recipe.\n")
|
||||||
|
.status(1)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn colon_separated_path_components_are_not_used_as_arguments() {
|
||||||
|
Test::new()
|
||||||
|
.justfile("foo bar:")
|
||||||
|
.args(["foo::bar"])
|
||||||
|
.stderr("error: Expected submodule at `foo` but found recipe.\n")
|
||||||
|
.status(1)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user