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

Stabilize !include path directives as import 'path' #1771

Merged
merged 1 commit into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading