Stabilize !include path as import 'path' (#1771)

This commit is contained in:
Casey Rodarmor 2023-12-20 12:31:51 +08:00 committed by GitHub
parent f7aa201200
commit e9bec8d398
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 172 additions and 301 deletions

View File

@ -2327,24 +2327,22 @@ $ (cd foo && just a b)
And will both invoke recipes `a` and `b` in `foo/justfile`. 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 One `justfile` can include the contents of another using an `import` statement.
verbatim text of another file.
If you have the following `justfile`: If you have the following `justfile`:
```mf ```mf
!include foo/bar.just import 'foo/bar.just'
a: b a: b
@echo A @echo A
``` ```
And the following text in `foo/bar.just`: And the following text in `foo/bar.just`:
```mf ```just
b: b:
@echo B @echo B
``` ```
@ -2352,25 +2350,21 @@ b:
`foo/bar.just` will be included in `justfile` and recipe `b` will be defined: `foo/bar.just` will be included in `justfile` and recipe `b` will be defined:
```sh ```sh
$ just --unstable b $ just b
B B
$ just --unstable a $ just a
B B
A A
``` ```
The `!include` directive path can be absolute or relative to the location of The `import` path can be absolute or relative to the location of the justfile
the justfile containing it. `!include` directives must appear at the beginning containing it.
of a line.
Justfiles are insensitive to order, so included files can reference variables Justfiles are insensitive to order, so included files can reference variables
and recipes defined after the `!include` directive. and recipes defined after the `import` statement.
`!include` directives are only processed before the first non-blank, Imported files can themselves contain `import`s, which are processed
non-comment line. recursively.
Included files can themselves contain `!include` directives, which are
processed recursively.
### Hiding `justfile`s ### Hiding `justfile`s

View File

@ -53,7 +53,7 @@ impl<'src> Analyzer<'src> {
self.analyze_set(set)?; self.analyze_set(set)?;
self.sets.insert(set.clone()); self.sets.insert(set.clone());
} }
Item::Include { absolute, .. } => { Item::Import { absolute, .. } => {
stack.push(asts.get(absolute.as_ref().unwrap()).unwrap()); stack.push(asts.get(absolute.as_ref().unwrap()).unwrap());
} }
} }

View File

@ -135,7 +135,6 @@ impl Display for CompileError<'_> {
Count("argument", *found), Count("argument", *found),
expected.display(), expected.display(),
), ),
IncludeMissingPath => write!(f, "!include directive has no argument",),
InconsistentLeadingWhitespace { expected, found } => write!( InconsistentLeadingWhitespace { expected, found } => write!(
f, f,
"Recipe line has inconsistent leading whitespace. Recipe started with `{}` but found \ "Recipe line has inconsistent leading whitespace. Recipe started with `{}` but found \
@ -203,7 +202,6 @@ impl Display for CompileError<'_> {
UnknownDependency { recipe, unknown } => { UnknownDependency { recipe, unknown } => {
write!(f, "Recipe `{recipe}` has unknown dependency `{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}`"), UnknownFunction { function } => write!(f, "Call to unknown function `{function}`"),
UnknownSetting { setting } => write!(f, "Unknown setting `{setting}`"), UnknownSetting { setting } => write!(f, "Unknown setting `{setting}`"),
UnknownStartOfToken => write!(f, "Unknown start of token:"), UnknownStartOfToken => write!(f, "Unknown start of token:"),

View File

@ -58,7 +58,6 @@ pub(crate) enum CompileErrorKind<'src> {
found: usize, found: usize,
expected: Range<usize>, expected: Range<usize>,
}, },
IncludeMissingPath,
InconsistentLeadingWhitespace { InconsistentLeadingWhitespace {
expected: &'src str, expected: &'src str,
found: &'src str, found: &'src str,
@ -111,9 +110,6 @@ pub(crate) enum CompileErrorKind<'src> {
recipe: &'src str, recipe: &'src str,
unknown: &'src str, unknown: &'src str,
}, },
UnknownDirective {
directive: &'src str,
},
UnknownFunction { UnknownFunction {
function: &'src str, function: &'src str,
}, },

View File

@ -4,7 +4,6 @@ pub(crate) struct Compiler;
impl Compiler { impl Compiler {
pub(crate) fn compile<'src>( pub(crate) fn compile<'src>(
unstable: bool,
loader: &'src Loader, loader: &'src Loader,
root: &Path, root: &Path,
) -> RunResult<'src, Compilation<'src>> { ) -> RunResult<'src, Compilation<'src>> {
@ -26,18 +25,13 @@ impl Compiler {
srcs.insert(current.clone(), src); srcs.insert(current.clone(), src);
for item in &mut ast.items { for item in &mut ast.items {
if let Item::Include { relative, absolute } = item { if let Item::Import { relative, absolute } = item {
if !unstable { let import = current.parent().unwrap().join(&relative.cooked).lexiclean();
return Err(Error::Unstable { if srcs.contains_key(&import) {
message: "The !include directive is currently unstable.".into(), return Err(Error::CircularImport { current, import });
});
} }
let include = current.parent().unwrap().join(relative).lexiclean(); *absolute = Some(import.clone());
if srcs.contains_key(&include) { stack.push(import);
return Err(Error::CircularInclude { current, include });
}
*absolute = Some(include.clone());
stack.push(include);
} }
} }
@ -75,14 +69,14 @@ mod tests {
fn include_justfile() { fn include_justfile() {
let justfile_a = r#" let justfile_a = r#"
# A comment at the top of the file # A comment at the top of the file
!include ./justfile_b import "./justfile_b"
#some_recipe: recipe_b #some_recipe: recipe_b
some_recipe: some_recipe:
echo "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 recipe_b: recipe_c
echo "recipe b" echo "recipe b"
@ -103,7 +97,7 @@ recipe_b: recipe_c
let loader = Loader::new(); let loader = Loader::new();
let justfile_a_path = tmp.path().join("justfile"); 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); assert_eq!(compilation.root_src(), justfile_a);
} }
@ -112,7 +106,7 @@ recipe_b: recipe_c
fn recursive_includes_fail() { fn recursive_includes_fail() {
let justfile_a = r#" let justfile_a = r#"
# A comment at the top of the file # A comment at the top of the file
!include ./subdir/justfile_b import "./subdir/justfile_b"
some_recipe: recipe_b some_recipe: recipe_b
echo "some recipe" echo "some recipe"
@ -120,7 +114,7 @@ some_recipe: recipe_b
"#; "#;
let justfile_b = r#" let justfile_b = r#"
!include ../justfile import "../justfile"
recipe_b: recipe_b:
echo "recipe b" echo "recipe b"
@ -135,11 +129,11 @@ recipe_b:
let loader = Loader::new(); let loader = Loader::new();
let justfile_a_path = tmp.path().join("justfile"); 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() && if current == tmp.path().join("subdir").join("justfile_b").lexiclean() &&
include == tmp.path().join("justfile").lexiclean() import == tmp.path().join("justfile").lexiclean()
); );
} }
} }

View File

@ -31,9 +31,9 @@ pub(crate) enum Error<'src> {
chooser: OsString, chooser: OsString,
io_error: io::Error, io_error: io::Error,
}, },
CircularInclude { CircularImport {
current: PathBuf, current: PathBuf,
include: PathBuf, import: PathBuf,
}, },
Code { Code {
recipe: &'src str, recipe: &'src str,
@ -263,10 +263,10 @@ impl<'src> ColorDisplay for Error<'src> {
let chooser = chooser.to_string_lossy(); let chooser = chooser.to_string_lossy();
write!(f, "Failed to write to chooser `{chooser}`: {io_error}")?; write!(f, "Failed to write to chooser `{chooser}`: {io_error}")?;
} }
CircularInclude { current, include } => { CircularImport { current, import } => {
let include = include.display(); let import = import.display();
let current = current.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, .. } => { Code { recipe, line_number, code, .. } => {
if let Some(n) = line_number { if let Some(n) = line_number {

View File

@ -8,8 +8,8 @@ pub(crate) enum Item<'src> {
Comment(&'src str), Comment(&'src str),
Recipe(UnresolvedRecipe<'src>), Recipe(UnresolvedRecipe<'src>),
Set(Set<'src>), Set(Set<'src>),
Include { Import {
relative: &'src str, relative: StringLiteral<'src>,
absolute: Option<PathBuf>, absolute: Option<PathBuf>,
}, },
} }
@ -22,7 +22,7 @@ impl<'src> Display for Item<'src> {
Item::Comment(comment) => write!(f, "{comment}"), Item::Comment(comment) => write!(f, "{comment}"),
Item::Recipe(recipe) => write!(f, "{}", recipe.color_display(Color::never())), Item::Recipe(recipe) => write!(f, "{}", recipe.color_display(Color::never())),
Item::Set(set) => write!(f, "{set}"), Item::Set(set) => write!(f, "{set}"),
Item::Include { relative, .. } => write!(f, "!include {relative}"), Item::Import { relative, .. } => write!(f, "import {relative}"),
} }
} }
} }

View File

@ -14,13 +14,14 @@ pub(crate) enum Keyword {
False, False,
If, If,
IgnoreComments, IgnoreComments,
Import,
PositionalArguments, PositionalArguments,
Set, Set,
Shell, Shell,
Tempdir,
True, True,
WindowsPowershell, WindowsPowershell,
WindowsShell, WindowsShell,
Tempdir,
} }
impl Keyword { impl Keyword {

View File

@ -481,7 +481,7 @@ impl<'src> Lexer<'src> {
fn lex_normal(&mut self, start: char) -> CompileResult<'src, ()> { fn lex_normal(&mut self, start: char) -> CompileResult<'src, ()> {
match start { match start {
' ' | '\t' => self.lex_whitespace(), ' ' | '\t' => self.lex_whitespace(),
'!' => self.lex_bang(), '!' => self.lex_digraph('!', '=', BangEquals),
'#' => self.lex_comment(), '#' => self.lex_comment(),
'$' => self.lex_single(Dollar), '$' => self.lex_single(Dollar),
'&' => self.lex_digraph('&', '&', AmpersandAmpersand), '&' => self.lex_digraph('&', '&', AmpersandAmpersand),
@ -685,33 +685,6 @@ impl<'src> Lexer<'src> {
!self.open_delimiters.is_empty() !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 /// Lex a two-character digraph
fn lex_digraph(&mut self, left: char, right: char, token: TokenKind) -> CompileResult<'src, ()> { fn lex_digraph(&mut self, left: char, right: char, token: TokenKind) -> CompileResult<'src, ()> {
self.presume(left)?; self.presume(left)?;
@ -729,6 +702,7 @@ impl<'src> Lexer<'src> {
// …and advance past another character, // …and advance past another character,
self.advance()?; self.advance()?;
// …so that the error we produce highlights the unexpected character. // …so that the error we produce highlights the unexpected character.
Err(self.error(UnexpectedCharacter { expected: right })) Err(self.error(UnexpectedCharacter { expected: right }))
} }
@ -980,7 +954,6 @@ mod tests {
AmpersandAmpersand => "&&", AmpersandAmpersand => "&&",
Asterisk => "*", Asterisk => "*",
At => "@", At => "@",
Bang => "!",
BangEquals => "!=", BangEquals => "!=",
BraceL => "{", BraceL => "{",
BraceR => "}", BraceR => "}",
@ -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! { error! {
name: tokenize_space_then_tab, name: tokenize_space_then_tab,
input: "a: input: "a:
@ -2285,8 +2234,8 @@ mod tests {
} }
error! { error! {
name: unexpected_character_after_bang, name: unexpected_character_after_at,
input: "!%", input: "@%",
offset: 1, offset: 1,
line: 0, line: 0,
column: 1, column: 1,

View File

@ -23,7 +23,7 @@ impl<'src> Node<'src> for Item<'src> {
Item::Comment(comment) => comment.tree(), Item::Comment(comment) => comment.tree(),
Item::Recipe(recipe) => recipe.tree(), Item::Recipe(recipe) => recipe.tree(),
Item::Set(set) => set.tree(), Item::Set(set) => set.tree(),
Item::Include { relative, .. } => Tree::atom("include").push(format!("\"{relative}\"")), Item::Import { relative, .. } => Tree::atom("import").push(format!("{relative}")),
} }
} }
} }

View File

@ -328,6 +328,13 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
self.presume_keyword(Keyword::Export)?; self.presume_keyword(Keyword::Export)?;
items.push(Item::Assignment(self.parse_assignment(true)?)); items.push(Item::Assignment(self.parse_assignment(true)?));
} }
Some(Keyword::Import) if self.next_are(&[Identifier, StringToken]) => {
self.presume_keyword(Keyword::Import)?;
items.push(Item::Import {
relative: self.parse_string_literal()?,
absolute: None,
});
}
Some(Keyword::Set) Some(Keyword::Set)
if self.next_are(&[Identifier, Identifier, ColonEquals]) if self.next_are(&[Identifier, Identifier, ColonEquals])
|| self.next_are(&[Identifier, Identifier, Comment, Eof]) || self.next_are(&[Identifier, Identifier, Comment, Eof])
@ -350,9 +357,6 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
} }
} }
} }
} else if self.next_is(Bang) {
let directive = self.parse_directive()?;
items.push(directive);
} else if self.accepted(At)? { } else if self.accepted(At)? {
let doc = pop_doc_comment(&mut items, eol_since_last_comment); let doc = pop_doc_comment(&mut items, eol_since_last_comment);
items.push(Item::Recipe(self.parse_recipe( items.push(Item::Recipe(self.parse_recipe(
@ -778,24 +782,6 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
Ok(value) Ok(value)
} }
fn parse_directive(&mut self) -> CompileResult<'src, Item<'src>> {
self.presume(Bang)?;
let name = self.expect(Identifier)?;
match name.lexeme() {
"include" => {
if let Some(include_line) = self.accept(Text)? {
Ok(Item::Include {
relative: include_line.lexeme().trim(),
absolute: None,
})
} else {
Err(self.error(CompileErrorKind::IncludeMissingPath)?)
}
}
directive => Err(name.error(CompileErrorKind::UnknownDirective { directive })),
}
}
/// Parse a setting /// Parse a setting
fn parse_set(&mut self) -> CompileResult<'src, Set<'src>> { fn parse_set(&mut self) -> CompileResult<'src, Set<'src>> {
self.presume_keyword(Keyword::Set)?; self.presume_keyword(Keyword::Set)?;
@ -1981,9 +1967,9 @@ mod tests {
} }
test! { test! {
name: include_directive, name: import,
text: "!include some/file/path.txt \n", text: "import \"some/file/path.txt\" \n",
tree: (justfile (include "some/file/path.txt")), tree: (justfile (import "some/file/path.txt")),
} }
error! { error! {
@ -2076,7 +2062,7 @@ mod tests {
column: 0, column: 0,
width: 1, width: 1,
kind: UnexpectedToken { kind: UnexpectedToken {
expected: vec![At, Bang, BracketL, Comment, Eof, Eol, Identifier], expected: vec![At, BracketL, Comment, Eof, Eol, Identifier],
found: BraceL, found: BraceL,
}, },
} }
@ -2428,16 +2414,4 @@ mod tests {
expected: 3..3, expected: 3..3,
}, },
} }
error! {
name: unknown_directive,
input: "!inclood",
offset: 1,
line: 0,
column: 1,
width: 7,
kind: UnknownDirective {
directive: "inclood"
},
}
} }

View File

@ -180,7 +180,7 @@ impl Subcommand {
loader: &'src Loader, loader: &'src Loader,
search: &Search, search: &Search,
) -> Result<Compilation<'src>, Error<'src>> { ) -> Result<Compilation<'src>, Error<'src>> {
let compilation = Compiler::compile(config.unstable, loader, &search.justfile)?; let compilation = Compiler::compile(loader, &search.justfile)?;
if config.verbosity.loud() { if config.verbosity.loud() {
for warning in &compilation.justfile.warnings { for warning in &compilation.justfile.warnings {

View File

@ -28,7 +28,7 @@ mod full {
pub fn summary(path: &Path) -> Result<Result<Summary, String>, io::Error> { pub fn summary(path: &Path) -> Result<Result<Summary, String>, io::Error> {
let loader = Loader::new(); let loader = Loader::new();
match Compiler::compile(false, &loader, path) { match Compiler::compile(&loader, path) {
Ok(compilation) => Ok(Ok(Summary::new(&compilation.justfile))), Ok(compilation) => Ok(Ok(Summary::new(&compilation.justfile))),
Err(error) => Ok(Err(if let Error::Compile { compile_error } = error { Err(error) => Ok(Err(if let Error::Compile { compile_error } = error {
compile_error.to_string() compile_error.to_string()

View File

@ -6,7 +6,6 @@ pub(crate) enum TokenKind {
Asterisk, Asterisk,
At, At,
Backtick, Backtick,
Bang,
BangEquals, BangEquals,
BraceL, BraceL,
BraceR, BraceR,
@ -49,7 +48,6 @@ impl Display for TokenKind {
Asterisk => "'*'", Asterisk => "'*'",
At => "'@'", At => "'@'",
Backtick => "backtick", Backtick => "backtick",
Bang => "'!'",
BangEquals => "'!='", BangEquals => "'!='",
BraceL => "'{'", BraceL => "'{'",
BraceR => "'}'", BraceR => "'}'",

View File

@ -26,7 +26,7 @@ fn non_leading_byte_order_mark_produces_error() {
) )
.stderr( .stderr(
" "
error: Expected \'@\', '!', \'[\', comment, end of file, end of line, or identifier, but found byte order mark error: Expected \'@\', \'[\', comment, end of file, end of line, or identifier, but found byte order mark
--> justfile:3:1 --> justfile:3:1
| |
3 | \u{feff} 3 | \u{feff}
@ -42,7 +42,7 @@ fn dont_mention_byte_order_mark_in_errors() {
.justfile("{") .justfile("{")
.stderr( .stderr(
" "
error: Expected '@', '!', '[', comment, end of file, end of line, or identifier, but found '{' error: Expected '@', '[', comment, end of file, end of line, or identifier, but found '{'
--> justfile:1:1 --> justfile:1:1
| |
1 | { 1 | {

View File

@ -75,9 +75,8 @@ error: Expected '*', ':', '$', identifier, or '+', but found end of file
#[test] #[test]
fn file_paths_are_relative() { fn file_paths_are_relative() {
Test::new() Test::new()
.justfile("!include foo/bar.just") .justfile("import 'foo/bar.just'")
.write("foo/bar.just", "baz") .write("foo/bar.just", "baz")
.args(["--unstable"])
.status(EXIT_FAILURE) .status(EXIT_FAILURE)
.stderr(format!( .stderr(format!(
" "
@ -95,10 +94,10 @@ error: Expected '*', ':', '$', identifier, or '+', but found end of file
#[test] #[test]
fn file_paths_not_in_subdir_are_absolute() { fn file_paths_not_in_subdir_are_absolute() {
Test::new() Test::new()
.write("foo/justfile", "!include ../bar.just") .write("foo/justfile", "import '../bar.just'")
.write("bar.just", "baz") .write("bar.just", "baz")
.no_justfile() .no_justfile()
.args(["--unstable", "--justfile", "foo/justfile"]) .args(["--justfile", "foo/justfile"])
.status(EXIT_FAILURE) .status(EXIT_FAILURE)
.stderr_regex(format!( .stderr_regex(format!(
" "

112
tests/imports.rs Normal file
View File

@ -0,0 +1,112 @@
use super::*;
#[test]
fn import_succeeds() {
Test::new()
.tree(tree! {
"import.justfile": "
b:
@echo B
",
})
.justfile(
"
import './import.justfile'
a: b
@echo A
",
)
.test_round_trip(false)
.arg("a")
.stdout("B\nA\n")
.run();
}
#[test]
fn trailing_spaces_after_import_are_ignored() {
Test::new()
.tree(tree! {
"import.justfile": "",
})
.justfile(
"
import './import.justfile'\x20
a:
@echo A
",
)
.test_round_trip(false)
.stdout("A\n")
.run();
}
#[test]
fn import_after_recipe() {
Test::new()
.tree(tree! {
"import.justfile": "
a:
@echo A
",
})
.justfile(
"
b: a
import './import.justfile'
",
)
.test_round_trip(false)
.stdout("A\n")
.run();
}
#[test]
fn circular_import() {
Test::new()
.justfile("import 'a'")
.tree(tree! {
a: "import 'b'",
b: "import 'a'",
})
.status(EXIT_FAILURE)
.stderr_regex(path_for_regex(
"error: Import `.*/a` in `.*/b` is circular\n",
))
.run();
}
#[test]
fn import_recipes_are_not_default() {
Test::new()
.tree(tree! {
"import.justfile": "bar:",
})
.justfile("import './import.justfile'")
.test_round_trip(false)
.status(EXIT_FAILURE)
.stderr("error: Justfile contains no default recipe.\n")
.run();
}
#[test]
fn listed_recipes_in_imports_are_in_load_order() {
Test::new()
.justfile(
"
import './import.justfile'
foo:
",
)
.write("import.justfile", "bar:")
.args(["--list", "--unsorted"])
.test_round_trip(false)
.stdout(
"
Available recipes:
foo
bar
",
)
.run();
}

View File

@ -1,144 +0,0 @@
use super::*;
#[test]
fn include_fails_without_unstable() {
Test::new()
.justfile("!include ./include.justfile")
.status(EXIT_FAILURE)
.stderr("error: The !include directive is currently unstable. Invoke `just` with the `--unstable` flag to enable unstable features.\n")
.run();
}
#[test]
fn include_succeeds_with_unstable() {
Test::new()
.tree(tree! {
"include.justfile": "
b:
@echo B
",
})
.justfile(
"
!include ./include.justfile
a: b
@echo A
",
)
.arg("--unstable")
.test_round_trip(false)
.arg("a")
.stdout("B\nA\n")
.run();
}
#[test]
fn trailing_spaces_after_include_are_ignored() {
Test::new()
.tree(tree! {
"include.justfile": "",
})
.justfile(
"
!include ./include.justfile\x20
a:
@echo A
",
)
.arg("--unstable")
.test_round_trip(false)
.stdout("A\n")
.run();
}
#[test]
fn include_directive_with_no_path() {
Test::new()
.justfile("!include")
.arg("--unstable")
.status(EXIT_FAILURE)
.stderr(
"
error: !include directive has no argument
--> justfile:1:9
|
1 | !include
| ^
",
)
.run();
}
#[test]
fn include_after_recipe() {
Test::new()
.tree(tree! {
"include.justfile": "
a:
@echo A
",
})
.justfile(
"
b: a
!include ./include.justfile
",
)
.arg("--unstable")
.test_round_trip(false)
.stdout("A\n")
.run();
}
#[test]
fn circular_include() {
Test::new()
.justfile("!include a")
.tree(tree! {
a: "!include b",
b: "!include a",
})
.arg("--unstable")
.status(EXIT_FAILURE)
.stderr_regex(path_for_regex(
"error: Include `.*/a` in `.*/b` is a circular include\n",
))
.run();
}
#[test]
fn include_recipes_are_not_default() {
Test::new()
.tree(tree! {
"include.justfile": "bar:",
})
.justfile("!include ./include.justfile")
.arg("--unstable")
.test_round_trip(false)
.status(EXIT_FAILURE)
.stderr("error: Justfile contains no default recipe.\n")
.run();
}
#[test]
fn listed_recipes_in_includes_are_in_load_order() {
Test::new()
.justfile(
"
!include ./include.justfile
foo:
",
)
.write("include.justfile", "bar:")
.args(["--list", "--unstable", "--unsorted"])
.test_round_trip(false)
.stdout(
"
Available recipes:
foo
bar
",
)
.run();
}

View File

@ -55,7 +55,7 @@ mod fallback;
mod fmt; mod fmt;
mod functions; mod functions;
mod ignore_comments; mod ignore_comments;
mod includes; mod imports;
mod init; mod init;
#[cfg(unix)] #[cfg(unix)]
mod interrupts; mod interrupts;