Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add modules #1782

Merged
merged 20 commits into from
Dec 28, 2023
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2329,7 +2329,7 @@ And will both invoke recipes `a` and `b` in `foo/justfile`.

### Imports

One `justfile` can include the contents of another using an `import` statement.
One `justfile` can include the contents of another using `import` statements.

If you have the following `justfile`:

Expand Down Expand Up @@ -2366,6 +2366,51 @@ and recipes defined after the `import` statement.
Imported files can themselves contain `import`s, which are processed
recursively.

### Modules<sup>master</sup>

A `justfile` can declare modules using `mod` statements. `mod` statements are
currently unstable, so you'll need to use the `--unstable` flag, or set the
`JUST_UNSTABLE` environment variable to use them.

If you have the following `justfile`:

```mf
mod bar

a:
@echo A
```

And the following text in `bar.just`:

```just
b:
@echo B
```

`bar.just` will be included in `justfile` as a submodule. Recipes, aliases, and
variables defined in one submodule cannot be used in another, and each module
uses its own settings.

Recipes in submodules can be invoked as subcommands:

```sh
$ just --unstable bar b
B
```

If a module is named `foo`, just will search for the module file in `foo.just`,
`foo/mod.just`, `foo/justfile`, and `foo/.justfile`. In the latter two cases,
the module file may have any capitalization.

Environment files are loaded for the root justfile.

Currently, recipes in submodules run with the same working directory as the
root `justfile`, and the `justfile()` and `justfile_directory()` functions
return the path to the root `justfile` and its parent directory.

See the [module stabilization tracking issue](https://github.com/casey/just/issues/929) for more information.

### Hiding `justfile`s

`just` looks for `justfile`s named `justfile` and `.justfile`, which can be used to keep a `justfile` hidden.
Expand Down
4 changes: 0 additions & 4 deletions src/alias.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ pub(crate) struct Alias<'src, T = Rc<Recipe<'src>>> {
}

impl<'src> Alias<'src, Name<'src>> {
pub(crate) fn line_number(&self) -> usize {
self.name.line
}

pub(crate) fn resolve(self, target: Rc<Recipe<'src>>) -> Alias<'src> {
assert_eq!(self.target.lexeme(), target.name.lexeme());

Expand Down
91 changes: 61 additions & 30 deletions src/analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub(crate) struct Analyzer<'src> {

impl<'src> Analyzer<'src> {
pub(crate) fn analyze(
loaded: Vec<PathBuf>,
loaded: &[PathBuf],
paths: &HashMap<PathBuf, PathBuf>,
asts: &HashMap<PathBuf, Ast<'src>>,
root: &Path,
Expand All @@ -19,7 +19,7 @@ impl<'src> Analyzer<'src> {

fn justfile(
mut self,
loaded: Vec<PathBuf>,
loaded: &[PathBuf],
paths: &HashMap<PathBuf, PathBuf>,
asts: &HashMap<PathBuf, Ast<'src>>,
root: &Path,
Expand All @@ -31,18 +31,62 @@ impl<'src> Analyzer<'src> {

let mut warnings = Vec::new();

let mut modules: BTreeMap<String, (Name, Justfile)> = BTreeMap::new();

let mut definitions: HashMap<&str, (&'static str, Name)> = HashMap::new();

let mut define = |name: Name<'src>,
second_type: &'static str,
duplicates_allowed: bool|
-> CompileResult<'src, ()> {
if let Some((first_type, original)) = definitions.get(name.lexeme()) {
if !(*first_type == second_type && duplicates_allowed) {
let (original, redefinition) = if name.line < original.line {
(name, *original)
} else {
(*original, name)
};

return Err(redefinition.token().error(Redefinition {
first_type,
second_type,
name: name.lexeme(),
first: original.line,
}));
}
}

definitions.insert(name.lexeme(), (second_type, name));

Ok(())
};

while let Some(ast) = stack.pop() {
for item in &ast.items {
match item {
Item::Alias(alias) => {
self.analyze_alias(alias)?;
define(alias.name, "alias", false)?;
Self::analyze_alias(alias)?;
self.aliases.insert(alias.clone());
}
Item::Assignment(assignment) => {
self.analyze_assignment(assignment)?;
self.assignments.insert(assignment.clone());
}
Item::Comment(_) => (),
Item::Import { absolute, .. } => {
stack.push(asts.get(absolute.as_ref().unwrap()).unwrap());
}
Item::Mod { absolute, name } => {
define(*name, "module", false)?;
modules.insert(
name.to_string(),
(
*name,
Self::analyze(loaded, paths, asts, absolute.as_ref().unwrap())?,
),
);
}
Item::Recipe(recipe) => {
if recipe.enabled() {
Self::analyze_recipe(recipe)?;
Expand All @@ -53,9 +97,6 @@ impl<'src> Analyzer<'src> {
self.analyze_set(set)?;
self.sets.insert(set.clone());
}
Item::Import { absolute, .. } => {
stack.push(asts.get(absolute.as_ref().unwrap()).unwrap());
}
}
}

Expand All @@ -69,14 +110,7 @@ impl<'src> Analyzer<'src> {
AssignmentResolver::resolve_assignments(&self.assignments)?;

for recipe in recipes {
if let Some(original) = recipe_table.get(recipe.name.lexeme()) {
if !settings.allow_duplicate_recipes {
return Err(recipe.name.token().error(DuplicateRecipe {
recipe: original.name(),
first: original.line_number(),
}));
}
}
define(recipe.name, "recipe", settings.allow_duplicate_recipes)?;
recipe_table.insert(recipe.clone());
}

Expand All @@ -103,10 +137,14 @@ impl<'src> Analyzer<'src> {
}),
aliases,
assignments: self.assignments,
loaded,
loaded: loaded.into(),
recipes,
settings,
warnings,
modules: modules
.into_iter()
.map(|(name, (_name, justfile))| (name, justfile))
.collect(),
})
}

Expand Down Expand Up @@ -164,16 +202,9 @@ impl<'src> Analyzer<'src> {
Ok(())
}

fn analyze_alias(&self, alias: &Alias<'src, Name<'src>>) -> CompileResult<'src, ()> {
fn analyze_alias(alias: &Alias<'src, Name<'src>>) -> CompileResult<'src, ()> {
let name = alias.name.lexeme();

if let Some(original) = self.aliases.get(name) {
return Err(alias.name.token().error(DuplicateAlias {
alias: name,
first: original.line_number(),
}));
}

for attr in &alias.attributes {
if *attr != Attribute::Private {
return Err(alias.name.token().error(AliasInvalidAttribute {
Expand Down Expand Up @@ -232,7 +263,7 @@ mod tests {
line: 1,
column: 6,
width: 3,
kind: DuplicateAlias { alias: "foo", first: 0 },
kind: Redefinition { first_type: "alias", second_type: "alias", name: "foo", first: 0 },
}

analysis_error! {
Expand All @@ -248,11 +279,11 @@ mod tests {
analysis_error! {
name: alias_shadows_recipe_before,
input: "bar: \n echo bar\nalias foo := bar\nfoo:\n echo foo",
offset: 23,
line: 2,
column: 6,
offset: 34,
line: 3,
column: 0,
width: 3,
kind: AliasShadowsRecipe {alias: "foo", recipe_line: 3},
kind: Redefinition { first_type: "alias", second_type: "recipe", name: "foo", first: 2 },
}

analysis_error! {
Expand All @@ -262,7 +293,7 @@ mod tests {
line: 2,
column: 6,
width: 3,
kind: AliasShadowsRecipe { alias: "foo", recipe_line: 0 },
kind: Redefinition { first_type: "alias", second_type: "recipe", name: "foo", first: 0 },
}

analysis_error! {
Expand Down Expand Up @@ -302,7 +333,7 @@ mod tests {
line: 2,
column: 0,
width: 1,
kind: DuplicateRecipe{recipe: "a", first: 0},
kind: Redefinition { first_type: "recipe", second_type: "recipe", name: "a", first: 0 },
}

analysis_error! {
Expand Down
45 changes: 33 additions & 12 deletions src/compile_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ impl<'src> CompileError<'src> {
}
}

fn capitalize(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
}

impl Display for CompileError<'_> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
use CompileErrorKind::*;
Expand Down Expand Up @@ -82,12 +90,6 @@ impl Display for CompileError<'_> {
write!(f, "at most {max} {}", Count("argument", *max))
}
}
DuplicateAlias { alias, first } => write!(
f,
"Alias `{alias}` first defined on line {} is redefined on line {}",
first.ordinal(),
self.token.line.ordinal(),
),
DuplicateAttribute { attribute, first } => write!(
f,
"Recipe attribute `{attribute}` first used on line {} is duplicated on line {}",
Expand All @@ -97,12 +99,6 @@ impl Display for CompileError<'_> {
DuplicateParameter { recipe, parameter } => {
write!(f, "Recipe `{recipe}` has duplicate parameter `{parameter}`")
}
DuplicateRecipe { recipe, first } => write!(
f,
"Recipe `{recipe}` first defined on line {} is redefined on line {}",
first.ordinal(),
self.token.line.ordinal(),
),
DuplicateSet { setting, first } => write!(
f,
"Setting `{setting}` first set on line {} is redefined on line {}",
Expand Down Expand Up @@ -183,6 +179,31 @@ impl Display for CompileError<'_> {
write!(f, "Parameter `{parameter}` follows variadic parameter")
}
ParsingRecursionDepthExceeded => write!(f, "Parsing recursion depth exceeded"),
Redefinition {
first,
first_type,
name,
second_type,
} => {
if first_type == second_type {
write!(
f,
"{} `{name}` first defined on line {} is redefined on line {}",
capitalize(first_type),
first.ordinal(),
self.token.line.ordinal(),
)
} else {
write!(
f,
"{} `{name}` defined on line {} is redefined as {} {second_type} on line {}",
capitalize(first_type),
first.ordinal(),
if *second_type == "alias" { "an" } else { "a" },
self.token.line.ordinal(),
)
}
}
RequiredParameterFollowsDefaultParameter { parameter } => write!(
f,
"Non-default parameter `{parameter}` follows default parameter"
Expand Down
10 changes: 4 additions & 6 deletions src/compile_error_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ pub(crate) enum CompileErrorKind<'src> {
min: usize,
max: usize,
},
DuplicateAlias {
alias: &'src str,
Redefinition {
first: usize,
first_type: &'static str,
name: &'src str,
second_type: &'static str,
},
DuplicateAttribute {
attribute: &'src str,
Expand All @@ -37,10 +39,6 @@ pub(crate) enum CompileErrorKind<'src> {
recipe: &'src str,
parameter: &'src str,
},
DuplicateRecipe {
recipe: &'src str,
first: usize,
},
DuplicateSet {
setting: &'src str,
first: usize,
Expand Down
Loading
Loading