Skip to content

Commit

Permalink
feat: module path aliases
Browse files Browse the repository at this point in the history
  • Loading branch information
corvusrabus committed Feb 22, 2025
1 parent d0372d1 commit 4287e01
Show file tree
Hide file tree
Showing 12 changed files with 196 additions and 22 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,22 @@ $ just b
echo 'Building!'
Building!
```
Aliases can also be used for recipes in modules.

Given we have a file `foo.just`

```justfile
bar:
@echo 'bar'
```

we can alias the `bar` recipe in `foo`:

```justfile
mod foo
alias fb := foo::bar
```

### Settings

Expand Down
9 changes: 2 additions & 7 deletions src/alias.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,9 @@ impl<'src, T> Keyed<'src> for Alias<'src, T> {
}
}

impl<'src> Display for Alias<'src, Name<'src>> {
impl<'src> Display for Alias<'src, Namepath<'src>> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(
f,
"alias {} := {}",
self.name.lexeme(),
self.target.lexeme()
)
write!(f, "alias {} := {}", self.name.lexeme(), self.target)
}
}

Expand Down
58 changes: 50 additions & 8 deletions src/analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use {super::*, CompileErrorKind::*};

#[derive(Default)]
pub(crate) struct Analyzer<'run, 'src> {
aliases: Table<'src, Alias<'src, Name<'src>>>,
aliases: Table<'src, Alias<'src, Namepath<'src>>>,
assignments: Vec<&'run Binding<'src, Expression<'src>>>,
modules: Table<'src, Justfile<'src>>,
recipes: Vec<&'run Recipe<'src, UnresolvedDependency<'src>>>,
Expand Down Expand Up @@ -149,7 +149,7 @@ impl<'run, 'src> Analyzer<'run, 'src> {

let mut aliases = Table::new();
while let Some(alias) = self.aliases.pop() {
aliases.insert(Self::resolve_alias(&recipes, alias)?);
aliases.insert(Self::resolve_alias(&self.modules, &recipes, alias)?);
}

for recipe in recipes.values() {
Expand Down Expand Up @@ -289,19 +289,61 @@ impl<'run, 'src> Analyzer<'run, 'src> {
Ok(())
}

fn resolve_alias(
recipes: &Table<'src, Rc<Recipe<'src>>>,
alias: Alias<'src, Name<'src>>,
fn resolve_alias<'a>(
modules: &'a Table<'src, Justfile<'src>>,
recipes: &'a Table<'src, Rc<Recipe<'src>>>,
alias: Alias<'src, Namepath<'src>>,
) -> CompileResult<'src, Alias<'src>> {
let Alias {
attributes,
name,
target,
} = alias;
let target_str = format!("{target}");
let mut path = target.into_inner();
let target = path
.pop()
.expect("Internal error: Alias target path can't be empty");

let alias = Alias {
attributes,
name,
target,
};

let mut maybe_recipes = Some(recipes);
if !path.is_empty() {
maybe_recipes =
Self::traverse_nonempty_module_name_path(&path, modules).map(|justfile| &justfile.recipes);
}

// Make sure the target recipe exists
match recipes.get(alias.target.lexeme()) {
match maybe_recipes.and_then(|recipes| recipes.get(target.lexeme())) {
Some(target) => Ok(alias.resolve(Rc::clone(target))),
None => Err(alias.name.token.error(UnknownAliasTarget {
alias: alias.name.lexeme(),
target: alias.target.lexeme(),
target: target_str,
})),
}
}

fn traverse_nonempty_module_name_path<'modules>(
path: &[Name],
mut modules: &'modules Table<'src, Justfile<'src>>,
) -> Option<&'modules Justfile<'src>> {
assert!(!path.is_empty());

let mut iter = path.iter();
let mut result = modules.get(iter.next().unwrap().lexeme())?;
modules = &result.modules;

for mod_name in iter {
result = modules.get(mod_name.lexeme())?;
modules = &result.modules;
}

Some(result)
}
}

#[cfg(test)]
Expand All @@ -325,7 +367,7 @@ mod tests {
line: 0,
column: 6,
width: 3,
kind: UnknownAliasTarget {alias: "foo", target: "bar"},
kind: UnknownAliasTarget {alias: "foo", target: "bar".to_string()},
}

analysis_error! {
Expand Down
2 changes: 1 addition & 1 deletion src/compile_error_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ pub(crate) enum CompileErrorKind<'src> {
UnicodeEscapeUnterminated,
UnknownAliasTarget {
alias: &'src str,
target: &'src str,
target: String,
},
UnknownAttribute {
attribute: &'src str,
Expand Down
2 changes: 1 addition & 1 deletion src/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use super::*;
/// A single top-level item
#[derive(Debug, Clone)]
pub(crate) enum Item<'src> {
Alias(Alias<'src, Name<'src>>),
Alias(Alias<'src, Namepath<'src>>),
Assignment(Assignment<'src>),
Comment(&'src str),
Import {
Expand Down
3 changes: 3 additions & 0 deletions src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,8 @@ impl<'src> Lexer<'src> {

if self.accepted('=')? {
self.token(ColonEquals);
} else if self.accepted(':')? {
self.token(ColonColon);
} else {
self.token(Colon);
self.recipe_body_pending = true;
Expand Down Expand Up @@ -982,6 +984,7 @@ mod tests {
BracketR => "]",
ByteOrderMark => "\u{feff}",
Colon => ":",
ColonColon => "::",
ColonEquals => ":=",
Comma => ",",
Dollar => "$",
Expand Down
16 changes: 16 additions & 0 deletions src/namepath.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ impl<'src> Namepath<'src> {
spaced: true,
}
}

pub(crate) fn new(path: Vec<Name<'src>>) -> Self {
Self(path)
}

pub fn into_inner(self) -> Vec<Name<'src>> {
self.0
}
}

impl Display for Namepath<'_> {
Expand All @@ -36,3 +44,11 @@ impl Serialize for Namepath<'_> {
serializer.serialize_str(&format!("{self}"))
}
}

impl<'src> Deref for Namepath<'src> {
type Target = Vec<Name<'src>>;

fn deref(&self) -> &Self::Target {
&self.0
}
}
19 changes: 17 additions & 2 deletions src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,26 @@ impl<'src> Node<'src> for Item<'src> {
}
}

impl<'src> Node<'src> for Alias<'src, Name<'src>> {
impl<'src> Node<'src> for Namepath<'src> {
fn tree(&self) -> Tree<'src> {
match self.len() {
1 => Tree::atom(self.first().unwrap().lexeme()),
_ => Tree::list(
self
.iter()
.map(|name| Tree::atom(Cow::Borrowed(name.lexeme()))),
),
}
}
}

impl<'src> Node<'src> for Alias<'src, Namepath<'src>> {
fn tree(&self) -> Tree<'src> {
let target = self.target.tree();

Tree::atom(Keyword::Alias.lexeme())
.push(self.name.lexeme())
.push(self.target.lexeme())
.push(target)
}
}

Expand Down
45 changes: 42 additions & 3 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -485,11 +485,11 @@ impl<'run, 'src> Parser<'run, 'src> {
fn parse_alias(
&mut self,
attributes: AttributeSet<'src>,
) -> CompileResult<'src, Alias<'src, Name<'src>>> {
) -> CompileResult<'src, Alias<'src, Namepath<'src>>> {
self.presume_keyword(Keyword::Alias)?;
let name = self.parse_name()?;
self.presume_any(&[Equals, ColonEquals])?;
let target = self.parse_name()?;
let target = self.parse_double_colon_path()?;
self.expect_eol()?;

attributes.ensure_valid_attributes("Alias", *name, &[AttributeDiscriminant::Private])?;
Expand Down Expand Up @@ -863,6 +863,19 @@ impl<'run, 'src> Parser<'run, 'src> {
self.expect(Identifier).map(Name::from_identifier)
}

/// Parse a path of the forms `a` or `a::b` or `a::b::c` etc.
fn parse_double_colon_path(&mut self) -> CompileResult<'src, Namepath<'src>> {
let first = self.parse_name()?;
let mut result = vec![first];

while self.accepted(ColonColon)? {
let name = self.parse_name()?;
result.push(name);
}

Ok(Namepath::new(result))
}

/// Parse sequence of comma-separated expressions
fn parse_sequence(&mut self) -> CompileResult<'src, Vec<Expression<'src>>> {
self.presume(ParenL)?;
Expand Down Expand Up @@ -1350,6 +1363,12 @@ mod tests {
tree: (justfile (alias t test)),
}

test! {
name: alias_module_path,
text: "alias fbb := foo::bar::baz",
tree: (justfile (alias fbb (foo bar baz))),
}

test! {
name: single_argument_attribute_shorthand,
text: "[group: 'foo']\nbar:",
Expand Down Expand Up @@ -2423,7 +2442,7 @@ mod tests {
line: 0,
column: 17,
width: 3,
kind: UnexpectedToken { expected: vec![Comment, Eof, Eol], found: Identifier },
kind: UnexpectedToken { expected: vec![ColonColon, Comment, Eof, Eol], found: Identifier },
}

error! {
Expand All @@ -2436,6 +2455,26 @@ mod tests {
kind: UnexpectedToken {expected: vec![Identifier], found:Eol},
}

error! {
name: alias_syntax_colon_end,
input: "alias foo := bar::\n",
offset: 18,
line: 0,
column: 18,
width: 1,
kind: UnexpectedToken {expected: vec![Identifier], found:Eol},
}

error! {
name: alias_syntax_single_colon,
input: "alias foo := bar:baz",
offset: 16,
line: 0,
column: 16,
width: 1,
kind: UnexpectedToken {expected: vec![ColonColon, Comment, Eof, Eol], found:Colon},
}

error! {
name: missing_colon,
input: "a b c\nd e f",
Expand Down
2 changes: 2 additions & 0 deletions src/token_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub(crate) enum TokenKind {
BracketR,
ByteOrderMark,
Colon,
ColonColon,
ColonEquals,
Comma,
Comment,
Expand Down Expand Up @@ -60,6 +61,7 @@ impl Display for TokenKind {
BracketR => "']'",
ByteOrderMark => "byte order mark",
Colon => "':'",
ColonColon => "'::'",
ColonEquals => "':='",
Comma => "','",
Comment => "comment",
Expand Down
45 changes: 45 additions & 0 deletions tests/alias.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use super::*;
#[test]
fn alias_nested_module() {
Test::new()
.write("foo.just", "mod bar\nbaz: \n @echo FOO")
.write("bar.just", "baz:\n @echo BAZ")
.justfile(
"
mod foo
alias b := foo::bar::baz
baz:
@echo 'HERE'
",
)
.arg("b")
.stdout("BAZ\n")
.run();
}

#[test]
fn unknown_nested_alias() {
Test::new()
.write("foo.just", "baz: \n @echo FOO")
.justfile(
"
mod foo
alias b := foo::bar::baz
",
)
.arg("b")
.stderr(
"\
error: Alias `b` has an unknown target `foo::bar::baz`
——▶ justfile:3:7
3 │ alias b := foo::bar::baz
│ ^
",
)
.status(EXIT_FAILURE)
.run();
}
1 change: 1 addition & 0 deletions tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ fn default<T: Default>() -> T {
#[macro_use]
mod test;

mod alias;
mod alias_style;
mod allow_duplicate_recipes;
mod allow_duplicate_variables;
Expand Down

0 comments on commit 4287e01

Please sign in to comment.