Skip to content

Commit

Permalink
Stabilize !include path as import 'path' (#1771)
Browse files Browse the repository at this point in the history
  • Loading branch information
casey authored Dec 20, 2023
1 parent f7aa201 commit e9bec8d
Show file tree
Hide file tree
Showing 19 changed files with 172 additions and 301 deletions.
28 changes: 11 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2327,50 +2327,44 @@ $ (cd foo && just a b)

And will both invoke recipes `a` and `b` in `foo/justfile`.

### Include Directives
### Imports

The `!include` directive, currently unstable, can be used to include the
verbatim text of another file.
One `justfile` can include the contents of another using an `import` statement.

If you have the following `justfile`:

```mf
!include foo/bar.just
import 'foo/bar.just'

a: b
@echo A

```

And the following text in `foo/bar.just`:

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

`foo/bar.just` will be included in `justfile` and recipe `b` will be defined:

```sh
$ just --unstable b
$ just b
B
$ just --unstable a
$ just a
B
A
```

The `!include` directive path can be absolute or relative to the location of
the justfile containing it. `!include` directives must appear at the beginning
of a line.
The `import` path can be absolute or relative to the location of the justfile
containing it.

Justfiles are insensitive to order, so included files can reference variables
and recipes defined after the `!include` directive.

`!include` directives are only processed before the first non-blank,
non-comment line.
and recipes defined after the `import` statement.

Included files can themselves contain `!include` directives, which are
processed recursively.
Imported files can themselves contain `import`s, which are processed
recursively.

### Hiding `justfile`s

Expand Down
2 changes: 1 addition & 1 deletion src/analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ impl<'src> Analyzer<'src> {
self.analyze_set(set)?;
self.sets.insert(set.clone());
}
Item::Include { absolute, .. } => {
Item::Import { absolute, .. } => {
stack.push(asts.get(absolute.as_ref().unwrap()).unwrap());
}
}
Expand Down
2 changes: 0 additions & 2 deletions src/compile_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,6 @@ impl Display for CompileError<'_> {
Count("argument", *found),
expected.display(),
),
IncludeMissingPath => write!(f, "!include directive has no argument",),
InconsistentLeadingWhitespace { expected, found } => write!(
f,
"Recipe line has inconsistent leading whitespace. Recipe started with `{}` but found \
Expand Down Expand Up @@ -203,7 +202,6 @@ impl Display for CompileError<'_> {
UnknownDependency { recipe, unknown } => {
write!(f, "Recipe `{recipe}` has unknown dependency `{unknown}`")
}
UnknownDirective { directive } => write!(f, "Unknown directive `!{directive}`"),
UnknownFunction { function } => write!(f, "Call to unknown function `{function}`"),
UnknownSetting { setting } => write!(f, "Unknown setting `{setting}`"),
UnknownStartOfToken => write!(f, "Unknown start of token:"),
Expand Down
4 changes: 0 additions & 4 deletions src/compile_error_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ pub(crate) enum CompileErrorKind<'src> {
found: usize,
expected: Range<usize>,
},
IncludeMissingPath,
InconsistentLeadingWhitespace {
expected: &'src str,
found: &'src str,
Expand Down Expand Up @@ -111,9 +110,6 @@ pub(crate) enum CompileErrorKind<'src> {
recipe: &'src str,
unknown: &'src str,
},
UnknownDirective {
directive: &'src str,
},
UnknownFunction {
function: &'src str,
},
Expand Down
34 changes: 14 additions & 20 deletions src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ pub(crate) struct Compiler;

impl Compiler {
pub(crate) fn compile<'src>(
unstable: bool,
loader: &'src Loader,
root: &Path,
) -> RunResult<'src, Compilation<'src>> {
Expand All @@ -26,18 +25,13 @@ impl Compiler {
srcs.insert(current.clone(), src);

for item in &mut ast.items {
if let Item::Include { relative, absolute } = item {
if !unstable {
return Err(Error::Unstable {
message: "The !include directive is currently unstable.".into(),
});
if let Item::Import { relative, absolute } = item {
let import = current.parent().unwrap().join(&relative.cooked).lexiclean();
if srcs.contains_key(&import) {
return Err(Error::CircularImport { current, import });
}
let include = current.parent().unwrap().join(relative).lexiclean();
if srcs.contains_key(&include) {
return Err(Error::CircularInclude { current, include });
}
*absolute = Some(include.clone());
stack.push(include);
*absolute = Some(import.clone());
stack.push(import);
}
}

Expand Down Expand Up @@ -75,14 +69,14 @@ mod tests {
fn include_justfile() {
let justfile_a = r#"
# A comment at the top of the file
!include ./justfile_b
import "./justfile_b"
#some_recipe: recipe_b
some_recipe:
echo "some recipe"
"#;

let justfile_b = r#"!include ./subdir/justfile_c
let justfile_b = r#"import "./subdir/justfile_c"
recipe_b: recipe_c
echo "recipe b"
Expand All @@ -103,7 +97,7 @@ recipe_b: recipe_c
let loader = Loader::new();

let justfile_a_path = tmp.path().join("justfile");
let compilation = Compiler::compile(true, &loader, &justfile_a_path).unwrap();
let compilation = Compiler::compile(&loader, &justfile_a_path).unwrap();

assert_eq!(compilation.root_src(), justfile_a);
}
Expand All @@ -112,15 +106,15 @@ recipe_b: recipe_c
fn recursive_includes_fail() {
let justfile_a = r#"
# A comment at the top of the file
!include ./subdir/justfile_b
import "./subdir/justfile_b"
some_recipe: recipe_b
echo "some recipe"
"#;

let justfile_b = r#"
!include ../justfile
import "../justfile"
recipe_b:
echo "recipe b"
Expand All @@ -135,11 +129,11 @@ recipe_b:
let loader = Loader::new();

let justfile_a_path = tmp.path().join("justfile");
let loader_output = Compiler::compile(true, &loader, &justfile_a_path).unwrap_err();
let loader_output = Compiler::compile(&loader, &justfile_a_path).unwrap_err();

assert_matches!(loader_output, Error::CircularInclude { current, include }
assert_matches!(loader_output, Error::CircularImport { current, import }
if current == tmp.path().join("subdir").join("justfile_b").lexiclean() &&
include == tmp.path().join("justfile").lexiclean()
import == tmp.path().join("justfile").lexiclean()
);
}
}
10 changes: 5 additions & 5 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ pub(crate) enum Error<'src> {
chooser: OsString,
io_error: io::Error,
},
CircularInclude {
CircularImport {
current: PathBuf,
include: PathBuf,
import: PathBuf,
},
Code {
recipe: &'src str,
Expand Down Expand Up @@ -263,10 +263,10 @@ impl<'src> ColorDisplay for Error<'src> {
let chooser = chooser.to_string_lossy();
write!(f, "Failed to write to chooser `{chooser}`: {io_error}")?;
}
CircularInclude { current, include } => {
let include = include.display();
CircularImport { current, import } => {
let import = import.display();
let current = current.display();
write!(f, "Include `{include}` in `{current}` is a circular include")?;
write!(f, "Import `{import}` in `{current}` is circular")?;
}
Code { recipe, line_number, code, .. } => {
if let Some(n) = line_number {
Expand Down
6 changes: 3 additions & 3 deletions src/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ pub(crate) enum Item<'src> {
Comment(&'src str),
Recipe(UnresolvedRecipe<'src>),
Set(Set<'src>),
Include {
relative: &'src str,
Import {
relative: StringLiteral<'src>,
absolute: Option<PathBuf>,
},
}
Expand All @@ -22,7 +22,7 @@ impl<'src> Display for Item<'src> {
Item::Comment(comment) => write!(f, "{comment}"),
Item::Recipe(recipe) => write!(f, "{}", recipe.color_display(Color::never())),
Item::Set(set) => write!(f, "{set}"),
Item::Include { relative, .. } => write!(f, "!include {relative}"),
Item::Import { relative, .. } => write!(f, "import {relative}"),
}
}
}
3 changes: 2 additions & 1 deletion src/keyword.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ pub(crate) enum Keyword {
False,
If,
IgnoreComments,
Import,
PositionalArguments,
Set,
Shell,
Tempdir,
True,
WindowsPowershell,
WindowsShell,
Tempdir,
}

impl Keyword {
Expand Down
59 changes: 4 additions & 55 deletions src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ impl<'src> Lexer<'src> {
fn lex_normal(&mut self, start: char) -> CompileResult<'src, ()> {
match start {
' ' | '\t' => self.lex_whitespace(),
'!' => self.lex_bang(),
'!' => self.lex_digraph('!', '=', BangEquals),
'#' => self.lex_comment(),
'$' => self.lex_single(Dollar),
'&' => self.lex_digraph('&', '&', AmpersandAmpersand),
Expand Down Expand Up @@ -685,33 +685,6 @@ impl<'src> Lexer<'src> {
!self.open_delimiters.is_empty()
}

fn lex_bang(&mut self) -> CompileResult<'src, ()> {
self.presume('!')?;

// Try to lex a `!=`
if self.accepted('=')? {
self.token(BangEquals);
return Ok(());
}

// Otherwise, lex a `!`
self.token(Bang);

if self.next.map(Self::is_identifier_start).unwrap_or_default() {
self.lex_identifier()?;

while !self.at_eol_or_eof() {
self.advance()?;
}

if self.current_token_length() > 0 {
self.token(Text);
}
}

Ok(())
}

/// Lex a two-character digraph
fn lex_digraph(&mut self, left: char, right: char, token: TokenKind) -> CompileResult<'src, ()> {
self.presume(left)?;
Expand All @@ -729,6 +702,7 @@ impl<'src> Lexer<'src> {

// …and advance past another character,
self.advance()?;

// …so that the error we produce highlights the unexpected character.
Err(self.error(UnexpectedCharacter { expected: right }))
}
Expand Down Expand Up @@ -980,7 +954,6 @@ mod tests {
AmpersandAmpersand => "&&",
Asterisk => "*",
At => "@",
Bang => "!",
BangEquals => "!=",
BraceL => "{",
BraceR => "}",
Expand Down Expand Up @@ -2091,30 +2064,6 @@ mod tests {
),
}

test! {
name: bang_eof,
text: "!",
tokens: (Bang),
}

test! {
name: character_after_bang,
text: "!{",
tokens: (Bang, BraceL)
}

test! {
name: identifier_after_bang,
text: "!include",
tokens: (Bang, Identifier:"include")
}

test! {
name: identifier_after_bang_with_more_stuff,
text: "!include some/stuff",
tokens: (Bang, Identifier:"include", Text:" some/stuff")
}

error! {
name: tokenize_space_then_tab,
input: "a:
Expand Down Expand Up @@ -2285,8 +2234,8 @@ mod tests {
}

error! {
name: unexpected_character_after_bang,
input: "!%",
name: unexpected_character_after_at,
input: "@%",
offset: 1,
line: 0,
column: 1,
Expand Down
2 changes: 1 addition & 1 deletion src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ impl<'src> Node<'src> for Item<'src> {
Item::Comment(comment) => comment.tree(),
Item::Recipe(recipe) => recipe.tree(),
Item::Set(set) => set.tree(),
Item::Include { relative, .. } => Tree::atom("include").push(format!("\"{relative}\"")),
Item::Import { relative, .. } => Tree::atom("import").push(format!("{relative}")),
}
}
}
Expand Down
Loading

0 comments on commit e9bec8d

Please sign in to comment.