Add modules (#1782)
This commit is contained in:
parent
bc628215c0
commit
316ea01295
47
README.md
47
README.md
@ -2329,7 +2329,7 @@ And will both invoke recipes `a` and `b` in `foo/justfile`.
|
|||||||
|
|
||||||
### Imports
|
### 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`:
|
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
|
Imported files can themselves contain `import`s, which are processed
|
||||||
recursively.
|
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
|
### Hiding `justfile`s
|
||||||
|
|
||||||
`just` looks for `justfile`s named `justfile` and `.justfile`, which can be used to keep a `justfile` hidden.
|
`just` looks for `justfile`s named `justfile` and `.justfile`, which can be used to keep a `justfile` hidden.
|
||||||
|
@ -13,10 +13,6 @@ pub(crate) struct Alias<'src, T = Rc<Recipe<'src>>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'src> Alias<'src, Name<'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> {
|
pub(crate) fn resolve(self, target: Rc<Recipe<'src>>) -> Alias<'src> {
|
||||||
assert_eq!(self.target.lexeme(), target.name.lexeme());
|
assert_eq!(self.target.lexeme(), target.name.lexeme());
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ pub(crate) struct Analyzer<'src> {
|
|||||||
|
|
||||||
impl<'src> Analyzer<'src> {
|
impl<'src> Analyzer<'src> {
|
||||||
pub(crate) fn analyze(
|
pub(crate) fn analyze(
|
||||||
loaded: Vec<PathBuf>,
|
loaded: &[PathBuf],
|
||||||
paths: &HashMap<PathBuf, PathBuf>,
|
paths: &HashMap<PathBuf, PathBuf>,
|
||||||
asts: &HashMap<PathBuf, Ast<'src>>,
|
asts: &HashMap<PathBuf, Ast<'src>>,
|
||||||
root: &Path,
|
root: &Path,
|
||||||
@ -19,7 +19,7 @@ impl<'src> Analyzer<'src> {
|
|||||||
|
|
||||||
fn justfile(
|
fn justfile(
|
||||||
mut self,
|
mut self,
|
||||||
loaded: Vec<PathBuf>,
|
loaded: &[PathBuf],
|
||||||
paths: &HashMap<PathBuf, PathBuf>,
|
paths: &HashMap<PathBuf, PathBuf>,
|
||||||
asts: &HashMap<PathBuf, Ast<'src>>,
|
asts: &HashMap<PathBuf, Ast<'src>>,
|
||||||
root: &Path,
|
root: &Path,
|
||||||
@ -31,11 +31,42 @@ impl<'src> Analyzer<'src> {
|
|||||||
|
|
||||||
let mut warnings = Vec::new();
|
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() {
|
while let Some(ast) = stack.pop() {
|
||||||
for item in &ast.items {
|
for item in &ast.items {
|
||||||
match item {
|
match item {
|
||||||
Item::Alias(alias) => {
|
Item::Alias(alias) => {
|
||||||
self.analyze_alias(alias)?;
|
define(alias.name, "alias", false)?;
|
||||||
|
Self::analyze_alias(alias)?;
|
||||||
self.aliases.insert(alias.clone());
|
self.aliases.insert(alias.clone());
|
||||||
}
|
}
|
||||||
Item::Assignment(assignment) => {
|
Item::Assignment(assignment) => {
|
||||||
@ -43,6 +74,19 @@ impl<'src> Analyzer<'src> {
|
|||||||
self.assignments.insert(assignment.clone());
|
self.assignments.insert(assignment.clone());
|
||||||
}
|
}
|
||||||
Item::Comment(_) => (),
|
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) => {
|
Item::Recipe(recipe) => {
|
||||||
if recipe.enabled() {
|
if recipe.enabled() {
|
||||||
Self::analyze_recipe(recipe)?;
|
Self::analyze_recipe(recipe)?;
|
||||||
@ -53,9 +97,6 @@ impl<'src> Analyzer<'src> {
|
|||||||
self.analyze_set(set)?;
|
self.analyze_set(set)?;
|
||||||
self.sets.insert(set.clone());
|
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)?;
|
AssignmentResolver::resolve_assignments(&self.assignments)?;
|
||||||
|
|
||||||
for recipe in recipes {
|
for recipe in recipes {
|
||||||
if let Some(original) = recipe_table.get(recipe.name.lexeme()) {
|
define(recipe.name, "recipe", settings.allow_duplicate_recipes)?;
|
||||||
if !settings.allow_duplicate_recipes {
|
|
||||||
return Err(recipe.name.token().error(DuplicateRecipe {
|
|
||||||
recipe: original.name(),
|
|
||||||
first: original.line_number(),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recipe_table.insert(recipe.clone());
|
recipe_table.insert(recipe.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,10 +137,14 @@ impl<'src> Analyzer<'src> {
|
|||||||
}),
|
}),
|
||||||
aliases,
|
aliases,
|
||||||
assignments: self.assignments,
|
assignments: self.assignments,
|
||||||
loaded,
|
loaded: loaded.into(),
|
||||||
recipes,
|
recipes,
|
||||||
settings,
|
settings,
|
||||||
warnings,
|
warnings,
|
||||||
|
modules: modules
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, (_name, justfile))| (name, justfile))
|
||||||
|
.collect(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,16 +202,9 @@ impl<'src> Analyzer<'src> {
|
|||||||
Ok(())
|
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();
|
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 {
|
for attr in &alias.attributes {
|
||||||
if *attr != Attribute::Private {
|
if *attr != Attribute::Private {
|
||||||
return Err(alias.name.token().error(AliasInvalidAttribute {
|
return Err(alias.name.token().error(AliasInvalidAttribute {
|
||||||
@ -232,7 +263,7 @@ mod tests {
|
|||||||
line: 1,
|
line: 1,
|
||||||
column: 6,
|
column: 6,
|
||||||
width: 3,
|
width: 3,
|
||||||
kind: DuplicateAlias { alias: "foo", first: 0 },
|
kind: Redefinition { first_type: "alias", second_type: "alias", name: "foo", first: 0 },
|
||||||
}
|
}
|
||||||
|
|
||||||
analysis_error! {
|
analysis_error! {
|
||||||
@ -248,11 +279,11 @@ mod tests {
|
|||||||
analysis_error! {
|
analysis_error! {
|
||||||
name: alias_shadows_recipe_before,
|
name: alias_shadows_recipe_before,
|
||||||
input: "bar: \n echo bar\nalias foo := bar\nfoo:\n echo foo",
|
input: "bar: \n echo bar\nalias foo := bar\nfoo:\n echo foo",
|
||||||
offset: 23,
|
offset: 34,
|
||||||
line: 2,
|
line: 3,
|
||||||
column: 6,
|
column: 0,
|
||||||
width: 3,
|
width: 3,
|
||||||
kind: AliasShadowsRecipe {alias: "foo", recipe_line: 3},
|
kind: Redefinition { first_type: "alias", second_type: "recipe", name: "foo", first: 2 },
|
||||||
}
|
}
|
||||||
|
|
||||||
analysis_error! {
|
analysis_error! {
|
||||||
@ -262,7 +293,7 @@ mod tests {
|
|||||||
line: 2,
|
line: 2,
|
||||||
column: 6,
|
column: 6,
|
||||||
width: 3,
|
width: 3,
|
||||||
kind: AliasShadowsRecipe { alias: "foo", recipe_line: 0 },
|
kind: Redefinition { first_type: "alias", second_type: "recipe", name: "foo", first: 0 },
|
||||||
}
|
}
|
||||||
|
|
||||||
analysis_error! {
|
analysis_error! {
|
||||||
@ -302,7 +333,7 @@ mod tests {
|
|||||||
line: 2,
|
line: 2,
|
||||||
column: 0,
|
column: 0,
|
||||||
width: 1,
|
width: 1,
|
||||||
kind: DuplicateRecipe{recipe: "a", first: 0},
|
kind: Redefinition { first_type: "recipe", second_type: "recipe", name: "a", first: 0 },
|
||||||
}
|
}
|
||||||
|
|
||||||
analysis_error! {
|
analysis_error! {
|
||||||
|
@ -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<'_> {
|
impl Display for CompileError<'_> {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
||||||
use CompileErrorKind::*;
|
use CompileErrorKind::*;
|
||||||
@ -82,12 +90,6 @@ impl Display for CompileError<'_> {
|
|||||||
write!(f, "at most {max} {}", Count("argument", *max))
|
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!(
|
DuplicateAttribute { attribute, first } => write!(
|
||||||
f,
|
f,
|
||||||
"Recipe attribute `{attribute}` first used on line {} is duplicated on line {}",
|
"Recipe attribute `{attribute}` first used on line {} is duplicated on line {}",
|
||||||
@ -97,12 +99,6 @@ impl Display for CompileError<'_> {
|
|||||||
DuplicateParameter { recipe, parameter } => {
|
DuplicateParameter { recipe, parameter } => {
|
||||||
write!(f, "Recipe `{recipe}` has duplicate parameter `{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!(
|
DuplicateSet { setting, first } => write!(
|
||||||
f,
|
f,
|
||||||
"Setting `{setting}` first set on line {} is redefined on line {}",
|
"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")
|
write!(f, "Parameter `{parameter}` follows variadic parameter")
|
||||||
}
|
}
|
||||||
ParsingRecursionDepthExceeded => write!(f, "Parsing recursion depth exceeded"),
|
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!(
|
RequiredParameterFollowsDefaultParameter { parameter } => write!(
|
||||||
f,
|
f,
|
||||||
"Non-default parameter `{parameter}` follows default parameter"
|
"Non-default parameter `{parameter}` follows default parameter"
|
||||||
|
@ -25,9 +25,11 @@ pub(crate) enum CompileErrorKind<'src> {
|
|||||||
min: usize,
|
min: usize,
|
||||||
max: usize,
|
max: usize,
|
||||||
},
|
},
|
||||||
DuplicateAlias {
|
Redefinition {
|
||||||
alias: &'src str,
|
|
||||||
first: usize,
|
first: usize,
|
||||||
|
first_type: &'static str,
|
||||||
|
name: &'src str,
|
||||||
|
second_type: &'static str,
|
||||||
},
|
},
|
||||||
DuplicateAttribute {
|
DuplicateAttribute {
|
||||||
attribute: &'src str,
|
attribute: &'src str,
|
||||||
@ -37,10 +39,6 @@ pub(crate) enum CompileErrorKind<'src> {
|
|||||||
recipe: &'src str,
|
recipe: &'src str,
|
||||||
parameter: &'src str,
|
parameter: &'src str,
|
||||||
},
|
},
|
||||||
DuplicateRecipe {
|
|
||||||
recipe: &'src str,
|
|
||||||
first: usize,
|
|
||||||
},
|
|
||||||
DuplicateSet {
|
DuplicateSet {
|
||||||
setting: &'src str,
|
setting: &'src str,
|
||||||
first: usize,
|
first: usize,
|
||||||
|
@ -4,6 +4,7 @@ pub(crate) struct Compiler;
|
|||||||
|
|
||||||
impl Compiler {
|
impl Compiler {
|
||||||
pub(crate) fn compile<'src>(
|
pub(crate) fn compile<'src>(
|
||||||
|
unstable: bool,
|
||||||
loader: &'src Loader,
|
loader: &'src Loader,
|
||||||
root: &Path,
|
root: &Path,
|
||||||
) -> RunResult<'src, Compilation<'src>> {
|
) -> RunResult<'src, Compilation<'src>> {
|
||||||
@ -25,20 +26,40 @@ impl Compiler {
|
|||||||
srcs.insert(current.clone(), src);
|
srcs.insert(current.clone(), src);
|
||||||
|
|
||||||
for item in &mut ast.items {
|
for item in &mut ast.items {
|
||||||
if let Item::Import { relative, absolute } = item {
|
match item {
|
||||||
let import = current.parent().unwrap().join(&relative.cooked).lexiclean();
|
Item::Mod { name, absolute } => {
|
||||||
if srcs.contains_key(&import) {
|
if !unstable {
|
||||||
return Err(Error::CircularImport { current, import });
|
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());
|
Item::Import { relative, absolute } => {
|
||||||
stack.push(import);
|
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());
|
asts.insert(current.clone(), ast.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let justfile = Analyzer::analyze(loaded, &paths, &asts, root)?;
|
let justfile = Analyzer::analyze(&loaded, &paths, &asts, root)?;
|
||||||
|
|
||||||
Ok(Compilation {
|
Ok(Compilation {
|
||||||
asts,
|
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)]
|
#[cfg(test)]
|
||||||
pub(crate) fn test_compile(src: &str) -> CompileResult<Justfile> {
|
pub(crate) fn test_compile(src: &str) -> CompileResult<Justfile> {
|
||||||
let tokens = Lexer::test_lex(src)?;
|
let tokens = Lexer::test_lex(src)?;
|
||||||
@ -57,7 +118,7 @@ impl Compiler {
|
|||||||
asts.insert(root.clone(), ast);
|
asts.insert(root.clone(), ast);
|
||||||
let mut paths: HashMap<PathBuf, PathBuf> = HashMap::new();
|
let mut paths: HashMap<PathBuf, PathBuf> = HashMap::new();
|
||||||
paths.insert(root.clone(), root.clone());
|
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 loader = Loader::new();
|
||||||
|
|
||||||
let justfile_a_path = tmp.path().join("justfile");
|
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);
|
assert_eq!(compilation.root_src(), justfile_a);
|
||||||
}
|
}
|
||||||
@ -129,7 +190,7 @@ recipe_b:
|
|||||||
let loader = Loader::new();
|
let loader = Loader::new();
|
||||||
|
|
||||||
let justfile_a_path = tmp.path().join("justfile");
|
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 }
|
assert_matches!(loader_output, Error::CircularImport { current, import }
|
||||||
if current == tmp.path().join("subdir").join("justfile_b").lexiclean() &&
|
if current == tmp.path().join("subdir").join("justfile_b").lexiclean() &&
|
||||||
|
16
src/error.rs
16
src/error.rs
@ -2,6 +2,10 @@ use super::*;
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) enum Error<'src> {
|
pub(crate) enum Error<'src> {
|
||||||
|
AmbiguousModuleFile {
|
||||||
|
module: Name<'src>,
|
||||||
|
found: Vec<String>,
|
||||||
|
},
|
||||||
ArgumentCountMismatch {
|
ArgumentCountMismatch {
|
||||||
recipe: &'src str,
|
recipe: &'src str,
|
||||||
parameters: Vec<Parameter<'src>>,
|
parameters: Vec<Parameter<'src>>,
|
||||||
@ -105,6 +109,9 @@ pub(crate) enum Error<'src> {
|
|||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
io_error: io::Error,
|
io_error: io::Error,
|
||||||
},
|
},
|
||||||
|
MissingModuleFile {
|
||||||
|
module: Name<'src>,
|
||||||
|
},
|
||||||
NoChoosableRecipes,
|
NoChoosableRecipes,
|
||||||
NoDefaultRecipe,
|
NoDefaultRecipe,
|
||||||
NoRecipes,
|
NoRecipes,
|
||||||
@ -167,6 +174,9 @@ impl<'src> Error<'src> {
|
|||||||
|
|
||||||
fn context(&self) -> Option<Token<'src>> {
|
fn context(&self) -> Option<Token<'src>> {
|
||||||
match self {
|
match self {
|
||||||
|
Self::AmbiguousModuleFile { module, .. } | Self::MissingModuleFile { module, .. } => {
|
||||||
|
Some(module.token())
|
||||||
|
}
|
||||||
Self::Backtick { token, .. } => Some(*token),
|
Self::Backtick { token, .. } => Some(*token),
|
||||||
Self::Compile { compile_error } => Some(compile_error.context()),
|
Self::Compile { compile_error } => Some(compile_error.context()),
|
||||||
Self::FunctionCall { function, .. } => Some(function.token()),
|
Self::FunctionCall { function, .. } => Some(function.token()),
|
||||||
@ -224,6 +234,11 @@ impl<'src> ColorDisplay for Error<'src> {
|
|||||||
write!(f, "{error}: {message}")?;
|
write!(f, "{error}: {message}")?;
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
|
AmbiguousModuleFile { module, found } =>
|
||||||
|
write!(f,
|
||||||
|
"Found multiple source files for module `{module}`: {}",
|
||||||
|
List::and_ticked(found),
|
||||||
|
)?,
|
||||||
ArgumentCountMismatch { recipe, found, min, max, .. } => {
|
ArgumentCountMismatch { recipe, found, min, max, .. } => {
|
||||||
let count = Count("argument", *found);
|
let count = Count("argument", *found);
|
||||||
if min == max {
|
if min == max {
|
||||||
@ -350,6 +365,7 @@ impl<'src> ColorDisplay for Error<'src> {
|
|||||||
let path = path.display();
|
let path = path.display();
|
||||||
write!(f, "Failed to read justfile at `{path}`: {io_error}")?;
|
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.")?,
|
NoChoosableRecipes => write!(f, "Justfile contains no choosable recipes.")?,
|
||||||
NoDefaultRecipe => write!(f, "Justfile contains no default recipe.")?,
|
NoDefaultRecipe => write!(f, "Justfile contains no default recipe.")?,
|
||||||
NoRecipes => write!(f, "Justfile contains no recipes.")?,
|
NoRecipes => write!(f, "Justfile contains no recipes.")?,
|
||||||
|
11
src/item.rs
11
src/item.rs
@ -6,12 +6,16 @@ pub(crate) enum Item<'src> {
|
|||||||
Alias(Alias<'src, Name<'src>>),
|
Alias(Alias<'src, Name<'src>>),
|
||||||
Assignment(Assignment<'src>),
|
Assignment(Assignment<'src>),
|
||||||
Comment(&'src str),
|
Comment(&'src str),
|
||||||
Recipe(UnresolvedRecipe<'src>),
|
|
||||||
Set(Set<'src>),
|
|
||||||
Import {
|
Import {
|
||||||
relative: StringLiteral<'src>,
|
relative: StringLiteral<'src>,
|
||||||
absolute: Option<PathBuf>,
|
absolute: Option<PathBuf>,
|
||||||
},
|
},
|
||||||
|
Mod {
|
||||||
|
name: Name<'src>,
|
||||||
|
absolute: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
Recipe(UnresolvedRecipe<'src>),
|
||||||
|
Set(Set<'src>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'src> Display for Item<'src> {
|
impl<'src> Display for Item<'src> {
|
||||||
@ -20,9 +24,10 @@ impl<'src> Display for Item<'src> {
|
|||||||
Item::Alias(alias) => write!(f, "{alias}"),
|
Item::Alias(alias) => write!(f, "{alias}"),
|
||||||
Item::Assignment(assignment) => write!(f, "{assignment}"),
|
Item::Assignment(assignment) => write!(f, "{assignment}"),
|
||||||
Item::Comment(comment) => write!(f, "{comment}"),
|
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::Recipe(recipe) => write!(f, "{}", recipe.color_display(Color::never())),
|
||||||
Item::Set(set) => write!(f, "{set}"),
|
Item::Set(set) => write!(f, "{set}"),
|
||||||
Item::Import { relative, .. } => write!(f, "import {relative}"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
248
src/justfile.rs
248
src/justfile.rs
@ -1,5 +1,13 @@
|
|||||||
use {super::*, serde::Serialize};
|
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)]
|
#[derive(Debug, PartialEq, Serialize)]
|
||||||
pub(crate) struct Justfile<'src> {
|
pub(crate) struct Justfile<'src> {
|
||||||
pub(crate) aliases: Table<'src, Alias<'src>>,
|
pub(crate) aliases: Table<'src, Alias<'src>>,
|
||||||
@ -8,6 +16,7 @@ pub(crate) struct Justfile<'src> {
|
|||||||
pub(crate) default: Option<Rc<Recipe<'src>>>,
|
pub(crate) default: Option<Rc<Recipe<'src>>>,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub(crate) loaded: Vec<PathBuf>,
|
pub(crate) loaded: Vec<PathBuf>,
|
||||||
|
pub(crate) modules: BTreeMap<String, Justfile<'src>>,
|
||||||
pub(crate) recipes: Table<'src, Rc<Recipe<'src>>>,
|
pub(crate) recipes: Table<'src, Rc<Recipe<'src>>>,
|
||||||
pub(crate) settings: Settings<'src>,
|
pub(crate) settings: Settings<'src>,
|
||||||
pub(crate) warnings: Vec<Warning>,
|
pub(crate) warnings: Vec<Warning>,
|
||||||
@ -67,6 +76,44 @@ impl<'src> Justfile<'src> {
|
|||||||
.next()
|
.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(
|
pub(crate) fn run(
|
||||||
&self,
|
&self,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
@ -92,33 +139,9 @@ impl<'src> Justfile<'src> {
|
|||||||
BTreeMap::new()
|
BTreeMap::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let scope = {
|
let root = Scope::new();
|
||||||
let mut scope = Scope::new();
|
|
||||||
let mut unknown_overrides = Vec::new();
|
|
||||||
|
|
||||||
for (name, value) in overrides {
|
let scope = self.scope(config, &dotenv, search, overrides, &root)?;
|
||||||
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,
|
|
||||||
)?
|
|
||||||
};
|
|
||||||
|
|
||||||
match &config.subcommand {
|
match &config.subcommand {
|
||||||
Subcommand::Command {
|
Subcommand::Command {
|
||||||
@ -193,13 +216,7 @@ impl<'src> Justfile<'src> {
|
|||||||
let argvec: Vec<&str> = if !arguments.is_empty() {
|
let argvec: Vec<&str> = if !arguments.is_empty() {
|
||||||
arguments.iter().map(String::as_str).collect()
|
arguments.iter().map(String::as_str).collect()
|
||||||
} else if let Some(recipe) = &self.default {
|
} else if let Some(recipe) = &self.default {
|
||||||
let min_arguments = recipe.min_arguments();
|
recipe.check_can_be_default_recipe()?;
|
||||||
if min_arguments > 0 {
|
|
||||||
return Err(Error::DefaultRecipeRequiresArguments {
|
|
||||||
recipe: recipe.name.lexeme(),
|
|
||||||
min_arguments,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
vec![recipe.name()]
|
vec![recipe.name()]
|
||||||
} else if self.recipes.is_empty() {
|
} else if self.recipes.is_empty() {
|
||||||
return Err(Error::NoRecipes);
|
return Err(Error::NoRecipes);
|
||||||
@ -209,33 +226,31 @@ impl<'src> Justfile<'src> {
|
|||||||
|
|
||||||
let arguments = argvec.as_slice();
|
let arguments = argvec.as_slice();
|
||||||
|
|
||||||
let mut missing = vec![];
|
let mut missing = Vec::new();
|
||||||
let mut grouped = vec![];
|
let mut invocations = Vec::new();
|
||||||
let mut rest = arguments;
|
let mut remaining = arguments;
|
||||||
|
let mut scopes = BTreeMap::new();
|
||||||
|
let arena: Arena<Scope> = Arena::new();
|
||||||
|
|
||||||
while let Some((argument, mut tail)) = rest.split_first() {
|
while let Some((first, mut rest)) = remaining.split_first() {
|
||||||
if let Some(recipe) = self.get_recipe(argument) {
|
if let Some((invocation, consumed)) = self.invocation(
|
||||||
if recipe.parameters.is_empty() {
|
0,
|
||||||
grouped.push((recipe, &[][..]));
|
&mut Vec::new(),
|
||||||
} else {
|
&arena,
|
||||||
let argument_range = recipe.argument_range();
|
&mut scopes,
|
||||||
let argument_count = cmp::min(tail.len(), recipe.max_arguments());
|
config,
|
||||||
if !argument_range.range_contains(&argument_count) {
|
&dotenv,
|
||||||
return Err(Error::ArgumentCountMismatch {
|
search,
|
||||||
recipe: recipe.name(),
|
&scope,
|
||||||
parameters: recipe.parameters.clone(),
|
first,
|
||||||
found: tail.len(),
|
rest,
|
||||||
min: recipe.min_arguments(),
|
)? {
|
||||||
max: recipe.max_arguments(),
|
rest = &rest[consumed..];
|
||||||
});
|
invocations.push(invocation);
|
||||||
}
|
|
||||||
grouped.push((recipe, &tail[0..argument_count]));
|
|
||||||
tail = &tail[argument_count..];
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
missing.push((*argument).to_owned());
|
missing.push((*first).to_owned());
|
||||||
}
|
}
|
||||||
rest = tail;
|
remaining = rest;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !missing.is_empty() {
|
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();
|
let mut ran = BTreeSet::new();
|
||||||
for (recipe, arguments) in grouped {
|
for invocation in invocations {
|
||||||
Self::run_recipe(&context, recipe, arguments, &dotenv, search, &mut ran)?;
|
let context = RecipeContext {
|
||||||
|
settings: invocation.settings,
|
||||||
|
config,
|
||||||
|
scope: invocation.scope,
|
||||||
|
search,
|
||||||
|
};
|
||||||
|
|
||||||
|
Self::run_recipe(
|
||||||
|
&context,
|
||||||
|
invocation.recipe,
|
||||||
|
invocation.arguments,
|
||||||
|
&dotenv,
|
||||||
|
search,
|
||||||
|
&mut ran,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -277,6 +299,98 @@ impl<'src> Justfile<'src> {
|
|||||||
.or_else(|| self.aliases.get(name).map(|alias| alias.target.as_ref()))
|
.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(
|
fn run_recipe(
|
||||||
context: &RecipeContext<'src, '_>,
|
context: &RecipeContext<'src, '_>,
|
||||||
recipe: &Recipe<'src>,
|
recipe: &Recipe<'src>,
|
||||||
@ -305,7 +419,7 @@ impl<'src> Justfile<'src> {
|
|||||||
dotenv,
|
dotenv,
|
||||||
&recipe.parameters,
|
&recipe.parameters,
|
||||||
arguments,
|
arguments,
|
||||||
&context.scope,
|
context.scope,
|
||||||
context.settings,
|
context.settings,
|
||||||
search,
|
search,
|
||||||
)?;
|
)?;
|
||||||
|
@ -15,6 +15,7 @@ pub(crate) enum Keyword {
|
|||||||
If,
|
If,
|
||||||
IgnoreComments,
|
IgnoreComments,
|
||||||
Import,
|
Import,
|
||||||
|
Mod,
|
||||||
PositionalArguments,
|
PositionalArguments,
|
||||||
Set,
|
Set,
|
||||||
Shell,
|
Shell,
|
||||||
|
@ -21,9 +21,10 @@ impl<'src> Node<'src> for Item<'src> {
|
|||||||
Item::Alias(alias) => alias.tree(),
|
Item::Alias(alias) => alias.tree(),
|
||||||
Item::Assignment(assignment) => assignment.tree(),
|
Item::Assignment(assignment) => assignment.tree(),
|
||||||
Item::Comment(comment) => comment.tree(),
|
Item::Comment(comment) => comment.tree(),
|
||||||
|
Item::Import { relative, .. } => Tree::atom("import").push(format!("{relative}")),
|
||||||
|
Item::Mod { name, .. } => Tree::atom("mod").push(name.lexeme()),
|
||||||
Item::Recipe(recipe) => recipe.tree(),
|
Item::Recipe(recipe) => recipe.tree(),
|
||||||
Item::Set(set) => set.tree(),
|
Item::Set(set) => set.tree(),
|
||||||
Item::Import { relative, .. } => Tree::atom("import").push(format!("{relative}")),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -335,6 +335,13 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
absolute: None,
|
absolute: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Some(Keyword::Mod) if self.next_are(&[Identifier, Identifier]) => {
|
||||||
|
self.presume_keyword(Keyword::Mod)?;
|
||||||
|
items.push(Item::Mod {
|
||||||
|
name: self.parse_name()?,
|
||||||
|
absolute: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
Some(Keyword::Set)
|
Some(Keyword::Set)
|
||||||
if self.next_are(&[Identifier, Identifier, ColonEquals])
|
if self.next_are(&[Identifier, Identifier, ColonEquals])
|
||||||
|| self.next_are(&[Identifier, Identifier, Comment, Eof])
|
|| self.next_are(&[Identifier, Identifier, Comment, Eof])
|
||||||
|
@ -77,6 +77,18 @@ impl<'src, D> Recipe<'src, D> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn check_can_be_default_recipe(&self) -> RunResult<'src, ()> {
|
||||||
|
let min_arguments = self.min_arguments();
|
||||||
|
if min_arguments > 0 {
|
||||||
|
return Err(Error::DefaultRecipeRequiresArguments {
|
||||||
|
recipe: self.name.lexeme(),
|
||||||
|
min_arguments,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn public(&self) -> bool {
|
pub(crate) fn public(&self) -> bool {
|
||||||
!self.private && !self.attributes.contains(&Attribute::Private)
|
!self.private && !self.attributes.contains(&Attribute::Private)
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ use super::*;
|
|||||||
|
|
||||||
pub(crate) struct RecipeContext<'src: 'run, 'run> {
|
pub(crate) struct RecipeContext<'src: 'run, 'run> {
|
||||||
pub(crate) config: &'run Config,
|
pub(crate) config: &'run Config,
|
||||||
pub(crate) scope: Scope<'src, 'run>,
|
pub(crate) scope: &'run Scope<'src, 'run>,
|
||||||
pub(crate) search: &'run Search,
|
pub(crate) search: &'run Search,
|
||||||
pub(crate) settings: &'run Settings<'src>,
|
pub(crate) settings: &'run Settings<'src>,
|
||||||
}
|
}
|
||||||
|
@ -8,14 +8,14 @@ pub(crate) struct Scope<'src: 'run, 'run> {
|
|||||||
|
|
||||||
impl<'src, 'run> Scope<'src, 'run> {
|
impl<'src, 'run> Scope<'src, 'run> {
|
||||||
pub(crate) fn child(&'run self) -> Scope<'src, 'run> {
|
pub(crate) fn child(&'run self) -> Scope<'src, 'run> {
|
||||||
Scope {
|
Self {
|
||||||
parent: Some(self),
|
parent: Some(self),
|
||||||
bindings: Table::new(),
|
bindings: Table::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new() -> Scope<'src, 'run> {
|
pub(crate) fn new() -> Scope<'src, 'run> {
|
||||||
Scope {
|
Self {
|
||||||
parent: None,
|
parent: None,
|
||||||
bindings: Table::new(),
|
bindings: Table::new(),
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use {super::*, std::path::Component};
|
use {super::*, std::path::Component};
|
||||||
|
|
||||||
const DEFAULT_JUSTFILE_NAME: &str = JUSTFILE_NAMES[0];
|
const DEFAULT_JUSTFILE_NAME: &str = JUSTFILE_NAMES[0];
|
||||||
const JUSTFILE_NAMES: &[&str] = &["justfile", ".justfile"];
|
pub(crate) const JUSTFILE_NAMES: &[&str] = &["justfile", ".justfile"];
|
||||||
const PROJECT_ROOT_CHILDREN: &[&str] = &[".bzr", ".git", ".hg", ".svn", "_darcs"];
|
const PROJECT_ROOT_CHILDREN: &[&str] = &[".bzr", ".git", ".hg", ".svn", "_darcs"];
|
||||||
|
|
||||||
pub(crate) struct Search {
|
pub(crate) struct Search {
|
||||||
@ -109,7 +109,7 @@ impl Search {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn justfile(directory: &Path) -> SearchResult<PathBuf> {
|
pub(crate) fn justfile(directory: &Path) -> SearchResult<PathBuf> {
|
||||||
for directory in directory.ancestors() {
|
for directory in directory.ancestors() {
|
||||||
let mut candidates = BTreeSet::new();
|
let mut candidates = BTreeSet::new();
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ impl Subcommand {
|
|||||||
}
|
}
|
||||||
Dump => Self::dump(config, ast, justfile)?,
|
Dump => Self::dump(config, ast, justfile)?,
|
||||||
Format => Self::format(config, &search, src, ast)?,
|
Format => Self::format(config, &search, src, ast)?,
|
||||||
List => Self::list(config, justfile),
|
List => Self::list(config, 0, justfile),
|
||||||
Show { ref name } => Self::show(config, name, justfile)?,
|
Show { ref name } => Self::show(config, name, justfile)?,
|
||||||
Summary => Self::summary(config, justfile),
|
Summary => Self::summary(config, justfile),
|
||||||
Variables => Self::variables(justfile),
|
Variables => Self::variables(justfile),
|
||||||
@ -180,7 +180,7 @@ impl Subcommand {
|
|||||||
loader: &'src Loader,
|
loader: &'src Loader,
|
||||||
search: &Search,
|
search: &Search,
|
||||||
) -> Result<Compilation<'src>, Error<'src>> {
|
) -> Result<Compilation<'src>, Error<'src>> {
|
||||||
let compilation = Compiler::compile(loader, &search.justfile)?;
|
let compilation = Compiler::compile(config.unstable, loader, &search.justfile)?;
|
||||||
|
|
||||||
if config.verbosity.loud() {
|
if config.verbosity.loud() {
|
||||||
for warning in &compilation.justfile.warnings {
|
for warning in &compilation.justfile.warnings {
|
||||||
@ -426,7 +426,7 @@ impl Subcommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list(config: &Config, justfile: &Justfile) {
|
fn list(config: &Config, level: usize, justfile: &Justfile) {
|
||||||
// Construct a target to alias map.
|
// Construct a target to alias map.
|
||||||
let mut recipe_aliases: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
|
let mut recipe_aliases: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
|
||||||
for alias in justfile.aliases.values() {
|
for alias in justfile.aliases.values() {
|
||||||
@ -465,9 +465,11 @@ impl Subcommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let max_line_width = cmp::min(line_widths.values().copied().max().unwrap_or(0), 30);
|
let max_line_width = cmp::min(line_widths.values().copied().max().unwrap_or(0), 30);
|
||||||
|
|
||||||
let doc_color = config.color.stdout().doc();
|
let doc_color = config.color.stdout().doc();
|
||||||
print!("{}", config.list_heading);
|
|
||||||
|
if level == 0 {
|
||||||
|
print!("{}", config.list_heading);
|
||||||
|
}
|
||||||
|
|
||||||
for recipe in justfile.public_recipes(config.unsorted) {
|
for recipe in justfile.public_recipes(config.unsorted) {
|
||||||
let name = recipe.name();
|
let name = recipe.name();
|
||||||
@ -476,7 +478,7 @@ impl Subcommand {
|
|||||||
.chain(recipe_aliases.get(name).unwrap_or(&Vec::new()))
|
.chain(recipe_aliases.get(name).unwrap_or(&Vec::new()))
|
||||||
.enumerate()
|
.enumerate()
|
||||||
{
|
{
|
||||||
print!("{}{name}", config.list_prefix);
|
print!("{}{name}", config.list_prefix.repeat(level + 1));
|
||||||
for parameter in &recipe.parameters {
|
for parameter in &recipe.parameters {
|
||||||
print!(" {}", parameter.color_display(config.color.stdout()));
|
print!(" {}", parameter.color_display(config.color.stdout()));
|
||||||
}
|
}
|
||||||
@ -506,6 +508,11 @@ impl Subcommand {
|
|||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (name, module) in &justfile.modules {
|
||||||
|
println!(" {name}:");
|
||||||
|
Self::list(config, level + 1, module);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show<'src>(config: &Config, name: &str, justfile: &Justfile<'src>) -> Result<(), Error<'src>> {
|
fn show<'src>(config: &Config, name: &str, justfile: &Justfile<'src>) -> Result<(), Error<'src>> {
|
||||||
|
@ -28,7 +28,7 @@ mod full {
|
|||||||
pub fn summary(path: &Path) -> Result<Result<Summary, String>, io::Error> {
|
pub fn summary(path: &Path) -> Result<Result<Summary, String>, io::Error> {
|
||||||
let loader = Loader::new();
|
let loader = Loader::new();
|
||||||
|
|
||||||
match Compiler::compile(&loader, path) {
|
match Compiler::compile(false, &loader, path) {
|
||||||
Ok(compilation) => Ok(Ok(Summary::new(&compilation.justfile))),
|
Ok(compilation) => Ok(Ok(Summary::new(&compilation.justfile))),
|
||||||
Err(error) => Ok(Err(if let Error::Compile { compile_error } = error {
|
Err(error) => Ok(Err(if let Error::Compile { compile_error } = error {
|
||||||
compile_error.to_string()
|
compile_error.to_string()
|
||||||
|
@ -68,7 +68,7 @@ pub(crate) fn analysis_error(
|
|||||||
let mut paths: HashMap<PathBuf, PathBuf> = HashMap::new();
|
let mut paths: HashMap<PathBuf, PathBuf> = HashMap::new();
|
||||||
paths.insert("justfile".into(), "justfile".into());
|
paths.insert("justfile".into(), "justfile".into());
|
||||||
|
|
||||||
match Analyzer::analyze(Vec::new(), &paths, &asts, &root) {
|
match Analyzer::analyze(&[], &paths, &asts, &root) {
|
||||||
Ok(_) => panic!("Analysis unexpectedly succeeded"),
|
Ok(_) => panic!("Analysis unexpectedly succeeded"),
|
||||||
Err(have) => {
|
Err(have) => {
|
||||||
let want = CompileError {
|
let want = CompileError {
|
||||||
|
128
tests/json.rs
128
tests/json.rs
@ -1,6 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn test(justfile: &str, value: Value) {
|
fn case(justfile: &str, value: Value) {
|
||||||
Test::new()
|
Test::new()
|
||||||
.justfile(justfile)
|
.justfile(justfile)
|
||||||
.args(["--dump", "--dump-format", "json", "--unstable"])
|
.args(["--dump", "--dump-format", "json", "--unstable"])
|
||||||
@ -10,7 +10,7 @@ fn test(justfile: &str, value: Value) {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn alias() {
|
fn alias() {
|
||||||
test(
|
case(
|
||||||
"
|
"
|
||||||
alias f := foo
|
alias f := foo
|
||||||
|
|
||||||
@ -26,6 +26,7 @@ fn alias() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"assignments": {},
|
"assignments": {},
|
||||||
|
"modules": {},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"foo": {
|
"foo": {
|
||||||
"attributes": [],
|
"attributes": [],
|
||||||
@ -61,7 +62,7 @@ fn alias() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn assignment() {
|
fn assignment() {
|
||||||
test(
|
case(
|
||||||
"foo := 'bar'",
|
"foo := 'bar'",
|
||||||
json!({
|
json!({
|
||||||
"aliases": {},
|
"aliases": {},
|
||||||
@ -73,6 +74,7 @@ fn assignment() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"first": null,
|
"first": null,
|
||||||
|
"modules": {},
|
||||||
"recipes": {},
|
"recipes": {},
|
||||||
"settings": {
|
"settings": {
|
||||||
"allow_duplicate_recipes": false,
|
"allow_duplicate_recipes": false,
|
||||||
@ -95,7 +97,7 @@ fn assignment() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn body() {
|
fn body() {
|
||||||
test(
|
case(
|
||||||
"
|
"
|
||||||
foo:
|
foo:
|
||||||
bar
|
bar
|
||||||
@ -105,6 +107,7 @@ fn body() {
|
|||||||
"aliases": {},
|
"aliases": {},
|
||||||
"assignments": {},
|
"assignments": {},
|
||||||
"first": "foo",
|
"first": "foo",
|
||||||
|
"modules": {},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"foo": {
|
"foo": {
|
||||||
"attributes": [],
|
"attributes": [],
|
||||||
@ -143,7 +146,7 @@ fn body() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dependencies() {
|
fn dependencies() {
|
||||||
test(
|
case(
|
||||||
"
|
"
|
||||||
foo:
|
foo:
|
||||||
bar: foo
|
bar: foo
|
||||||
@ -152,6 +155,7 @@ fn dependencies() {
|
|||||||
"aliases": {},
|
"aliases": {},
|
||||||
"assignments": {},
|
"assignments": {},
|
||||||
"first": "foo",
|
"first": "foo",
|
||||||
|
"modules": {},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"bar": {
|
"bar": {
|
||||||
"attributes": [],
|
"attributes": [],
|
||||||
@ -202,7 +206,7 @@ fn dependencies() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dependency_argument() {
|
fn dependency_argument() {
|
||||||
test(
|
case(
|
||||||
"
|
"
|
||||||
x := 'foo'
|
x := 'foo'
|
||||||
foo *args:
|
foo *args:
|
||||||
@ -230,6 +234,7 @@ fn dependency_argument() {
|
|||||||
"value": "foo",
|
"value": "foo",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"modules": {},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"bar": {
|
"bar": {
|
||||||
"doc": null,
|
"doc": null,
|
||||||
@ -298,7 +303,7 @@ fn dependency_argument() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn duplicate_recipes() {
|
fn duplicate_recipes() {
|
||||||
test(
|
case(
|
||||||
"
|
"
|
||||||
set allow-duplicate-recipes
|
set allow-duplicate-recipes
|
||||||
alias f := foo
|
alias f := foo
|
||||||
@ -316,6 +321,7 @@ fn duplicate_recipes() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"assignments": {},
|
"assignments": {},
|
||||||
|
"modules": {},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"foo": {
|
"foo": {
|
||||||
"body": [],
|
"body": [],
|
||||||
@ -358,12 +364,13 @@ fn duplicate_recipes() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn doc_comment() {
|
fn doc_comment() {
|
||||||
test(
|
case(
|
||||||
"# hello\nfoo:",
|
"# hello\nfoo:",
|
||||||
json!({
|
json!({
|
||||||
"aliases": {},
|
"aliases": {},
|
||||||
"first": "foo",
|
"first": "foo",
|
||||||
"assignments": {},
|
"assignments": {},
|
||||||
|
"modules": {},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"foo": {
|
"foo": {
|
||||||
"body": [],
|
"body": [],
|
||||||
@ -399,12 +406,13 @@ fn doc_comment() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_justfile() {
|
fn empty_justfile() {
|
||||||
test(
|
case(
|
||||||
"",
|
"",
|
||||||
json!({
|
json!({
|
||||||
"aliases": {},
|
"aliases": {},
|
||||||
"assignments": {},
|
"assignments": {},
|
||||||
"first": null,
|
"first": null,
|
||||||
|
"modules": {},
|
||||||
"recipes": {},
|
"recipes": {},
|
||||||
"settings": {
|
"settings": {
|
||||||
"allow_duplicate_recipes": false,
|
"allow_duplicate_recipes": false,
|
||||||
@ -427,7 +435,7 @@ fn empty_justfile() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parameters() {
|
fn parameters() {
|
||||||
test(
|
case(
|
||||||
"
|
"
|
||||||
a:
|
a:
|
||||||
b x:
|
b x:
|
||||||
@ -440,6 +448,7 @@ fn parameters() {
|
|||||||
"aliases": {},
|
"aliases": {},
|
||||||
"first": "a",
|
"first": "a",
|
||||||
"assignments": {},
|
"assignments": {},
|
||||||
|
"modules": {},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"a": {
|
"a": {
|
||||||
"attributes": [],
|
"attributes": [],
|
||||||
@ -570,7 +579,7 @@ fn parameters() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn priors() {
|
fn priors() {
|
||||||
test(
|
case(
|
||||||
"
|
"
|
||||||
a:
|
a:
|
||||||
b: a && c
|
b: a && c
|
||||||
@ -580,6 +589,7 @@ fn priors() {
|
|||||||
"aliases": {},
|
"aliases": {},
|
||||||
"assignments": {},
|
"assignments": {},
|
||||||
"first": "a",
|
"first": "a",
|
||||||
|
"modules": {},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"a": {
|
"a": {
|
||||||
"body": [],
|
"body": [],
|
||||||
@ -649,12 +659,13 @@ fn priors() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn private() {
|
fn private() {
|
||||||
test(
|
case(
|
||||||
"_foo:",
|
"_foo:",
|
||||||
json!({
|
json!({
|
||||||
"aliases": {},
|
"aliases": {},
|
||||||
"assignments": {},
|
"assignments": {},
|
||||||
"first": "_foo",
|
"first": "_foo",
|
||||||
|
"modules": {},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"_foo": {
|
"_foo": {
|
||||||
"body": [],
|
"body": [],
|
||||||
@ -690,12 +701,13 @@ fn private() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quiet() {
|
fn quiet() {
|
||||||
test(
|
case(
|
||||||
"@foo:",
|
"@foo:",
|
||||||
json!({
|
json!({
|
||||||
"aliases": {},
|
"aliases": {},
|
||||||
"assignments": {},
|
"assignments": {},
|
||||||
"first": "foo",
|
"first": "foo",
|
||||||
|
"modules": {},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"foo": {
|
"foo": {
|
||||||
"body": [],
|
"body": [],
|
||||||
@ -731,7 +743,7 @@ fn quiet() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn settings() {
|
fn settings() {
|
||||||
test(
|
case(
|
||||||
"
|
"
|
||||||
set dotenv-load
|
set dotenv-load
|
||||||
set dotenv-filename := \"filename\"
|
set dotenv-filename := \"filename\"
|
||||||
@ -748,6 +760,7 @@ fn settings() {
|
|||||||
"aliases": {},
|
"aliases": {},
|
||||||
"assignments": {},
|
"assignments": {},
|
||||||
"first": "foo",
|
"first": "foo",
|
||||||
|
"modules": {},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"foo": {
|
"foo": {
|
||||||
"body": [["#!bar"]],
|
"body": [["#!bar"]],
|
||||||
@ -786,7 +799,7 @@ fn settings() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn shebang() {
|
fn shebang() {
|
||||||
test(
|
case(
|
||||||
"
|
"
|
||||||
foo:
|
foo:
|
||||||
#!bar
|
#!bar
|
||||||
@ -795,6 +808,7 @@ fn shebang() {
|
|||||||
"aliases": {},
|
"aliases": {},
|
||||||
"assignments": {},
|
"assignments": {},
|
||||||
"first": "foo",
|
"first": "foo",
|
||||||
|
"modules": {},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"foo": {
|
"foo": {
|
||||||
"body": [["#!bar"]],
|
"body": [["#!bar"]],
|
||||||
@ -830,12 +844,13 @@ fn shebang() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn simple() {
|
fn simple() {
|
||||||
test(
|
case(
|
||||||
"foo:",
|
"foo:",
|
||||||
json!({
|
json!({
|
||||||
"aliases": {},
|
"aliases": {},
|
||||||
"assignments": {},
|
"assignments": {},
|
||||||
"first": "foo",
|
"first": "foo",
|
||||||
|
"modules": {},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"foo": {
|
"foo": {
|
||||||
"body": [],
|
"body": [],
|
||||||
@ -871,7 +886,7 @@ fn simple() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn attribute() {
|
fn attribute() {
|
||||||
test(
|
case(
|
||||||
"
|
"
|
||||||
[no-exit-message]
|
[no-exit-message]
|
||||||
foo:
|
foo:
|
||||||
@ -880,6 +895,7 @@ fn attribute() {
|
|||||||
"aliases": {},
|
"aliases": {},
|
||||||
"assignments": {},
|
"assignments": {},
|
||||||
"first": "foo",
|
"first": "foo",
|
||||||
|
"modules": {},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"foo": {
|
"foo": {
|
||||||
"attributes": ["no-exit-message"],
|
"attributes": ["no-exit-message"],
|
||||||
@ -912,3 +928,81 @@ fn attribute() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn module() {
|
||||||
|
Test::new()
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.tree(tree! {
|
||||||
|
"foo.just": "bar:",
|
||||||
|
})
|
||||||
|
.args(["--dump", "--dump-format", "json", "--unstable"])
|
||||||
|
.test_round_trip(false)
|
||||||
|
.stdout(format!(
|
||||||
|
"{}\n",
|
||||||
|
serde_json::to_string(&json!({
|
||||||
|
"aliases": {},
|
||||||
|
"assignments": {},
|
||||||
|
"first": null,
|
||||||
|
"modules": {
|
||||||
|
"foo": {
|
||||||
|
"aliases": {},
|
||||||
|
"assignments": {},
|
||||||
|
"first": "bar",
|
||||||
|
"modules": {},
|
||||||
|
"recipes": {
|
||||||
|
"bar": {
|
||||||
|
"attributes": [],
|
||||||
|
"body": [],
|
||||||
|
"dependencies": [],
|
||||||
|
"doc": null,
|
||||||
|
"name": "bar",
|
||||||
|
"parameters": [],
|
||||||
|
"priors": 0,
|
||||||
|
"private": false,
|
||||||
|
"quiet": false,
|
||||||
|
"shebang": false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"allow_duplicate_recipes": false,
|
||||||
|
"dotenv_filename": null,
|
||||||
|
"dotenv_load": null,
|
||||||
|
"dotenv_path": null,
|
||||||
|
"export": false,
|
||||||
|
"fallback": false,
|
||||||
|
"positional_arguments": false,
|
||||||
|
"shell": null,
|
||||||
|
"tempdir" : null,
|
||||||
|
"ignore_comments": false,
|
||||||
|
"windows_powershell": false,
|
||||||
|
"windows_shell": null,
|
||||||
|
},
|
||||||
|
"warnings": [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"recipes": {},
|
||||||
|
"settings": {
|
||||||
|
"allow_duplicate_recipes": false,
|
||||||
|
"dotenv_filename": null,
|
||||||
|
"dotenv_load": null,
|
||||||
|
"dotenv_path": null,
|
||||||
|
"export": false,
|
||||||
|
"fallback": false,
|
||||||
|
"positional_arguments": false,
|
||||||
|
"shell": null,
|
||||||
|
"tempdir" : null,
|
||||||
|
"ignore_comments": false,
|
||||||
|
"windows_powershell": false,
|
||||||
|
"windows_shell": null,
|
||||||
|
},
|
||||||
|
"warnings": [],
|
||||||
|
}))
|
||||||
|
.unwrap()
|
||||||
|
))
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
@ -63,6 +63,7 @@ mod invocation_directory;
|
|||||||
mod json;
|
mod json;
|
||||||
mod line_prefixes;
|
mod line_prefixes;
|
||||||
mod misc;
|
mod misc;
|
||||||
|
mod modules;
|
||||||
mod multibyte_char;
|
mod multibyte_char;
|
||||||
mod newline_escape;
|
mod newline_escape;
|
||||||
mod no_cd;
|
mod no_cd;
|
||||||
|
@ -133,11 +133,11 @@ test! {
|
|||||||
name: alias_shadows_recipe,
|
name: alias_shadows_recipe,
|
||||||
justfile: "bar:\n echo bar\nalias foo := bar\nfoo:\n echo foo",
|
justfile: "bar:\n echo bar\nalias foo := bar\nfoo:\n echo foo",
|
||||||
stderr: "
|
stderr: "
|
||||||
error: Alias `foo` defined on line 3 shadows recipe `foo` defined on line 4
|
error: Alias `foo` defined on line 3 is redefined as a recipe on line 4
|
||||||
--> justfile:3:7
|
--> justfile:4:1
|
||||||
|
|
|
|
||||||
3 | alias foo := bar
|
4 | foo:
|
||||||
| ^^^
|
| ^^^
|
||||||
",
|
",
|
||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
}
|
}
|
||||||
|
446
tests/modules.rs
Normal file
446
tests/modules.rs
Normal file
@ -0,0 +1,446 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modules_are_unstable() {
|
||||||
|
Test::new()
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.arg("foo")
|
||||||
|
.arg("foo")
|
||||||
|
.stderr(
|
||||||
|
"error: Modules are currently unstable. \
|
||||||
|
Invoke `just` with the `--unstable` flag to enable unstable features.\n",
|
||||||
|
)
|
||||||
|
.status(EXIT_FAILURE)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_recipe_in_submodule_must_have_no_arguments() {
|
||||||
|
Test::new()
|
||||||
|
.write("foo.just", "foo bar:\n @echo FOO")
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.arg("--unstable")
|
||||||
|
.arg("foo")
|
||||||
|
.stderr("error: Recipe `foo` cannot be used as default recipe since it requires at least 1 argument.\n")
|
||||||
|
.status(EXIT_FAILURE)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn module_recipes_can_be_run_as_subcommands() {
|
||||||
|
Test::new()
|
||||||
|
.write("foo.just", "foo:\n @echo FOO")
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.arg("--unstable")
|
||||||
|
.arg("foo")
|
||||||
|
.arg("foo")
|
||||||
|
.stdout("FOO\n")
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn assignments_are_evaluated_in_modules() {
|
||||||
|
Test::new()
|
||||||
|
.write("foo.just", "bar := 'CHILD'\nfoo:\n @echo {{bar}}")
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
bar := 'PARENT'
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.arg("--unstable")
|
||||||
|
.arg("foo")
|
||||||
|
.arg("foo")
|
||||||
|
.stdout("CHILD\n")
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn module_subcommand_runs_default_recipe() {
|
||||||
|
Test::new()
|
||||||
|
.write("foo.just", "foo:\n @echo FOO")
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.arg("--unstable")
|
||||||
|
.arg("foo")
|
||||||
|
.stdout("FOO\n")
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modules_can_contain_other_modules() {
|
||||||
|
Test::new()
|
||||||
|
.write("bar.just", "baz:\n @echo BAZ")
|
||||||
|
.write("foo.just", "mod bar")
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.arg("--unstable")
|
||||||
|
.arg("foo")
|
||||||
|
.arg("bar")
|
||||||
|
.arg("baz")
|
||||||
|
.stdout("BAZ\n")
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn circular_module_imports_are_detected() {
|
||||||
|
Test::new()
|
||||||
|
.write("bar.just", "mod foo")
|
||||||
|
.write("foo.just", "mod bar")
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.arg("--unstable")
|
||||||
|
.arg("foo")
|
||||||
|
.arg("bar")
|
||||||
|
.arg("baz")
|
||||||
|
.stderr_regex(path_for_regex(
|
||||||
|
"error: Import `.*/foo.just` in `.*/bar.just` is circular\n",
|
||||||
|
))
|
||||||
|
.status(EXIT_FAILURE)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modules_use_module_settings() {
|
||||||
|
Test::new()
|
||||||
|
.write(
|
||||||
|
"foo.just",
|
||||||
|
"set allow-duplicate-recipes\nfoo:\nfoo:\n @echo FOO\n",
|
||||||
|
)
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.arg("--unstable")
|
||||||
|
.arg("foo")
|
||||||
|
.arg("foo")
|
||||||
|
.stdout("FOO\n")
|
||||||
|
.run();
|
||||||
|
|
||||||
|
Test::new()
|
||||||
|
.write("foo.just", "\nfoo:\nfoo:\n @echo FOO\n")
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
|
||||||
|
set allow-duplicate-recipes
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.status(EXIT_FAILURE)
|
||||||
|
.arg("--unstable")
|
||||||
|
.arg("foo")
|
||||||
|
.arg("foo")
|
||||||
|
.stderr(
|
||||||
|
"
|
||||||
|
error: Recipe `foo` first defined on line 2 is redefined on line 3
|
||||||
|
--> foo.just:3:1
|
||||||
|
|
|
||||||
|
3 | foo:
|
||||||
|
| ^^^
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modules_conflict_with_recipes() {
|
||||||
|
Test::new()
|
||||||
|
.write("foo.just", "")
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
foo:
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.stderr(
|
||||||
|
"
|
||||||
|
error: Module `foo` defined on line 1 is redefined as a recipe on line 2
|
||||||
|
--> justfile:2:1
|
||||||
|
|
|
||||||
|
2 | foo:
|
||||||
|
| ^^^
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.status(EXIT_FAILURE)
|
||||||
|
.arg("--unstable")
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modules_conflict_with_aliases() {
|
||||||
|
Test::new()
|
||||||
|
.write("foo.just", "")
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
bar:
|
||||||
|
alias foo := bar
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.stderr(
|
||||||
|
"
|
||||||
|
error: Module `foo` defined on line 1 is redefined as an alias on line 3
|
||||||
|
--> justfile:3:7
|
||||||
|
|
|
||||||
|
3 | alias foo := bar
|
||||||
|
| ^^^
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.status(EXIT_FAILURE)
|
||||||
|
.arg("--unstable")
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modules_conflict_with_other_modules() {
|
||||||
|
Test::new()
|
||||||
|
.write("foo.just", "")
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
mod foo
|
||||||
|
|
||||||
|
bar:
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.status(EXIT_FAILURE)
|
||||||
|
.stderr(
|
||||||
|
"
|
||||||
|
error: Module `foo` first defined on line 1 is redefined on line 2
|
||||||
|
--> justfile:2:5
|
||||||
|
|
|
||||||
|
2 | mod foo
|
||||||
|
| ^^^
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.arg("--unstable")
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modules_are_dumped_correctly() {
|
||||||
|
Test::new()
|
||||||
|
.write("foo.just", "foo:\n @echo FOO")
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.arg("--unstable")
|
||||||
|
.arg("--dump")
|
||||||
|
.stdout("mod foo\n")
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modules_can_be_in_subdirectory() {
|
||||||
|
Test::new()
|
||||||
|
.write("foo/mod.just", "foo:\n @echo FOO")
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.arg("--unstable")
|
||||||
|
.arg("foo")
|
||||||
|
.arg("foo")
|
||||||
|
.stdout("FOO\n")
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modules_in_subdirectory_can_be_named_justfile() {
|
||||||
|
Test::new()
|
||||||
|
.write("foo/justfile", "foo:\n @echo FOO")
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.arg("--unstable")
|
||||||
|
.arg("foo")
|
||||||
|
.arg("foo")
|
||||||
|
.stdout("FOO\n")
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modules_in_subdirectory_can_be_named_justfile_with_any_case() {
|
||||||
|
Test::new()
|
||||||
|
.write("foo/JUSTFILE", "foo:\n @echo FOO")
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.arg("--unstable")
|
||||||
|
.arg("foo")
|
||||||
|
.arg("foo")
|
||||||
|
.stdout("FOO\n")
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modules_in_subdirectory_can_have_leading_dot() {
|
||||||
|
Test::new()
|
||||||
|
.write("foo/.justfile", "foo:\n @echo FOO")
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.arg("--unstable")
|
||||||
|
.arg("foo")
|
||||||
|
.arg("foo")
|
||||||
|
.stdout("FOO\n")
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modules_require_unambiguous_file() {
|
||||||
|
Test::new()
|
||||||
|
.write("foo/justfile", "foo:\n @echo FOO")
|
||||||
|
.write("foo.just", "foo:\n @echo FOO")
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.arg("--unstable")
|
||||||
|
.status(EXIT_FAILURE)
|
||||||
|
.stderr(
|
||||||
|
"
|
||||||
|
error: Found multiple source files for module `foo`: `foo.just` and `foo/justfile`
|
||||||
|
--> justfile:1:5
|
||||||
|
|
|
||||||
|
1 | mod foo
|
||||||
|
| ^^^
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_module_file_error() {
|
||||||
|
Test::new()
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.arg("--unstable")
|
||||||
|
.status(EXIT_FAILURE)
|
||||||
|
.stderr(
|
||||||
|
"
|
||||||
|
error: Could not find source file for module `foo`.
|
||||||
|
--> justfile:1:5
|
||||||
|
|
|
||||||
|
1 | mod foo
|
||||||
|
| ^^^
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_displays_recipes_in_submodules() {
|
||||||
|
Test::new()
|
||||||
|
.write("foo.just", "bar:\n @echo FOO")
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.arg("--unstable")
|
||||||
|
.arg("--list")
|
||||||
|
.stdout(
|
||||||
|
"
|
||||||
|
Available recipes:
|
||||||
|
foo:
|
||||||
|
bar
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn root_dotenv_is_available_to_submodules() {
|
||||||
|
Test::new()
|
||||||
|
.write("foo.just", "foo:\n @echo $DOTENV_KEY")
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
set dotenv-load
|
||||||
|
|
||||||
|
mod foo
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.arg("--unstable")
|
||||||
|
.arg("foo")
|
||||||
|
.arg("foo")
|
||||||
|
.stdout("dotenv-value\n")
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dotenv_settings_in_submodule_are_ignored() {
|
||||||
|
Test::new()
|
||||||
|
.write(
|
||||||
|
"foo.just",
|
||||||
|
"set dotenv-load := false\nfoo:\n @echo $DOTENV_KEY",
|
||||||
|
)
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
set dotenv-load
|
||||||
|
|
||||||
|
mod foo
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.arg("--unstable")
|
||||||
|
.arg("foo")
|
||||||
|
.arg("foo")
|
||||||
|
.stdout("dotenv-value\n")
|
||||||
|
.run();
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user