Allow duplicate recipes (#1095)
This commit is contained in:
parent
0988a424ed
commit
bcdaa95a66
22
README.md
22
README.md
@ -522,7 +522,8 @@ foo:
|
|||||||
#### Table of Settings
|
#### Table of Settings
|
||||||
|
|
||||||
| Name | Value | Description |
|
| 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. |
|
| `dotenv-load` | boolean | Load a `.env` file, if present. |
|
||||||
| `export` | boolean | Export all variables as environment variables. |
|
| `export` | boolean | Export all variables as environment variables. |
|
||||||
| `positional-arguments` | boolean | Pass positional arguments. |
|
| `positional-arguments` | boolean | Pass positional arguments. |
|
||||||
@ -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`.
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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 }) => {
|
||||||
|
@ -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",
|
||||||
|
@ -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),
|
||||||
|
@ -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,
|
||||||
|
38
tests/allow_duplicate_recipes.rs
Normal file
38
tests/allow_duplicate_recipes.rs
Normal 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();
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user