diff --git a/src/alias.rs b/src/alias.rs index 18280546fe..904a014af5 100644 --- a/src/alias.rs +++ b/src/alias.rs @@ -1,7 +1,7 @@ use super::*; /// An alias, e.g. `name := target` -#[derive(Debug, PartialEq, Clone, Serialize)] +#[derive(Debug, PartialEq, Ord, PartialOrd, Eq, Clone, Serialize)] pub(crate) struct Alias<'src, T = Rc>> { pub(crate) attributes: AttributeSet<'src>, pub(crate) name: Name<'src>, diff --git a/src/analyzer.rs b/src/analyzer.rs index c2c3da67ec..e12ae225a0 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -86,6 +86,17 @@ impl<'run, 'src> Analyzer<'run, 'src> { if recipe.enabled() { Self::analyze_recipe(recipe)?; self.recipes.push(recipe); + + for attribute in &recipe.attributes { + if let Attribute::Alias(name, _) = attribute { + Self::define(&mut definitions, *name, "alias", false)?; + self.aliases.insert(Alias { + name: *name, + target: recipe.name, + attributes: AttributeSet::new(), + }); + } + } } } Item::Set(set) => { diff --git a/src/attribute.rs b/src/attribute.rs index fb2fef4f89..1e99086779 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -9,6 +9,7 @@ use super::*; #[strum_discriminants(derive(EnumString, Ord, PartialOrd))] #[strum_discriminants(strum(serialize_all = "kebab-case"))] pub(crate) enum Attribute<'src> { + Alias(Name<'src>, StringLiteral<'src>), Confirm(Option>), Doc(Option>), ExitMessage, @@ -32,7 +33,7 @@ impl AttributeDiscriminant { fn argument_range(self) -> RangeInclusive { match self { Self::Confirm | Self::Doc => 0..=1, - Self::Group | Self::Extension | Self::WorkingDirectory => 1..=1, + Self::Alias | Self::Group | Self::Extension | Self::WorkingDirectory => 1..=1, Self::ExitMessage | Self::Linux | Self::Macos @@ -52,7 +53,7 @@ impl AttributeDiscriminant { impl<'src> Attribute<'src> { pub(crate) fn new( name: Name<'src>, - arguments: Vec>, + arguments: Vec<(Token<'src>, StringLiteral<'src>)>, ) -> CompileResult<'src, Self> { let discriminant = name .lexeme() @@ -77,12 +78,41 @@ impl<'src> Attribute<'src> { ); } + let mut arguments = arguments.into_iter(); + let (token, argument) = arguments + .next() + .map(|(token, arg)| (Some(token), Some(arg))) + .unwrap_or_default(); + Ok(match discriminant { - AttributeDiscriminant::Confirm => Self::Confirm(arguments.into_iter().next()), - AttributeDiscriminant::Doc => Self::Doc(arguments.into_iter().next()), + AttributeDiscriminant::Alias => { + let string_literal = argument.unwrap(); + let delim = string_literal.kind.delimiter_len(); + let token = token.unwrap(); + let token = Token { + kind: TokenKind::Identifier, + column: token.column + delim, + length: token.length - (delim * 2), + offset: token.offset + delim, + ..token + }; + + let alias = token.lexeme(); + let valid_alias = alias.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'); + + if alias.is_empty() || !valid_alias { + return Err(token.error(CompileErrorKind::InvalidAliasName { + name: token.lexeme(), + })); + } + + Self::Alias(Name::from_identifier(token), string_literal) + } + AttributeDiscriminant::Confirm => Self::Confirm(argument), + AttributeDiscriminant::Doc => Self::Doc(argument), AttributeDiscriminant::ExitMessage => Self::ExitMessage, - AttributeDiscriminant::Extension => Self::Extension(arguments.into_iter().next().unwrap()), - AttributeDiscriminant::Group => Self::Group(arguments.into_iter().next().unwrap()), + AttributeDiscriminant::Extension => Self::Extension(argument.unwrap()), + AttributeDiscriminant::Group => Self::Group(argument.unwrap()), AttributeDiscriminant::Linux => Self::Linux, AttributeDiscriminant::Macos => Self::Macos, AttributeDiscriminant::NoCd => Self::NoCd, @@ -92,17 +122,14 @@ impl<'src> Attribute<'src> { AttributeDiscriminant::PositionalArguments => Self::PositionalArguments, AttributeDiscriminant::Private => Self::Private, AttributeDiscriminant::Script => Self::Script({ - let mut arguments = arguments.into_iter(); - arguments.next().map(|command| Interpreter { + argument.map(|command| Interpreter { command, - arguments: arguments.collect(), + arguments: arguments.map(|(_, arg)| arg).collect(), }) }), AttributeDiscriminant::Unix => Self::Unix, AttributeDiscriminant::Windows => Self::Windows, - AttributeDiscriminant::WorkingDirectory => { - Self::WorkingDirectory(arguments.into_iter().next().unwrap()) - } + AttributeDiscriminant::WorkingDirectory => Self::WorkingDirectory(argument.unwrap()), }) } @@ -115,7 +142,7 @@ impl<'src> Attribute<'src> { } pub(crate) fn repeatable(&self) -> bool { - matches!(self, Attribute::Group(_)) + matches!(self, Attribute::Group(_) | Attribute::Alias(_, _)) } } @@ -124,7 +151,8 @@ impl Display for Attribute<'_> { write!(f, "{}", self.name())?; match self { - Self::Confirm(Some(argument)) + Self::Alias(_, argument) + | Self::Confirm(Some(argument)) | Self::Doc(Some(argument)) | Self::Extension(argument) | Self::Group(argument) diff --git a/src/attribute_set.rs b/src/attribute_set.rs index a40b001d1a..d8504e9c70 100644 --- a/src/attribute_set.rs +++ b/src/attribute_set.rs @@ -1,9 +1,13 @@ use {super::*, std::collections}; -#[derive(Default, Debug, Clone, PartialEq, Serialize)] +#[derive(Default, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Serialize)] pub(crate) struct AttributeSet<'src>(BTreeSet>); impl<'src> AttributeSet<'src> { + pub(crate) fn new() -> Self { + Self(BTreeSet::new()) + } + pub(crate) fn len(&self) -> usize { self.0.len() } diff --git a/src/compile_error.rs b/src/compile_error.rs index b09a1e5eb3..434566b8f6 100644 --- a/src/compile_error.rs +++ b/src/compile_error.rs @@ -170,6 +170,10 @@ impl Display for CompileError<'_> { "Internal error, this may indicate a bug in just: {message}\n\ consider filing an issue: https://github.com/casey/just/issues/new" ), + InvalidAliasName { name } => write!( + f, + "`{name}` is not a valid alias. Aliases must only contain alphanumeric characters and underscores." + ), InvalidAttribute { item_name, item_kind, diff --git a/src/compile_error_kind.rs b/src/compile_error_kind.rs index ca6f6fa7a6..b823fb9485 100644 --- a/src/compile_error_kind.rs +++ b/src/compile_error_kind.rs @@ -74,6 +74,9 @@ pub(crate) enum CompileErrorKind<'src> { Internal { message: String, }, + InvalidAliasName { + name: &'src str, + }, InvalidAttribute { item_kind: &'static str, item_name: &'src str, diff --git a/src/parser.rs b/src/parser.rs index 9305a42db5..f3ea34d2ed 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1181,10 +1181,10 @@ impl<'run, 'src> Parser<'run, 'src> { let mut arguments = Vec::new(); if self.accepted(Colon)? { - arguments.push(self.parse_string_literal()?); + arguments.push(self.parse_string_literal_token()?); } else if self.accepted(ParenL)? { loop { - arguments.push(self.parse_string_literal()?); + arguments.push(self.parse_string_literal_token()?); if !self.accepted(Comma)? { break; diff --git a/tests/attributes.rs b/tests/attributes.rs index 80393f1aa2..b755b89f0a 100644 --- a/tests/attributes.rs +++ b/tests/attributes.rs @@ -254,3 +254,165 @@ fn duplicate_non_repeatable_attributes_are_forbidden() { .status(EXIT_FAILURE) .run(); } + +#[test] +fn aliases_can_be_defined_as_attributes() { + Test::new() + .justfile( + " + [alias('bar')] + baz: + ", + ) + .arg("bar") + .status(EXIT_SUCCESS) + .run(); +} + +#[test] +fn multiple_aliases_can_be_defined_as_attributes() { + Test::new() + .justfile( + " + [alias('bar')] + [alias('foo')] + baz: + ", + ) + .arg("foo") + .status(EXIT_SUCCESS) + .run(); +} + +#[test] +fn duplicate_alias_attributes_are_forbidden() { + Test::new() + .justfile( + " + [alias('foo')] + [alias('foo')] + baz: + ", + ) + .arg("foo") + .stderr( + " + error: Alias `foo` first defined on line 1 is redefined on line 2 + ——▶ justfile:2:9 + │ + 2 │ [alias('foo')] + │ ^^^ + ", + ) + .status(EXIT_FAILURE) + .run(); +} + +#[test] +fn alias_attributes_duplicating_alias_definition_is_forbidden() { + Test::new() + .justfile( + " + alias foo := baz + [alias('foo')] + baz: + ", + ) + .arg("foo") + .stderr( + " + error: Alias `foo` first defined on line 1 is redefined on line 2 + ——▶ justfile:2:9 + │ + 2 │ [alias('foo')] + │ ^^^ + ", + ) + .status(EXIT_FAILURE) + .run(); +} + +#[test] +fn alias_definitions_duplicating_alias_attributes_is_forbidden() { + Test::new() + .justfile( + " + [alias('foo')] + baz: + + alias foo := baz + ", + ) + .arg("foo") + .stderr( + " + error: Alias `foo` first defined on line 1 is redefined on line 4 + ——▶ justfile:4:7 + │ + 4 │ alias foo := baz + │ ^^^ + ", + ) + .status(EXIT_FAILURE) + .run(); +} + +#[test] +fn alphanumeric_and_underscores_are_valid_alias_attributes() { + Test::new() + .justfile( + " + [alias('alpha_numeric_123')] + baz: + ", + ) + .arg("alpha_numeric_123") + .status(EXIT_SUCCESS) + .run(); +} + +#[test] +fn nonalphanumeric_alias_attribute_is_forbidden() { + Test::new() + .justfile( + " + [alias('invalid name!')] + baz: + ", + ) + .arg("foo") + .stderr( + " + error: `invalid name!` is not a valid alias. Aliases must only contain alphanumeric characters and underscores. + ——▶ justfile:1:9 + │ + 1 │ [alias('invalid name!')] + │ ^^^^^^^^^^^^^ + ", + ) + .status(EXIT_FAILURE) + .run(); +} + +#[test] +fn empty_alias_attribute_is_forbidden() { + Test::new() + .justfile( + " + [alias('')] + baz: + ", + ) + .arg("baz") + .stderr( + " + error: `` is not a valid alias. Aliases must only contain alphanumeric characters and underscores. + ——▶ justfile:1:9 + │ + 1 │ [alias('')] + │ ^ + ", + ) + .status(EXIT_FAILURE) + .run(); +}