summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCasey Rodarmor <casey@rodarmor.com>2023-12-27 20:27:15 -0800
committerGitHub <noreply@github.com>2023-12-28 04:27:15 +0000
commit316ea0129524f69aaff8ba2d5ff2907a3eb761ea (patch)
tree57d29c0b6c34463762275f129da325390a18af67
parentbc628215c038c69e7c51e51796b35d505fc95695 (diff)
Add modules (#1782)
-rw-r--r--README.md47
-rw-r--r--src/alias.rs4
-rw-r--r--src/analyzer.rs91
-rw-r--r--src/compile_error.rs45
-rw-r--r--src/compile_error_kind.rs10
-rw-r--r--src/compiler.rs81
-rw-r--r--src/error.rs16
-rw-r--r--src/item.rs11
-rw-r--r--src/justfile.rs250
-rw-r--r--src/keyword.rs1
-rw-r--r--src/node.rs3
-rw-r--r--src/parser.rs7
-rw-r--r--src/recipe.rs12
-rw-r--r--src/recipe_context.rs2
-rw-r--r--src/scope.rs4
-rw-r--r--src/search.rs4
-rw-r--r--src/subcommand.rs19
-rw-r--r--src/summary.rs2
-rw-r--r--src/testing.rs2
-rw-r--r--tests/json.rs128
-rw-r--r--tests/lib.rs1
-rw-r--r--tests/misc.rs8
-rw-r--r--tests/modules.rs446
23 files changed, 1025 insertions, 169 deletions
diff --git a/README.md b/README.md
index 2eea7fed..7b8c2210 100644
--- a/README.md
+++ b/README.md
@@ -2329,7 +2329,7 @@ And will both invoke recipes `a` and `b` in `foo/justfile`.
### Imports
-One `justfile` can include the contents of another using an `import` statement.
+One `justfile` can include the contents of another using `import` statements.
If you have the following `justfile`:
@@ -2366,6 +2366,51 @@ and recipes defined after the `import` statement.
Imported files can themselves contain `import`s, which are processed
recursively.
+### Modules<sup>master</sup>
+
+A `justfile` can declare modules using `mod` statements. `mod` statements are
+currently unstable, so you'll need to use the `--unstable` flag, or set the
+`JUST_UNSTABLE` environment variable to use them.
+
+If you have the following `justfile`:
+
+```mf
+mod bar
+
+a:
+ @echo A
+```
+
+And the following text in `bar.just`:
+
+```just
+b:
+ @echo B
+```
+
+`bar.just` will be included in `justfile` as a submodule. Recipes, aliases, and
+variables defined in one submodule cannot be used in another, and each module
+uses its own settings.
+
+Recipes in submodules can be invoked as subcommands:
+
+```sh
+$ just --unstable bar b
+B
+```
+
+If a module is named `foo`, just will search for the module file in `foo.just`,
+`foo/mod.just`, `foo/justfile`, and `foo/.justfile`. In the latter two cases,
+the module file may have any capitalization.
+
+Environment files are loaded for the root justfile.
+
+Currently, recipes in submodules run with the same working directory as the
+root `justfile`, and the `justfile()` and `justfile_directory()` functions
+return the path to the root `justfile` and its parent directory.
+
+See the [module stabilization tracking issue](https://github.com/casey/just/issues/929) for more information.
+
### Hiding `justfile`s
`just` looks for `justfile`s named `justfile` and `.justfile`, which can be used to keep a `justfile` hidden.
diff --git a/src/alias.rs b/src/alias.rs
index 0c19fe5a..bb9d4400 100644
--- a/src/alias.rs
+++ b/src/alias.rs
@@ -13,10 +13,6 @@ pub(crate) struct Alias<'src, T = Rc<Recipe<'src>>> {
}
impl<'src> Alias<'src, Name<'src>> {
- pub(crate) fn line_number(&self) -> usize {
- self.name.line
- }
-
pub(crate) fn resolve(self, target: Rc<Recipe<'src>>) -> Alias<'src> {
assert_eq!(self.target.lexeme(), target.name.lexeme());
diff --git a/src/analyzer.rs b/src/analyzer.rs
index e8745e95..0f138a7c 100644
--- a/src/analyzer.rs
+++ b/src/analyzer.rs
@@ -9,7 +9,7 @@ pub(crate) struct Analyzer<'src> {
impl<'src> Analyzer<'src> {
pub(crate) fn analyze(
- loaded: Vec<PathBuf>,
+ loaded: &[PathBuf],
paths: &HashMap<PathBuf, PathBuf>,
asts: &HashMap<PathBuf, Ast<'src>>,
root: &Path,
@@ -19,7 +19,7 @@ impl<'src> Analyzer<'src> {
fn justfile(
mut self,
- loaded: Vec<PathBuf>,
+ loaded: &[PathBuf],
paths: &HashMap<PathBuf, PathBuf>,
asts: &HashMap<PathBuf, Ast<'src>>,
root: &Path,
@@ -31,11 +31,42 @@ impl<'src> Analyzer<'src> {
let mut warnings = Vec::new();
+ let mut modules: BTreeMap<String, (Name, Justfile)> = BTreeMap::new();
+
+ let mut definitions: HashMap<&str, (&'static str, Name)> = HashMap::new();
+
+ let mut define = |name: Name<'src>,
+ second_type: &'static str,
+ duplicates_allowed: bool|
+ -> CompileResult<'src, ()> {
+ if let Some((first_type, original)) = definitions.get(name.lexeme()) {
+ if !(*first_type == second_type && duplicates_allowed) {
+ let (original, redefinition) = if name.line < original.line {
+ (name, *original)
+ } else {
+ (*original, name)
+ };
+
+ return Err(redefinition.token().error(Redefinition {
+ first_type,
+ second_type,
+ name: name.lexeme(),
+ first: original.line,
+ }));
+ }
+ }
+
+ definitions.insert(name.lexeme(), (second_type, name));
+
+ Ok(())
+ };
+
while let Some(ast) = stack.pop() {
for item in &ast.items {
match item {
Item::Alias(alias) => {
- self.analyze_alias(alias)?;
+ define(alias.name, "alias", false)?;
+ Self::analyze_alias(alias)?;
self.aliases.insert(alias.clone());
}
Item::Assignment(assignment) => {
@@ -43,6 +74,19 @@ impl<'src> Analyzer<'src> {
self.assignments.insert(assignment.clone());
}
Item::Comment(_) => (),
+ Item::Import { absolute, .. } => {
+ stack.push(asts.get(absolute.as_ref().unwrap()).unwrap());
+ }
+ Item::Mod { absolute, name } => {
+ define(*name, "module", false)?;
+ modules.insert(
+ name.to_string(),
+ (
+ *name,
+ Self::analyze(loaded, paths, asts, absolute.as_ref().unwrap())?,
+ ),
+ );
+ }
Item::Recipe(recipe) => {
if recipe.enabled() {
Self::analyze_recipe(recipe)?;
@@ -53,9 +97,6 @@ impl<'src> Analyzer<'src> {
self.analyze_set(set)?;
self.sets.insert(set.clone());
}
- Item::Import { absolute, .. } => {
- stack.push(asts.get(absolute.as_ref().unwrap()).unwrap());
- }
}
}
@@ -69,14 +110,7 @@ impl<'src> Analyzer<'src> {
AssignmentResolver::resolve_assignments(&self.assignments)?;
for recipe in recipes {
- if let Some(original) = recipe_table.get(recipe.name.lexeme()) {
- if !settings.allow_duplicate_recipes {
- return Err(recipe.name.token().error(DuplicateRecipe {
- recipe: original.name(),
- first: original.line_number(),
- }));
- }
- }
+ define(recipe.name, "recipe", settings.allow_duplicate_recipes)?;
recipe_table.insert(recipe.clone());
}
@@ -103,10 +137,14 @@ impl<'src> Analyzer<'src> {
}),
aliases,
assignments: self.assignments,
- loaded,
+ loaded: loaded.into(),
recipes,
settings,
warnings,
+ modules: modules
+ .into_iter()
+ .map(|(name, (_name, justfile))| (name, justfile))
+ .collect(),
})
}
@@ -164,16 +202,9 @@ impl<'src> Analyzer<'src> {
Ok(())
}
- fn analyze_alias(&self, alias: &Alias<'src, Name<'src>>) -> CompileResult<'src, ()> {
+ fn analyze_alias(alias: &Alias<'src, Name<'src>>) -> CompileResult<'src, ()> {
let name = alias.name.lexeme();
- if let Some(original) = self.aliases.get(name) {
- return Err(alias.name.token().error(DuplicateAlias {
- alias: name,
- first: original.line_number(),
- }));
- }
-
for attr in &alias.attributes {
if *attr != Attribute::Private {
return Err(alias.name.token().error(AliasInvalidAttribute {
@@ -232,7 +263,7 @@ mod tests {
line: 1,
column: 6,
width: 3,
- kind: DuplicateAlias { alias: "foo", first: 0 },
+ kind: Redefinition { first_type: "alias", second_type: "alias", name: "foo", first: 0 },
}
analysis_error! {
@@ -248,11 +279,11 @@ mod tests {
analysis_error! {
name: alias_shadows_recipe_before,
input: "bar: \n echo bar\nalias foo := bar\nfoo:\n echo foo",
- offset: 23,
- line: 2,
- column: 6,
+ offset: 34,
+ line: 3,
+ column: 0,
width: 3,
- kind: AliasShadowsRecipe {alias: "foo", recipe_line: 3},
+ kind: Redefinition { first_type: "alias", second_type: "recipe", name: "foo", first: 2 },
}
analysis_error! {
@@ -262,7 +293,7 @@ mod tests {
line: 2,
column: 6,
width: 3,
- kind: AliasShadowsRecipe { alias: "foo", recipe_line: 0 },
+ kind: Redefinition { first_type: "alias", second_type: "recipe", name: "foo", first: 0 },
}
analysis_error! {
@@ -302,7 +333,7 @@ mod tests {
line: 2,
column: 0,
width: 1,
- kind: DuplicateRecipe{recipe: "a", first: 0},
+ kind: Redefinition { first_type: "recipe", second_type: "recipe", name: "a", first: 0 },
}
analysis_error! {
diff --git a/src/compile_error.rs b/src/compile_error.rs
index 3d3718b6..d1af6a0f 100644
--- a/src/compile_error.rs
+++ b/src/compile_error.rs
@@ -19,6 +19,14 @@ impl<'src> CompileError<'src> {
}
}
+fn capitalize(s: &str) -> String {
+ let mut chars = s.chars();
+ match chars.next() {
+ None => String::new(),
+ Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
+ }
+}
+
impl Display for CompileError<'_> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
use CompileErrorKind::*;
@@ -82,12 +90,6 @@ impl Display for CompileError<'_> {
write!(f, "at most {max} {}", Count("argument", *max))
}
}
- DuplicateAlias { alias, first } => write!(
- f,
- "Alias `{alias}` first defined on line {} is redefined on line {}",
- first.ordinal(),
- self.token.line.ordinal(),
- ),
DuplicateAttribute { attribute, first } => write!(
f,
"Recipe attribute `{attribute}` first used on line {} is duplicated on line {}",
@@ -97,12 +99,6 @@ impl Display for CompileError<'_> {
DuplicateParameter { recipe, parameter } => {
write!(f, "Recipe `{recipe}` has duplicate parameter `{parameter}`")
}
- DuplicateRecipe { recipe, first } => write!(
- f,
- "Recipe `{recipe}` first defined on line {} is redefined on line {}",
- first.ordinal(),
- self.token.line.ordinal(),
- ),
DuplicateSet { setting, first } => write!(
f,
"Setting `{setting}` first set on line {} is redefined on line {}",
@@ -183,6 +179,31 @@ impl Display for CompileError<'_> {
write!(f, "Parameter `{parameter}` follows variadic parameter")
}
ParsingRecursionDepthExceeded => write!(f, "Parsing recursion depth exceeded"),
+ Redefinition {
+ first,
+ first_type,
+ name,
+ second_type,
+ } => {
+ if first_type == second_type {
+ write!(
+ f,
+ "{} `{name}` first defined on line {} is redefined on line {}",
+ capitalize(first_type),
+ first.ordinal(),
+ self.token.line.ordinal(),
+ )
+ } else {
+ write!(
+ f,
+ "{} `{name}` defined on line {} is redefined as {} {second_type} on line {}",
+ capitalize(first_type),
+ first.ordinal(),
+ if *second_type == "alias" { "an" } else { "a" },
+ self.token.line.ordinal(),
+ )
+ }
+ }
RequiredParameterFollowsDefaultParameter { parameter } => write!(
f,
"Non-default parameter `{parameter}` follows default parameter"
diff --git a/src/compile_error_kind.rs b/src/compile_error_kind.rs
index 0be55552..4c98d5e2 100644
--- a/src/compile_error_kind.rs
+++ b/src/compile_error_kind.rs
@@ -25,9 +25,11 @@ pub(crate) enum CompileErrorKind<'src> {
min: usize,
max: usize,
},
- DuplicateAlias {
- alias: &'src str,
+ Redefinition {
first: usize,
+ first_type: &'static str,
+ name: &'src str,
+ second_type: &'static str,
},
DuplicateAttribute {
attribute: &'src str,
@@ -37,10 +39,6 @@ pub(crate) enum CompileErrorKind<'src> {
recipe: &'src str,
parameter: &'src str,
},
- DuplicateRecipe {
- recipe: &'src str,
- first: usize,
- },
DuplicateSet {
setting: &'src str,
first: usize,
diff --git a/src/compiler.rs b/src/compiler.rs
index 5545f6de..b7b0eca2 100644
--- a/src/compiler.rs
+++ b/src/compiler.rs
@@ -4,6 +4,7 @@ pub(crate) struct Compiler;
impl Compiler {
pub(crate) fn compile<'src>(
+ unstable: bool,
loader: &'src Loader,
root: &Path,
) -> RunResult<'src, Compilation<'src>> {
@@ -25,20 +26,40 @@ impl Compiler {
srcs.insert(current.clone(), src);
for item in &mut ast.items {
- if let Item::Import { relative, absolute } = item {
- let import = current.parent().unwrap().join(&relative.cooked).lexiclean();
- if srcs.contains_key(&import) {
- return Err(Error::CircularImport { current, import });
+ match item {
+ Item::Mod { name, absolute } => {
+ if !unstable {
+ return Err(Error::Unstable {
+ message: "Modules are currently unstable.".into(),
+ });
+ }
+
+ let parent = current.parent().unwrap();
+
+ let import = Self::find_module_file(parent, *name)?;
+
+ if srcs.contains_key(&import) {
+ return Err(Error::CircularImport { current, import });
+ }
+ *absolute = Some(import.clone());
+ stack.push(import);
}
- *absolute = Some(import.clone());
- stack.push(import);
+ Item::Import { relative, absolute } => {
+ let import = current.parent().unwrap().join(&relative.cooked).lexiclean();
+ if srcs.contains_key(&import) {
+ return Err(Error::CircularImport { current, import });
+ }
+ *absolute = Some(import.clone());
+ stack.push(import);
+ }
+ _ => {}
}
}
asts.insert(current.clone(), ast.clone());
}
- let justfile = Analyzer::analyze(loaded, &paths, &asts, root)?;
+ let justfile = Analyzer::analyze(&loaded, &paths, &asts, root)?;
Ok(Compilation {
asts,
@@ -48,6 +69,46 @@ impl Compiler {
})
}
+ fn find_module_file<'src>(parent: &Path, module: Name<'src>) -> RunResult<'src, PathBuf> {
+ let mut candidates = vec![format!("{module}.just"), format!("{module}/mod.just")]
+ .into_iter()
+ .filter(|path| parent.join(path).is_file())
+ .collect::<Vec<String>>();
+
+ let directory = parent.join(module.lexeme());
+
+ if directory.exists() {
+ let entries = fs::read_dir(&directory).map_err(|io_error| SearchError::Io {
+ io_error,
+ directory: directory.clone(),
+ })?;
+
+ for entry in entries {
+ let entry = entry.map_err(|io_error| SearchError::Io {
+ io_error,
+ directory: directory.clone(),
+ })?;
+
+ if let Some(name) = entry.file_name().to_str() {
+ for justfile_name in search::JUSTFILE_NAMES {
+ if name.eq_ignore_ascii_case(justfile_name) {
+ candidates.push(format!("{module}/{name}"));
+ }
+ }
+ }
+ }
+ }
+
+ match candidates.as_slice() {
+ [] => Err(Error::MissingModuleFile { module }),
+ [file] => Ok(parent.join(file).lexiclean()),
+ found => Err(Error::AmbiguousModuleFile {
+ found: found.into(),
+ module,
+ }),
+ }
+ }
+
#[cfg(test)]
pub(crate) fn test_compile(src: &str) -> CompileResult<Justfile> {
let tokens = Lexer::test_lex(src)?;
@@ -57,7 +118,7 @@ impl Compiler {
asts.insert(root.clone(), ast);
let mut paths: HashMap<PathBuf, PathBuf> = HashMap::new();
paths.insert(root.clone(), root.clone());
- Analyzer::analyze(Vec::new(), &paths, &asts, &root)
+ Analyzer::analyze(&[], &paths, &asts, &root)
}
}
@@ -97,7 +158,7 @@ recipe_b: recipe_c
let loader = Loader::new();
let justfile_a_path = tmp.path().join("justfile");
- let compilation = Compiler::compile(&loader, &justfile_a_path).unwrap();
+ let compilation = Compiler::compile(false, &loader, &justfile_a_path).unwrap();
assert_eq!(compilation.root_src(), justfile_a);
}
@@ -129,7 +190,7 @@ recipe_b:
let loader = Loader::new();
let justfile_a_path = tmp.path().join("justfile");
- let loader_output = Compiler::compile(&loader, &justfile_a_path).unwrap_err();
+ let loader_output = Compiler::compile(false, &loader, &justfile_a_path).unwrap_err();
assert_matches!(loader_output, Error::CircularImport { current, import }
if current == tmp.path().join("subdir").join("justfile_b").lexiclean() &&
diff --git a/src/error.rs b/src/error.rs
index f6c1f68c..a445e2e0 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -2,6 +2,10 @@ use super::*;
#[derive(Debug)]
pub(crate) enum Error<'src> {
+ AmbiguousModuleFile {
+ module: Name<'src>,
+ found: Vec<String>,
+ },
ArgumentCountMismatch {
recipe: &'src str,
parameters: Vec<Parameter<'src>>,
@@ -105,6 +109,9 @@ pub(crate) enum Error<'src> {
path: PathBuf,
io_error: io::Error,
},
+ MissingModuleFile {
+ module: Name<'src>,
+ },
NoChoosableRecipes,
NoDefaultRecipe,
NoRecipes,
@@ -167,6 +174,9 @@ impl<'src> Error<'src> {
fn context(&self) -> Option<Token<'src>> {
match self {
+ Self::AmbiguousModuleFile { module, .. } | Self::MissingModuleFile { module, .. } => {
+ Some(module.token())
+ }
Self::Backtick { token, .. } => Some(*token),
Self::Compile { compile_error } => Some(compile_error.context()),
Self::FunctionCall { function, .. } => Some(function.token()),
@@ -224,6 +234,11 @@ impl<'src> ColorDisplay for Error<'src> {
write!(f, "{error}: {message}")?;
match self {
+ AmbiguousModuleFile { module, found } =>
+ write!(f,
+ "Found multiple source files for module `{module}`: {}",
+ List::and_ticked(found),
+ )?,
ArgumentCountMismatch { recipe, found, min, max, .. } => {
let count = Count("argument", *found);
if min == max {
@@ -350,6 +365,7 @@ impl<'src> ColorDisplay for Error<'src> {
let path = path.display();
write!(f, "Failed to read justfile at `{path}`: {io_error}")?;
}
+ MissingModuleFile { module } => write!(f, "Could not find source file for module `{module}`.")?,
NoChoosableRecipes => write!(f, "Justfile contains no choosable recipes.")?,
NoDefaultRecipe => write!(f, "Justfile contains no default recipe.")?,
NoRecipes => write!(f, "Justfile contains no recipes.")?,
diff --git a/src/item.rs b/src/item.rs
index a12160cb..a709e360 100644
--- a/src/item.rs
+++ b/src/item.rs
@@ -6,12 +6,16 @@ pub(crate) enum Item<'src> {
Alias(Alias<'src, Name<'src>>),
Assignment(Assignment<'src>),
Comment(&'src str),
- Recipe(UnresolvedRecipe<'src>),
- Set(Set<'src>),
Import {
relative: StringLiteral<'src>,
absolute: Option<PathBuf>,
},
+ Mod {
+ name: Name<'src>,
+ absolute: Option<PathBuf>,
+ },
+ Recipe(UnresolvedRecipe<'src>),
+ Set(Set<'src>),
}
impl<'src> Display for Item<'src> {
@@ -20,9 +24,10 @@ impl<'src> Display for Item<'src> {
Item::Alias(alias) => write!(f, "{alias}"),
Item::Assignment(assignment) => write!(f, "{assignment}"),
Item::Comment(comment) => write!(f, "{comment}"),
+ Item::Import { relative, .. } => write!(f, "import {relative}"),
+ Item::Mod { name, .. } => write!(f, "mod {name}"),
Item::Recipe(recipe) => write!(f, "{}", recipe.color_display(Color::never())),
Item::Set(set) => write!(f, "{set}"),
- Item::Import { relative, .. } => write!(f, "import {relative}"),
}
}
}
diff --git a/src/justfile.rs b/src/justfile.rs
index 45b327d5..23134208 100644
--- a/src/justfile.rs
+++ b/src/justfile.rs
@@ -1,5 +1,13 @@
use {super::*, serde::Serialize};
+#[derive(Debug)]
+struct Invocation<'src: 'run, 'run> {
+ arguments: &'run [&'run str],
+ recipe: &'run Recipe<'src>,
+ settings: &'run Settings<'src>,
+ scope: &'run Scope<'src, 'run>,
+}
+
#[derive(Debug, PartialEq, Serialize)]
pub(crate) struct Justfile<'src> {
pub(crate) aliases: Table<'src, Alias<'src>>,
@@ -8,6 +16,7 @@ pub(crate) struct Justfile<'src> {
pub(crate) default: Option<Rc<Recipe<'src>>>,
#[serde(skip)]
pub(crate) loaded: Vec<PathBuf>,
+ pub(crate) modules: BTreeMap<String, Justfile<'src>>,
pub(crate) recipes: Table<'src, Rc<Recipe<'src>>>,
pub(crate) settings: Settings<'src>,
pub(crate) warnings: Vec<Warning>,
@@ -67,6 +76,44 @@ impl<'src> Justfile<'src> {
.next()
}
+ fn scope<'run>(
+ &'run self,
+ config: &'run Config,
+ dotenv: &'run BTreeMap<String, String>,
+ search: &'run Search,
+ overrides: &BTreeMap<String, String>,
+ parent: &'run Scope<'src, 'run>,
+ ) -> RunResult<'src, Scope<'src, 'run>>
+ where
+ 'src: 'run,
+ {
+ let mut scope = parent.child();
+ let mut unknown_overrides = Vec::new();
+
+ for (name, value) in overrides {
+ if let Some(assignment) = self.assignments.get(name) {
+ scope.bind(assignment.export, assignment.name, value.clone());
+ } else {
+ unknown_overrides.push(name.clone());
+ }
+ }
+
+ if !unknown_overrides.is_empty() {
+ return Err(Error::UnknownOverrides {
+ overrides: unknown_overrides,
+ });
+ }
+
+ Evaluator::evaluate_assignments(
+ &self.assignments,
+ config,
+ dotenv,
+ scope,
+ &self.settings,
+ search,
+ )
+ }
+
pub(crate) fn run(
&self,
config: &Config,
@@ -92,33 +139,9 @@ impl<'src> Justfile<'src> {
BTreeMap::new()
};
- let scope = {
- let mut scope = Scope::new();
- let mut unknown_overrides = Vec::new();
+ let root = Scope::new();
- for (name, value) in overrides {
- if let Some(assignment) = self.assignments.get(name) {
- scope.bind(assignment.export, assignment.name, value.clone());
- } else {
- unknown_overrides.push(name.clone());
- }
- }
-
- if !unknown_overrides.is_empty() {
- return Err(Error::UnknownOverrides {
- overrides: unknown_overrides,
- });
- }
-
- Evaluator::evaluate_assignments(
- &self.assignments,
- config,
- &dotenv,
- scope,
- &self.settings,
- search,
- )?
- };
+ let scope = self.scope(config, &dotenv, search, overrides, &root)?;
match &config.subcommand {
Subcommand::Command {
@@ -193,13 +216,7 @@ impl<'src> Justfile<'src> {
let argvec: Vec<&str> = if !arguments.is_empty() {
arguments.iter().map(String::as_str).collect()
} else if let Some(recipe) = &self.default {
- let min_arguments = recipe.min_arguments();
- if min_arguments > 0 {
- return Err(Error::DefaultRecipeRequiresArguments {
- recipe: recipe.name.lexeme(),
- min_arguments,
- });
- }
+ recipe.check_can_be_default_recipe()?;
vec![recipe.name()]
} else if self.recipes.is_empty() {
return Err(Error::NoRecipes);
@@ -209,33 +226,31 @@ impl<'src> Justfile<'src> {
let arguments = argvec.as_slice();
- let mut missing = vec![];
- let mut grouped = vec![];
- let mut rest = arguments;
-
- while let Some((argument, mut tail)) = rest.split_first() {
- if let Some(recipe) = self.get_recipe(argument) {
- if recipe.parameters.is_empty() {
- grouped.push((recipe, &[][..]));
- } else {
- 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(Error::ArgumentCountMismatch {
- recipe: recipe.name(),
- parameters: recipe.parameters.clone(),
- found: tail.len(),
- min: recipe.min_arguments(),
- max: recipe.max_arguments(),
- });
- }
- grouped.push((recipe, &tail[0..argument_count]));
- tail = &tail[argument_count..];
- }
+ let mut missing = Vec::new();
+ let mut invocations = Vec::new();
+ let mut remaining = arguments;
+ let mut scopes = BTreeMap::new();
+ let arena: Arena<Scope> = Arena::new();
+
+ while let Some((first, mut rest)) = remaining.split_first() {
+ if let Some((invocation, consumed)) = self.invocation(
+ 0,
+ &mut Vec::new(),
+ &arena,
+ &mut scopes,
+ config,
+ &dotenv,
+ search,
+ &scope,
+ first,
+ rest,
+ )? {
+ rest = &rest[consumed..];
+ invocations.push(invocation);
} else {
- missing.push((*argument).to_owned());
+ missing.push((*first).to_owned());
}
- rest = tail;
+ remaining = rest;
}
if !missing.is_empty() {
@@ -250,16 +265,23 @@ impl<'src> Justfile<'src> {
});
}
- let context = RecipeContext {
- settings: &self.settings,
- config,
- scope,
- search,
- };
-
let mut ran = BTreeSet::new();
- for (recipe, arguments) in grouped {
- Self::run_recipe(&context, recipe, arguments, &dotenv, search, &mut ran)?;
+ for invocation in invocations {
+ let context = RecipeContext {
+ settings: invocation.settings,
+ config,
+ scope: invocation.scope,
+ search,
+ };
+
+ Self::run_recipe(
+ &context,
+ invocation.recipe,
+ invocation.arguments,
+ &dotenv,
+ search,
+ &mut ran,
+ )?;
}
Ok(())
@@ -277,6 +299,98 @@ impl<'src> Justfile<'src> {
.or_else(|| self.aliases.get(name).map(|alias| alias.target.as_ref()))
}
+ #[allow(clippy::too_many_arguments)]
+ fn invocation<'run>(
+ &'run self,
+ depth: usize,
+ path: &mut Vec<&'run str>,
+ arena: &'run Arena<Scope<'src, 'run>>,
+ scopes: &mut BTreeMap<Vec<&'run str>, &'run Scope<'src, 'run>>,
+ config: &'run Config,
+ dotenv: &'run BTreeMap<String, String>,
+ search: &'run Search,
+ parent: &'run Scope<'src, 'run>,
+ first: &'run str,
+ rest: &'run [&'run str],
+ ) -> RunResult<'src, Option<(Invocation<'src, 'run>, usize)>> {
+ if let Some(module) = self.modules.get(first) {
+ path.push(first);
+
+ let scope = if let Some(scope) = scopes.get(path) {
+ scope
+ } else {
+ let scope = module.scope(config, dotenv, search, &BTreeMap::new(), parent)?;
+ let scope = arena.alloc(scope);
+ scopes.insert(path.clone(), scope);
+ scopes.get(path).unwrap()
+ };
+
+ if rest.is_empty() {
+ if let Some(recipe) = &module.default {
+ recipe.check_can_be_default_recipe()?;
+ return Ok(Some((
+ Invocation {
+ settings: &module.settings,
+ recipe,
+ arguments: &[],
+ scope,
+ },
+ 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: &[],
+ recipe,
+ scope: parent,
+ settings: &self.settings,
+ },
+ 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],
+ recipe,
+ scope: parent,
+ settings: &self.settings,
+ },
+ depth + argument_count,
+ )))
+ }
+ } else {
+ Ok(None)
+ }
+ }
+
fn run_recipe(
context: &RecipeContext<'src, '_>,
recipe: &Recipe<'src>,
@@ -305,7 +419,7 @@ impl<'src> Justfile<'src> {
dotenv,