Allow duplicate recipes (#1095)

This commit is contained in:
Greg Lutostanski 2022-02-14 20:37:06 -06:00 committed by GitHub
parent 0988a424ed
commit bcdaa95a66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 197 additions and 41 deletions

View File

@ -521,13 +521,14 @@ foo:
#### Table of Settings #### Table of Settings
| Name | Value | Description | | Name | Value | Description |
| ---------------------- | ------------------ | -------------------------------------------------------------- | | ------------------------- | ------------------ | --------------------------------------------------------------------------------------------- |
| `dotenv-load` | boolean | Load a `.env` file, if present. | | `allow-duplicate-recipes` | boolean | Allow recipes appearing later in a `justfile` to override earlier recipes with the same name. |
| `export` | boolean | Export all variables as environment variables. | | `dotenv-load` | boolean | Load a `.env` file, if present. |
| `positional-arguments` | boolean | Pass positional arguments. | | `export` | boolean | Export all variables as environment variables. |
| `shell` | `[COMMAND, ARGS…]` | Set the command used to invoke recipes and evaluate backticks. | | `positional-arguments` | boolean | Pass positional arguments. |
| `windows-powershell` | boolean | Use PowerShell on Windows as default shell. | | `shell` | `[COMMAND, ARGS…]` | Set the command used to invoke recipes and evaluate backticks. |
| `windows-powershell` | boolean | Use PowerShell on Windows as default shell. |
Boolean settings can be written as: Boolean settings can be written as:
@ -541,6 +542,25 @@ Which is equivalent to:
set NAME := true set NAME := true
``` ```
#### Allow Duplicate Recipes
If `allow-duplicate-recipes` is set to `true`, defining multiple recipes with the same name is not an error and the last definition is used. Defaults to `false`.
```make
set allow-duplicate-recipes
@foo:
echo foo
@foo:
echo bar
```
```sh
$ just foo
bar
```
#### Dotenv Load #### Dotenv Load
If `dotenv-load` is `true`, a `.env` file will be loaded if present. Defaults to `true`. If `dotenv-load` is `true`, a `.env` file will be loaded if present. Defaults to `true`.

View File

@ -16,6 +16,8 @@ impl<'src> Analyzer<'src> {
} }
pub(crate) fn justfile(mut self, ast: Ast<'src>) -> CompileResult<'src, Justfile<'src>> { pub(crate) fn justfile(mut self, ast: Ast<'src>) -> CompileResult<'src, Justfile<'src>> {
let mut recipes = Vec::new();
for item in ast.items { for item in ast.items {
match item { match item {
Item::Alias(alias) => { Item::Alias(alias) => {
@ -28,8 +30,8 @@ impl<'src> Analyzer<'src> {
} }
Item::Comment(_) => (), Item::Comment(_) => (),
Item::Recipe(recipe) => { Item::Recipe(recipe) => {
self.analyze_recipe(&recipe)?; Self::analyze_recipe(&recipe)?;
self.recipes.insert(recipe); recipes.push(recipe);
} }
Item::Set(set) => { Item::Set(set) => {
self.analyze_set(&set)?; self.analyze_set(&set)?;
@ -38,31 +40,13 @@ impl<'src> Analyzer<'src> {
} }
} }
let assignments = self.assignments;
AssignmentResolver::resolve_assignments(&assignments)?;
let recipes = RecipeResolver::resolve_recipes(self.recipes, &assignments)?;
for recipe in recipes.values() {
for parameter in &recipe.parameters {
if assignments.contains_key(parameter.name.lexeme()) {
return Err(parameter.name.token().error(ParameterShadowsVariable {
parameter: parameter.name.lexeme(),
}));
}
}
}
let mut aliases = Table::new();
while let Some(alias) = self.aliases.pop() {
aliases.insert(Self::resolve_alias(&recipes, alias)?);
}
let mut settings = Settings::new(); let mut settings = Settings::new();
for (_, set) in self.sets { for (_, set) in self.sets {
match set.value { match set.value {
Setting::AllowDuplicateRecipes(allow_duplicate_recipes) => {
settings.allow_duplicate_recipes = allow_duplicate_recipes;
}
Setting::DotenvLoad(dotenv_load) => { Setting::DotenvLoad(dotenv_load) => {
settings.dotenv_load = Some(dotenv_load); settings.dotenv_load = Some(dotenv_load);
} }
@ -82,6 +66,39 @@ impl<'src> Analyzer<'src> {
} }
} }
let assignments = self.assignments;
AssignmentResolver::resolve_assignments(&assignments)?;
for recipe in recipes {
if let Some(original) = self.recipes.get(recipe.name.lexeme()) {
if !settings.allow_duplicate_recipes {
return Err(recipe.name.token().error(DuplicateRecipe {
recipe: original.name(),
first: original.line_number(),
}));
}
}
self.recipes.insert(recipe);
}
let recipes = RecipeResolver::resolve_recipes(self.recipes, &assignments)?;
for recipe in recipes.values() {
for parameter in &recipe.parameters {
if assignments.contains_key(parameter.name.lexeme()) {
return Err(parameter.name.token().error(ParameterShadowsVariable {
parameter: parameter.name.lexeme(),
}));
}
}
}
let mut aliases = Table::new();
while let Some(alias) = self.aliases.pop() {
aliases.insert(Self::resolve_alias(&recipes, alias)?);
}
Ok(Justfile { Ok(Justfile {
warnings: ast.warnings, warnings: ast.warnings,
first: recipes first: recipes
@ -101,14 +118,7 @@ impl<'src> Analyzer<'src> {
}) })
} }
fn analyze_recipe(&self, recipe: &UnresolvedRecipe<'src>) -> CompileResult<'src, ()> { fn analyze_recipe(recipe: &UnresolvedRecipe<'src>) -> CompileResult<'src, ()> {
if let Some(original) = self.recipes.get(recipe.name.lexeme()) {
return Err(recipe.name.token().error(DuplicateRecipe {
recipe: original.name(),
first: original.line_number(),
}));
}
let mut parameters = BTreeSet::new(); let mut parameters = BTreeSet::new();
let mut passed_default = false; let mut passed_default = false;

View File

@ -4,6 +4,7 @@ use crate::common::*;
#[strum(serialize_all = "kebab_case")] #[strum(serialize_all = "kebab_case")]
pub(crate) enum Keyword { pub(crate) enum Keyword {
Alias, Alias,
AllowDuplicateRecipes,
DotenvLoad, DotenvLoad,
Else, Else,
Export, Export,

View File

@ -221,7 +221,11 @@ impl<'src> Node<'src> for Set<'src> {
set.push_mut(self.name.lexeme().replace('-', "_")); set.push_mut(self.name.lexeme().replace('-', "_"));
match &self.value { match &self.value {
DotenvLoad(value) | Export(value) | PositionalArguments(value) | WindowsPowerShell(value) => { AllowDuplicateRecipes(value)
| DotenvLoad(value)
| Export(value)
| PositionalArguments(value)
| WindowsPowerShell(value) => {
set.push_mut(value.to_string()); set.push_mut(value.to_string());
} }
Shell(setting::Shell { command, arguments }) => { Shell(setting::Shell { command, arguments }) => {

View File

@ -729,7 +729,13 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
let name = Name::from_identifier(self.presume(Identifier)?); let name = Name::from_identifier(self.presume(Identifier)?);
let lexeme = name.lexeme(); let lexeme = name.lexeme();
if Keyword::DotenvLoad == lexeme { if Keyword::AllowDuplicateRecipes == lexeme {
let value = self.parse_set_bool()?;
return Ok(Set {
value: Setting::AllowDuplicateRecipes(value),
name,
});
} else if Keyword::DotenvLoad == lexeme {
let value = self.parse_set_bool()?; let value = self.parse_set_bool()?;
return Ok(Set { return Ok(Set {
value: Setting::DotenvLoad(value), value: Setting::DotenvLoad(value),
@ -1714,6 +1720,12 @@ mod tests {
tree: (justfile (set dotenv_load true)), tree: (justfile (set dotenv_load true)),
} }
test! {
name: set_allow_duplicate_recipes_implicit,
text: "set allow-duplicate-recipes",
tree: (justfile (set allow_duplicate_recipes true)),
}
test! { test! {
name: set_dotenv_load_true, name: set_dotenv_load_true,
text: "set dotenv-load := true", text: "set dotenv-load := true",

View File

@ -2,6 +2,7 @@ use crate::common::*;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) enum Setting<'src> { pub(crate) enum Setting<'src> {
AllowDuplicateRecipes(bool),
DotenvLoad(bool), DotenvLoad(bool),
Export(bool), Export(bool),
PositionalArguments(bool), PositionalArguments(bool),
@ -18,7 +19,8 @@ pub(crate) struct Shell<'src> {
impl<'src> Display for Setting<'src> { impl<'src> Display for Setting<'src> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
match self { match self {
Setting::DotenvLoad(value) Setting::AllowDuplicateRecipes(value)
| Setting::DotenvLoad(value)
| Setting::Export(value) | Setting::Export(value)
| Setting::PositionalArguments(value) | Setting::PositionalArguments(value)
| Setting::WindowsPowerShell(value) => write!(f, "{}", value), | Setting::WindowsPowerShell(value) => write!(f, "{}", value),

View File

@ -7,6 +7,7 @@ pub(crate) const WINDOWS_POWERSHELL_ARGS: &[&str] = &["-NoLogo", "-Command"];
#[derive(Debug, PartialEq, Serialize)] #[derive(Debug, PartialEq, Serialize)]
pub(crate) struct Settings<'src> { pub(crate) struct Settings<'src> {
pub(crate) allow_duplicate_recipes: bool,
pub(crate) dotenv_load: Option<bool>, pub(crate) dotenv_load: Option<bool>,
pub(crate) export: bool, pub(crate) export: bool,
pub(crate) positional_arguments: bool, pub(crate) positional_arguments: bool,
@ -17,6 +18,7 @@ pub(crate) struct Settings<'src> {
impl<'src> Settings<'src> { impl<'src> Settings<'src> {
pub(crate) fn new() -> Settings<'src> { pub(crate) fn new() -> Settings<'src> {
Settings { Settings {
allow_duplicate_recipes: false,
dotenv_load: None, dotenv_load: None,
export: false, export: false,
positional_arguments: false, positional_arguments: false,

View File

@ -0,0 +1,38 @@
use crate::common::*;
#[test]
fn allow_duplicate_recipes() {
Test::new()
.justfile(
"
b:
echo foo
b:
echo bar
set allow-duplicate-recipes
",
)
.stdout("bar\n")
.stderr("echo bar\n")
.run();
}
#[test]
fn allow_duplicate_recipes_with_args() {
Test::new()
.justfile(
"
b a:
echo foo
b c d:
echo bar {{c}} {{d}}
set allow-duplicate-recipes
",
)
.args(&["b", "one", "two"])
.stdout("bar one two\n")
.stderr("echo bar one two\n")
.run();
}

View File

@ -39,6 +39,7 @@ fn alias() {
} }
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false,
"dotenv_load": null, "dotenv_load": null,
"export": false, "export": false,
"positional_arguments": false, "positional_arguments": false,
@ -66,6 +67,7 @@ fn assignment() {
"first": null, "first": null,
"recipes": {}, "recipes": {},
"settings": { "settings": {
"allow_duplicate_recipes": false,
"dotenv_load": null, "dotenv_load": null,
"export": false, "export": false,
"positional_arguments": false, "positional_arguments": false,
@ -106,6 +108,7 @@ fn body() {
} }
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false,
"dotenv_load": null, "dotenv_load": null,
"export": false, "export": false,
"positional_arguments": false, "positional_arguments": false,
@ -156,6 +159,7 @@ fn dependencies() {
} }
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false,
"dotenv_load": null, "dotenv_load": null,
"export": false, "export": false,
"positional_arguments": false, "positional_arguments": false,
@ -243,6 +247,59 @@ fn dependency_argument() {
} }
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false,
"dotenv_load": null,
"export": false,
"positional_arguments": false,
"shell": null,
"windows_powershell": false,
},
"warnings": [],
}),
);
}
#[test]
fn duplicate_recipes() {
test(
"
set allow-duplicate-recipes
alias f := foo
foo:
foo bar:
",
json!({
"first": "foo",
"aliases": {
"f": {
"name": "f",
"target": "foo",
}
},
"assignments": {},
"recipes": {
"foo": {
"body": [],
"dependencies": [],
"doc": null,
"name": "foo",
"parameters": [
{
"name": "bar",
"export": false,
"default": null,
"kind": "singular",
},
],
"priors": 0,
"private": false,
"quiet": false,
"shebang": false,
}
},
"settings": {
"allow_duplicate_recipes": true,
"dotenv_load": null, "dotenv_load": null,
"export": false, "export": false,
"positional_arguments": false, "positional_arguments": false,
@ -276,6 +333,7 @@ fn doc_comment() {
} }
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false,
"dotenv_load": null, "dotenv_load": null,
"export": false, "export": false,
"positional_arguments": false, "positional_arguments": false,
@ -297,6 +355,7 @@ fn empty_justfile() {
"first": null, "first": null,
"recipes": {}, "recipes": {},
"settings": { "settings": {
"allow_duplicate_recipes": false,
"dotenv_load": null, "dotenv_load": null,
"export": false, "export": false,
"positional_arguments": false, "positional_arguments": false,
@ -427,6 +486,7 @@ fn parameters() {
}, },
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false,
"dotenv_load": null, "dotenv_load": null,
"export": false, "export": false,
"positional_arguments": false, "positional_arguments": false,
@ -496,6 +556,7 @@ fn priors() {
}, },
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false,
"dotenv_load": null, "dotenv_load": null,
"export": false, "export": false,
"positional_arguments": false, "positional_arguments": false,
@ -529,6 +590,7 @@ fn private() {
} }
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false,
"dotenv_load": null, "dotenv_load": null,
"export": false, "export": false,
"positional_arguments": false, "positional_arguments": false,
@ -562,6 +624,7 @@ fn quiet() {
} }
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false,
"dotenv_load": null, "dotenv_load": null,
"export": false, "export": false,
"positional_arguments": false, "positional_arguments": false,
@ -613,6 +676,7 @@ fn settings() {
} }
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false,
"dotenv_load": true, "dotenv_load": true,
"export": true, "export": true,
"positional_arguments": true, "positional_arguments": true,
@ -652,6 +716,7 @@ fn shebang() {
} }
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false,
"dotenv_load": null, "dotenv_load": null,
"export": false, "export": false,
"positional_arguments": false, "positional_arguments": false,
@ -685,6 +750,7 @@ fn simple() {
} }
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false,
"dotenv_load": null, "dotenv_load": null,
"export": false, "export": false,
"positional_arguments": false, "positional_arguments": false,

View File

@ -1,6 +1,7 @@
#[macro_use] #[macro_use]
mod test; mod test;
mod allow_duplicate_recipes;
mod assert_stdout; mod assert_stdout;
mod assert_success; mod assert_success;
mod byte_order_mark; mod byte_order_mark;