Allow private attribute on aliases (#1434)

This commit is contained in:
Greg Shuflin 2022-12-20 00:44:19 -08:00 committed by GitHub
parent c7acaa82cb
commit fbe1c4c7a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 105 additions and 9 deletions

View File

@ -2007,12 +2007,15 @@ $ just --summary
test test
``` ```
The `[private]` attribute<sup>master</sup> may also be used to hide recipes without needing to change the name: The `[private]` attribute<sup>master</sup> may also be used to hide recipes or aliases without needing to change the name:
```just ```just
[private] [private]
foo: foo:
[private]
alias b := bar
bar: bar:
``` ```

View File

@ -3,6 +3,7 @@ use super::*;
/// An alias, e.g. `name := target` /// An alias, e.g. `name := target`
#[derive(Debug, PartialEq, Clone, Serialize)] #[derive(Debug, PartialEq, Clone, Serialize)]
pub(crate) struct Alias<'src, T = Rc<Recipe<'src>>> { pub(crate) struct Alias<'src, T = Rc<Recipe<'src>>> {
pub(crate) attributes: BTreeSet<Attribute>,
pub(crate) name: Name<'src>, pub(crate) name: Name<'src>,
#[serde( #[serde(
bound(serialize = "T: Keyed<'src>"), bound(serialize = "T: Keyed<'src>"),
@ -20,6 +21,7 @@ impl<'src> Alias<'src, Name<'src>> {
assert_eq!(self.target.lexeme(), target.name.lexeme()); assert_eq!(self.target.lexeme(), target.name.lexeme());
Alias { Alias {
attributes: self.attributes,
name: self.name, name: self.name,
target, target,
} }
@ -28,7 +30,7 @@ impl<'src> Alias<'src, Name<'src>> {
impl Alias<'_> { impl Alias<'_> {
pub(crate) fn is_private(&self) -> bool { pub(crate) fn is_private(&self) -> bool {
self.name.lexeme().starts_with('_') self.name.lexeme().starts_with('_') || self.attributes.contains(&Attribute::Private)
} }
} }

View File

@ -2,6 +2,8 @@ use super::*;
use CompileErrorKind::*; use CompileErrorKind::*;
const VALID_ALIAS_ATTRIBUTES: [Attribute; 1] = [Attribute::Private];
#[derive(Default)] #[derive(Default)]
pub(crate) struct Analyzer<'src> { pub(crate) struct Analyzer<'src> {
recipes: Table<'src, UnresolvedRecipe<'src>>, recipes: Table<'src, UnresolvedRecipe<'src>>,
@ -195,6 +197,15 @@ impl<'src> Analyzer<'src> {
})); }));
} }
for attr in &alias.attributes {
if !VALID_ALIAS_ATTRIBUTES.contains(attr) {
return Err(alias.name.token().error(AliasInvalidAttribute {
alias: name,
attr: *attr,
}));
}
}
Ok(()) Ok(())
} }

View File

@ -24,6 +24,14 @@ impl Display for CompileError<'_> {
use CompileErrorKind::*; use CompileErrorKind::*;
match &*self.kind { match &*self.kind {
AliasInvalidAttribute { alias, attr } => {
write!(
f,
"Alias {} has an invalid attribute `{}`",
alias,
attr.to_str()
)?;
}
AliasShadowsRecipe { alias, recipe_line } => { AliasShadowsRecipe { alias, recipe_line } => {
write!( write!(
f, f,

View File

@ -2,6 +2,10 @@ use super::*;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub(crate) enum CompileErrorKind<'src> { pub(crate) enum CompileErrorKind<'src> {
AliasInvalidAttribute {
alias: &'src str,
attr: Attribute,
},
AliasShadowsRecipe { AliasShadowsRecipe {
alias: &'src str, alias: &'src str,
recipe_line: usize, recipe_line: usize,

View File

@ -325,7 +325,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
} else if self.next_is(Identifier) { } else if self.next_is(Identifier) {
match Keyword::from_lexeme(next.lexeme()) { match Keyword::from_lexeme(next.lexeme()) {
Some(Keyword::Alias) if self.next_are(&[Identifier, Identifier, ColonEquals]) => { Some(Keyword::Alias) if self.next_are(&[Identifier, Identifier, ColonEquals]) => {
items.push(Item::Alias(self.parse_alias()?)); items.push(Item::Alias(self.parse_alias(BTreeSet::new())?));
} }
Some(Keyword::Export) if self.next_are(&[Identifier, Identifier, ColonEquals]) => { Some(Keyword::Export) if self.next_are(&[Identifier, Identifier, ColonEquals]) => {
self.presume_keyword(Keyword::Export)?; self.presume_keyword(Keyword::Export)?;
@ -361,9 +361,17 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
BTreeSet::new(), BTreeSet::new(),
)?)); )?));
} else if let Some(attributes) = self.parse_attributes()? { } else if let Some(attributes) = self.parse_attributes()? {
let next_keyword = Keyword::from_lexeme(self.next()?.lexeme());
match next_keyword {
Some(Keyword::Alias) if self.next_are(&[Identifier, Identifier, ColonEquals]) => {
items.push(Item::Alias(self.parse_alias(attributes)?));
}
_ => {
let quiet = self.accepted(At)?; let quiet = self.accepted(At)?;
let doc = pop_doc_comment(&mut items, eol_since_last_comment); let doc = pop_doc_comment(&mut items, eol_since_last_comment);
items.push(Item::Recipe(self.parse_recipe(doc, quiet, attributes)?)); items.push(Item::Recipe(self.parse_recipe(doc, quiet, attributes)?));
}
}
} else { } else {
return Err(self.unexpected_token()?); return Err(self.unexpected_token()?);
} }
@ -383,13 +391,20 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
} }
/// Parse an alias, e.g `alias name := target` /// Parse an alias, e.g `alias name := target`
fn parse_alias(&mut self) -> CompileResult<'src, Alias<'src, Name<'src>>> { fn parse_alias(
&mut self,
attributes: BTreeSet<Attribute>,
) -> CompileResult<'src, Alias<'src, Name<'src>>> {
self.presume_keyword(Keyword::Alias)?; self.presume_keyword(Keyword::Alias)?;
let name = self.parse_name()?; let name = self.parse_name()?;
self.presume_any(&[Equals, ColonEquals])?; self.presume_any(&[Equals, ColonEquals])?;
let target = self.parse_name()?; let target = self.parse_name()?;
self.expect_eol()?; self.expect_eol()?;
Ok(Alias { name, target }) Ok(Alias {
attributes,
name,
target,
})
} }
/// Parse an assignment, e.g. `foo := bar` /// Parse an assignment, e.g. `foo := bar`
@ -978,6 +993,12 @@ mod tests {
tree: (justfile (alias t test)), tree: (justfile (alias t test)),
} }
test! {
name: alias_with_attributee,
text: "[private]\nalias t := test",
tree: (justfile (alias t test)),
}
test! { test! {
name: aliases_multiple, name: aliases_multiple,
text: "alias t := test\nalias b := build", text: "alias t := test\nalias b := build",
@ -996,6 +1017,18 @@ mod tests {
), ),
} }
test! {
name: recipe_named_alias,
text: r#"
[private]
alias:
echo 'echoing alias'
"#,
tree: (justfile
(recipe alias (body ("echo 'echoing alias'")))
),
}
test! { test! {
name: export, name: export,
text: r#"export x := "hello""#, text: r#"export x := "hello""#,

View File

@ -1,5 +1,17 @@
use super::*; use super::*;
test! {
name: invalid_alias_attribute,
justfile: "[private]\n[linux]\nalias t := test\n\ntest:\n",
stderr: "
error: Alias t has an invalid attribute `linux`
|
3 | alias t := test
| ^
",
status: EXIT_FAILURE,
}
test! { test! {
name: expected_keyword, name: expected_keyword,
justfile: "foo := if '' == '' { '' } arlo { '' }", justfile: "foo := if '' == '' { '' } arlo { '' }",

View File

@ -22,6 +22,7 @@ fn alias() {
"f": { "f": {
"name": "f", "name": "f",
"target": "foo", "target": "foo",
"attributes": [],
} }
}, },
"assignments": {}, "assignments": {},
@ -299,6 +300,7 @@ fn duplicate_recipes() {
"first": "foo", "first": "foo",
"aliases": { "aliases": {
"f": { "f": {
"attributes": [],
"name": "f", "name": "f",
"target": "foo", "target": "foo",
} }

View File

@ -1,7 +1,7 @@
use super::*; use super::*;
#[test] #[test]
fn attribute() { fn private_attribute_for_recipe() {
Test::new() Test::new()
.justfile( .justfile(
" "
@ -17,3 +17,24 @@ fn attribute() {
) )
.run(); .run();
} }
#[test]
fn private_attribute_for_alias() {
Test::new()
.justfile(
"
[private]
alias f := foo
foo:
",
)
.args(&["--list"])
.stdout(
"
Available recipes:
foo
",
)
.run();
}