Stabilize !include path
as import 'path'
(#1771)
This commit is contained in:
parent
f7aa201200
commit
e9bec8d398
28
README.md
28
README.md
@ -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
|
||||||
|
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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:"),
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
|
@ -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()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
src/error.rs
10
src/error.rs
@ -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 {
|
||||||
|
@ -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}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
59
src/lexer.rs
59
src/lexer.rs
@ -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,
|
||||||
|
@ -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}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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()
|
||||||
|
@ -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 => "'}'",
|
||||||
|
@ -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 | {
|
||||||
|
@ -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
112
tests/imports.rs
Normal 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();
|
||||||
|
}
|
@ -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();
|
|
||||||
}
|
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user