diff --git a/README.md b/README.md index 0a59cf6..e9c7ecd 100644 --- a/README.md +++ b/README.md @@ -521,13 +521,14 @@ foo: #### Table of Settings -| Name | Value | Description | -| ---------------------- | ------------------ | -------------------------------------------------------------- | -| `dotenv-load` | boolean | Load a `.env` file, if present. | -| `export` | boolean | Export all variables as environment variables. | -| `positional-arguments` | boolean | Pass positional arguments. | -| `shell` | `[COMMAND, ARGS…]` | Set the command used to invoke recipes and evaluate backticks. | -| `windows-powershell` | boolean | Use PowerShell on Windows as default shell. | +| Name | Value | Description | +| ------------------------- | ------------------ | --------------------------------------------------------------------------------------------- | +| `allow-duplicate-recipes` | boolean | Allow recipes appearing later in a `justfile` to override earlier recipes with the same name. | +| `dotenv-load` | boolean | Load a `.env` file, if present. | +| `export` | boolean | Export all variables as environment variables. | +| `positional-arguments` | boolean | Pass positional arguments. | +| `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: @@ -541,6 +542,25 @@ Which is equivalent to: 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 If `dotenv-load` is `true`, a `.env` file will be loaded if present. Defaults to `true`. diff --git a/src/analyzer.rs b/src/analyzer.rs index c8bfe3d..8ded49b 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -16,6 +16,8 @@ impl<'src> Analyzer<'src> { } pub(crate) fn justfile(mut self, ast: Ast<'src>) -> CompileResult<'src, Justfile<'src>> { + let mut recipes = Vec::new(); + for item in ast.items { match item { Item::Alias(alias) => { @@ -28,8 +30,8 @@ impl<'src> Analyzer<'src> { } Item::Comment(_) => (), Item::Recipe(recipe) => { - self.analyze_recipe(&recipe)?; - self.recipes.insert(recipe); + Self::analyze_recipe(&recipe)?; + recipes.push(recipe); } Item::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(); for (_, set) in self.sets { match set.value { + Setting::AllowDuplicateRecipes(allow_duplicate_recipes) => { + settings.allow_duplicate_recipes = allow_duplicate_recipes; + } Setting::DotenvLoad(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 { warnings: ast.warnings, first: recipes @@ -101,14 +118,7 @@ impl<'src> Analyzer<'src> { }) } - fn analyze_recipe(&self, recipe: &UnresolvedRecipe<'src>) -> CompileResult<'src, ()> { - if let Some(original) = self.recipes.get(recipe.name.lexeme()) { - return Err(recipe.name.token().error(DuplicateRecipe { - recipe: original.name(), - first: original.line_number(), - })); - } - + fn analyze_recipe(recipe: &UnresolvedRecipe<'src>) -> CompileResult<'src, ()> { let mut parameters = BTreeSet::new(); let mut passed_default = false; diff --git a/src/keyword.rs b/src/keyword.rs index 2ed06d8..0f3f6fc 100644 --- a/src/keyword.rs +++ b/src/keyword.rs @@ -4,6 +4,7 @@ use crate::common::*; #[strum(serialize_all = "kebab_case")] pub(crate) enum Keyword { Alias, + AllowDuplicateRecipes, DotenvLoad, Else, Export, diff --git a/src/node.rs b/src/node.rs index 270d6bb..adba402 100644 --- a/src/node.rs +++ b/src/node.rs @@ -221,7 +221,11 @@ impl<'src> Node<'src> for Set<'src> { set.push_mut(self.name.lexeme().replace('-', "_")); 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()); } Shell(setting::Shell { command, arguments }) => { diff --git a/src/parser.rs b/src/parser.rs index 996c07a..cbefc1a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -729,7 +729,13 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { let name = Name::from_identifier(self.presume(Identifier)?); 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()?; return Ok(Set { value: Setting::DotenvLoad(value), @@ -1714,6 +1720,12 @@ mod tests { 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! { name: set_dotenv_load_true, text: "set dotenv-load := true", diff --git a/src/setting.rs b/src/setting.rs index 39c9eca..220ce9d 100644 --- a/src/setting.rs +++ b/src/setting.rs @@ -2,6 +2,7 @@ use crate::common::*; #[derive(Debug, Clone)] pub(crate) enum Setting<'src> { + AllowDuplicateRecipes(bool), DotenvLoad(bool), Export(bool), PositionalArguments(bool), @@ -18,7 +19,8 @@ pub(crate) struct Shell<'src> { impl<'src> Display for Setting<'src> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { match self { - Setting::DotenvLoad(value) + Setting::AllowDuplicateRecipes(value) + | Setting::DotenvLoad(value) | Setting::Export(value) | Setting::PositionalArguments(value) | Setting::WindowsPowerShell(value) => write!(f, "{}", value), diff --git a/src/settings.rs b/src/settings.rs index 8078ebc..9ba196d 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -7,6 +7,7 @@ pub(crate) const WINDOWS_POWERSHELL_ARGS: &[&str] = &["-NoLogo", "-Command"]; #[derive(Debug, PartialEq, Serialize)] pub(crate) struct Settings<'src> { + pub(crate) allow_duplicate_recipes: bool, pub(crate) dotenv_load: Option, pub(crate) export: bool, pub(crate) positional_arguments: bool, @@ -17,6 +18,7 @@ pub(crate) struct Settings<'src> { impl<'src> Settings<'src> { pub(crate) fn new() -> Settings<'src> { Settings { + allow_duplicate_recipes: false, dotenv_load: None, export: false, positional_arguments: false, diff --git a/tests/allow_duplicate_recipes.rs b/tests/allow_duplicate_recipes.rs new file mode 100644 index 0000000..d597ce0 --- /dev/null +++ b/tests/allow_duplicate_recipes.rs @@ -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(); +} diff --git a/tests/json.rs b/tests/json.rs index 2604f6f..345ca2a 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -39,6 +39,7 @@ fn alias() { } }, "settings": { + "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, "positional_arguments": false, @@ -66,6 +67,7 @@ fn assignment() { "first": null, "recipes": {}, "settings": { + "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, "positional_arguments": false, @@ -106,6 +108,7 @@ fn body() { } }, "settings": { + "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, "positional_arguments": false, @@ -156,6 +159,7 @@ fn dependencies() { } }, "settings": { + "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, "positional_arguments": false, @@ -243,6 +247,59 @@ fn dependency_argument() { } }, "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, "export": false, "positional_arguments": false, @@ -276,6 +333,7 @@ fn doc_comment() { } }, "settings": { + "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, "positional_arguments": false, @@ -297,6 +355,7 @@ fn empty_justfile() { "first": null, "recipes": {}, "settings": { + "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, "positional_arguments": false, @@ -427,6 +486,7 @@ fn parameters() { }, }, "settings": { + "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, "positional_arguments": false, @@ -496,6 +556,7 @@ fn priors() { }, }, "settings": { + "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, "positional_arguments": false, @@ -529,6 +590,7 @@ fn private() { } }, "settings": { + "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, "positional_arguments": false, @@ -562,6 +624,7 @@ fn quiet() { } }, "settings": { + "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, "positional_arguments": false, @@ -613,6 +676,7 @@ fn settings() { } }, "settings": { + "allow_duplicate_recipes": false, "dotenv_load": true, "export": true, "positional_arguments": true, @@ -652,6 +716,7 @@ fn shebang() { } }, "settings": { + "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, "positional_arguments": false, @@ -685,6 +750,7 @@ fn simple() { } }, "settings": { + "allow_duplicate_recipes": false, "dotenv_load": null, "export": false, "positional_arguments": false, diff --git a/tests/lib.rs b/tests/lib.rs index 7492e41..8739415 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,6 +1,7 @@ #[macro_use] mod test; +mod allow_duplicate_recipes; mod assert_stdout; mod assert_success; mod byte_order_mark;