Optional modules and imports (#1797)

This commit is contained in:
Casey Rodarmor 2023-12-29 12:16:31 -08:00 committed by GitHub
parent 177bb682b8
commit e2c0d86bdd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1046 additions and 352 deletions

View File

@ -43,12 +43,14 @@ grammar
```
justfile : item* EOF
item : recipe
| alias
item : alias
| assignment
| export
| setting
| eol
| export
| import
| module
| recipe
| setting
eol : NEWLINE
| COMMENT NEWLINE
@ -72,6 +74,10 @@ setting : 'set' 'allow-duplicate-recipes' boolean?
| 'set' 'windows-powershell' boolean?
| 'set' 'windows-shell' ':=' '[' string (',' string)* ','? ']'
import : 'import' '?'? string?
module : 'mod' '?'? NAME string?
boolean : ':=' ('true' | 'false')
expression : 'if' condition '{' expression '}' 'else' '{' expression '}'

885
README.md

File diff suppressed because it is too large Load Diff

View File

@ -38,7 +38,7 @@ impl<'src> Analyzer<'src> {
let mut define = |name: Name<'src>,
second_type: &'static str,
duplicates_allowed: bool|
-> CompileResult<'src, ()> {
-> 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 {
@ -75,18 +75,19 @@ impl<'src> Analyzer<'src> {
}
Item::Comment(_) => (),
Item::Import { absolute, .. } => {
stack.push(asts.get(absolute.as_ref().unwrap()).unwrap());
if let Some(absolute) = absolute {
stack.push(asts.get(absolute).unwrap());
}
Item::Mod { absolute, name, .. } => {
}
Item::Module { absolute, name, .. } => {
if let Some(absolute) = absolute {
define(*name, "module", false)?;
modules.insert(
name.to_string(),
(
*name,
Self::analyze(loaded, paths, asts, absolute.as_ref().unwrap())?,
),
(*name, Self::analyze(loaded, paths, asts, absolute)?),
);
}
}
Item::Recipe(recipe) => {
if recipe.enabled() {
Self::analyze_recipe(recipe)?;
@ -153,7 +154,7 @@ impl<'src> Analyzer<'src> {
})
}
fn analyze_recipe(recipe: &UnresolvedRecipe<'src>) -> CompileResult<'src, ()> {
fn analyze_recipe(recipe: &UnresolvedRecipe<'src>) -> CompileResult<'src> {
let mut parameters = BTreeSet::new();
let mut passed_default = false;
@ -198,7 +199,7 @@ impl<'src> Analyzer<'src> {
Ok(())
}
fn analyze_assignment(&self, assignment: &Assignment<'src>) -> CompileResult<'src, ()> {
fn analyze_assignment(&self, assignment: &Assignment<'src>) -> CompileResult<'src> {
if self.assignments.contains_key(assignment.name.lexeme()) {
return Err(assignment.name.token().error(DuplicateVariable {
variable: assignment.name.lexeme(),
@ -207,7 +208,7 @@ impl<'src> Analyzer<'src> {
Ok(())
}
fn analyze_alias(alias: &Alias<'src, Name<'src>>) -> CompileResult<'src, ()> {
fn analyze_alias(alias: &Alias<'src, Name<'src>>) -> CompileResult<'src> {
let name = alias.name.lexeme();
for attr in &alias.attributes {
@ -222,7 +223,7 @@ impl<'src> Analyzer<'src> {
Ok(())
}
fn analyze_set(&self, set: &Set<'src>) -> CompileResult<'src, ()> {
fn analyze_set(&self, set: &Set<'src>) -> CompileResult<'src> {
if let Some(original) = self.sets.get(set.name.lexeme()) {
return Err(set.name.error(DuplicateSet {
setting: original.name.lexeme(),

View File

@ -9,7 +9,7 @@ pub(crate) struct AssignmentResolver<'src: 'run, 'run> {
impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
pub(crate) fn resolve_assignments(
assignments: &'run Table<'src, Assignment<'src>>,
) -> CompileResult<'src, ()> {
) -> CompileResult<'src> {
let mut resolver = Self {
stack: Vec::new(),
evaluated: BTreeSet::new(),
@ -23,7 +23,7 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
Ok(())
}
fn resolve_assignment(&mut self, name: &'src str) -> CompileResult<'src, ()> {
fn resolve_assignment(&mut self, name: &'src str) -> CompileResult<'src> {
if self.evaluated.contains(name) {
return Ok(());
}
@ -52,7 +52,7 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
Ok(())
}
fn resolve_expression(&mut self, expression: &Expression<'src>) -> CompileResult<'src, ()> {
fn resolve_expression(&mut self, expression: &Expression<'src>) -> CompileResult<'src> {
match expression {
Expression::Variable { name } => {
let variable = name.lexeme();

View File

@ -27,10 +27,11 @@ impl Compiler {
for item in &mut ast.items {
match item {
Item::Mod {
name,
Item::Module {
absolute,
path,
name,
optional,
relative,
} => {
if !unstable {
return Err(Error::Unstable {
@ -40,29 +41,49 @@ impl Compiler {
let parent = current.parent().unwrap();
let import = if let Some(path) = path {
parent.join(Self::expand_tilde(&path.cooked)?)
let import = if let Some(relative) = relative {
let path = parent.join(Self::expand_tilde(&relative.cooked)?);
if path.is_file() {
Some(path)
} else {
None
}
} else {
Self::find_module_file(parent, *name)?
};
if let Some(import) = import {
if srcs.contains_key(&import) {
return Err(Error::CircularImport { current, import });
}
*absolute = Some(import.clone());
stack.push((import, depth + 1));
} else if !*optional {
return Err(Error::MissingModuleFile { module: *name });
}
Item::Import { relative, absolute } => {
}
Item::Import {
relative,
absolute,
optional,
path,
} => {
let import = current
.parent()
.unwrap()
.join(Self::expand_tilde(&relative.cooked)?)
.lexiclean();
if import.is_file() {
if srcs.contains_key(&import) {
return Err(Error::CircularImport { current, import });
}
*absolute = Some(import.clone());
stack.push((import, depth + 1));
} else if !*optional {
return Err(Error::MissingImportFile { path: *path });
}
}
_ => {}
}
@ -81,7 +102,7 @@ impl Compiler {
})
}
fn find_module_file<'src>(parent: &Path, module: Name<'src>) -> RunResult<'src, PathBuf> {
fn find_module_file<'src>(parent: &Path, module: Name<'src>) -> RunResult<'src, Option<PathBuf>> {
let mut candidates = vec![format!("{module}.just"), format!("{module}/mod.just")]
.into_iter()
.filter(|path| parent.join(path).is_file())
@ -112,8 +133,8 @@ impl Compiler {
}
match candidates.as_slice() {
[] => Err(Error::MissingModuleFile { module }),
[file] => Ok(parent.join(file).lexiclean()),
[] => Ok(None),
[file] => Ok(Some(parent.join(file).lexiclean())),
found => Err(Error::AmbiguousModuleFile {
found: found.into(),
module,

View File

@ -110,6 +110,9 @@ pub(crate) enum Error<'src> {
path: PathBuf,
io_error: io::Error,
},
MissingImportFile {
path: Token<'src>,
},
MissingModuleFile {
module: Name<'src>,
},
@ -181,6 +184,7 @@ impl<'src> Error<'src> {
Self::Backtick { token, .. } => Some(*token),
Self::Compile { compile_error } => Some(compile_error.context()),
Self::FunctionCall { function, .. } => Some(function.token()),
Self::MissingImportFile { path } => Some(*path),
_ => None,
}
}
@ -369,6 +373,7 @@ impl<'src> ColorDisplay for Error<'src> {
let path = path.display();
write!(f, "Failed to read justfile at `{path}`: {io_error}")?;
}
MissingImportFile { .. } => write!(f, "Could not find source file for import.")?,
MissingModuleFile { module } => write!(f, "Could not find source file for module `{module}`.")?,
NoChoosableRecipes => write!(f, "Justfile contains no choosable recipes.")?,
NoDefaultRecipe => write!(f, "Justfile contains no default recipe.")?,

View File

@ -7,13 +7,16 @@ pub(crate) enum Item<'src> {
Assignment(Assignment<'src>),
Comment(&'src str),
Import {
absolute: Option<PathBuf>,
optional: bool,
path: Token<'src>,
relative: StringLiteral<'src>,
absolute: Option<PathBuf>,
},
Mod {
name: Name<'src>,
Module {
absolute: Option<PathBuf>,
path: Option<StringLiteral<'src>>,
name: Name<'src>,
optional: bool,
relative: Option<StringLiteral<'src>>,
},
Recipe(UnresolvedRecipe<'src>),
Set(Set<'src>),
@ -25,11 +28,32 @@ impl<'src> Display for Item<'src> {
Item::Alias(alias) => write!(f, "{alias}"),
Item::Assignment(assignment) => write!(f, "{assignment}"),
Item::Comment(comment) => write!(f, "{comment}"),
Item::Import { relative, .. } => write!(f, "import {relative}"),
Item::Mod { name, path, .. } => {
write!(f, "mod {name}")?;
Item::Import {
relative, optional, ..
} => {
write!(f, "import")?;
if let Some(path) = path {
if *optional {
write!(f, "?")?;
}
write!(f, " {relative}")
}
Item::Module {
name,
relative,
optional,
..
} => {
write!(f, "mod")?;
if *optional {
write!(f, "?")?;
}
write!(f, " {name}")?;
if let Some(path) = relative {
write!(f, " {path}")?;
}

View File

@ -116,7 +116,7 @@ impl<'src> Justfile<'src> {
search: &Search,
overrides: &BTreeMap<String, String>,
arguments: &[String],
) -> RunResult<'src, ()> {
) -> RunResult<'src> {
let unknown_overrides = overrides
.keys()
.filter(|name| !self.assignments.contains_key(name.as_str()))
@ -393,7 +393,7 @@ impl<'src> Justfile<'src> {
dotenv: &BTreeMap<String, String>,
search: &Search,
ran: &mut BTreeSet<Vec<String>>,
) -> RunResult<'src, ()> {
) -> RunResult<'src> {
let mut invocation = vec![recipe.name().to_owned()];
for argument in arguments {
invocation.push((*argument).to_string());

View File

@ -75,7 +75,7 @@ impl<'src> Lexer<'src> {
/// Advance over the character in `self.next`, updating `self.token_end`
/// accordingly.
fn advance(&mut self) -> CompileResult<'src, ()> {
fn advance(&mut self) -> CompileResult<'src> {
match self.next {
Some(c) => {
let len_utf8 = c.len_utf8();
@ -97,7 +97,7 @@ impl<'src> Lexer<'src> {
}
/// Advance over N characters.
fn skip(&mut self, n: usize) -> CompileResult<'src, ()> {
fn skip(&mut self, n: usize) -> CompileResult<'src> {
for _ in 0..n {
self.advance()?;
}
@ -124,7 +124,7 @@ impl<'src> Lexer<'src> {
}
}
fn presume(&mut self, c: char) -> CompileResult<'src, ()> {
fn presume(&mut self, c: char) -> CompileResult<'src> {
if !self.next_is(c) {
return Err(self.internal_error(format!("Lexer presumed character `{c}`")));
}
@ -134,7 +134,7 @@ impl<'src> Lexer<'src> {
Ok(())
}
fn presume_str(&mut self, s: &str) -> CompileResult<'src, ()> {
fn presume_str(&mut self, s: &str) -> CompileResult<'src> {
for c in s.chars() {
self.presume(c)?;
}
@ -328,7 +328,7 @@ impl<'src> Lexer<'src> {
}
/// Handle blank lines and indentation
fn lex_line_start(&mut self) -> CompileResult<'src, ()> {
fn lex_line_start(&mut self) -> CompileResult<'src> {
enum Indentation<'src> {
// Line only contains whitespace
Blank,
@ -478,7 +478,7 @@ impl<'src> Lexer<'src> {
}
/// Lex token beginning with `start` outside of a recipe body
fn lex_normal(&mut self, start: char) -> CompileResult<'src, ()> {
fn lex_normal(&mut self, start: char) -> CompileResult<'src> {
match start {
' ' | '\t' => self.lex_whitespace(),
'!' if self.rest().starts_with("!include") => Err(self.error(Include)),
@ -493,10 +493,11 @@ impl<'src> Lexer<'src> {
',' => self.lex_single(Comma),
'/' => self.lex_single(Slash),
':' => self.lex_colon(),
'\\' => self.lex_escape(),
'=' => self.lex_choices('=', &[('=', EqualsEquals), ('~', EqualsTilde)], Equals),
'?' => self.lex_single(QuestionMark),
'@' => self.lex_single(At),
'[' => self.lex_delimiter(BracketL),
'\\' => self.lex_escape(),
'\n' | '\r' => self.lex_eol(),
'\u{feff}' => self.lex_single(ByteOrderMark),
']' => self.lex_delimiter(BracketR),
@ -516,7 +517,7 @@ impl<'src> Lexer<'src> {
&mut self,
interpolation_start: Token<'src>,
start: char,
) -> CompileResult<'src, ()> {
) -> CompileResult<'src> {
if self.rest_starts_with("}}") {
// end current interpolation
if self.interpolation_stack.pop().is_none() {
@ -539,7 +540,7 @@ impl<'src> Lexer<'src> {
}
/// Lex token while in recipe body
fn lex_body(&mut self) -> CompileResult<'src, ()> {
fn lex_body(&mut self) -> CompileResult<'src> {
enum Terminator {
Newline,
NewlineCarriageReturn,
@ -602,14 +603,14 @@ impl<'src> Lexer<'src> {
}
/// Lex a single-character token
fn lex_single(&mut self, kind: TokenKind) -> CompileResult<'src, ()> {
fn lex_single(&mut self, kind: TokenKind) -> CompileResult<'src> {
self.advance()?;
self.token(kind);
Ok(())
}
/// Lex a double-character token
fn lex_double(&mut self, kind: TokenKind) -> CompileResult<'src, ()> {
fn lex_double(&mut self, kind: TokenKind) -> CompileResult<'src> {
self.advance()?;
self.advance()?;
self.token(kind);
@ -624,7 +625,7 @@ impl<'src> Lexer<'src> {
first: char,
choices: &[(char, TokenKind)],
otherwise: TokenKind,
) -> CompileResult<'src, ()> {
) -> CompileResult<'src> {
self.presume(first)?;
for (second, then) in choices {
@ -640,7 +641,7 @@ impl<'src> Lexer<'src> {
}
/// Lex an opening or closing delimiter
fn lex_delimiter(&mut self, kind: TokenKind) -> CompileResult<'src, ()> {
fn lex_delimiter(&mut self, kind: TokenKind) -> CompileResult<'src> {
use Delimiter::*;
match kind {
@ -669,7 +670,7 @@ impl<'src> Lexer<'src> {
}
/// Pop a delimiter from the open delimiter stack and error if incorrect type
fn close_delimiter(&mut self, close: Delimiter) -> CompileResult<'src, ()> {
fn close_delimiter(&mut self, close: Delimiter) -> CompileResult<'src> {
match self.open_delimiters.pop() {
Some((open, _)) if open == close => Ok(()),
Some((open, open_line)) => Err(self.error(MismatchedClosingDelimiter {
@ -687,7 +688,7 @@ impl<'src> Lexer<'src> {
}
/// 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)?;
if self.accepted(right)? {
@ -710,7 +711,7 @@ impl<'src> Lexer<'src> {
}
/// Lex a token starting with ':'
fn lex_colon(&mut self) -> CompileResult<'src, ()> {
fn lex_colon(&mut self) -> CompileResult<'src> {
self.presume(':')?;
if self.accepted('=')? {
@ -724,7 +725,7 @@ impl<'src> Lexer<'src> {
}
/// Lex an token starting with '\' escape
fn lex_escape(&mut self) -> CompileResult<'src, ()> {
fn lex_escape(&mut self) -> CompileResult<'src> {
self.presume('\\')?;
// Treat newline escaped with \ as whitespace
@ -749,7 +750,7 @@ impl<'src> Lexer<'src> {
}
/// Lex a carriage return and line feed
fn lex_eol(&mut self) -> CompileResult<'src, ()> {
fn lex_eol(&mut self) -> CompileResult<'src> {
if self.accepted('\r')? {
if !self.accepted('\n')? {
return Err(self.error(UnpairedCarriageReturn));
@ -770,7 +771,7 @@ impl<'src> Lexer<'src> {
}
/// Lex name: [a-zA-Z_][a-zA-Z0-9_]*
fn lex_identifier(&mut self) -> CompileResult<'src, ()> {
fn lex_identifier(&mut self) -> CompileResult<'src> {
self.advance()?;
while let Some(c) = self.next {
@ -787,7 +788,7 @@ impl<'src> Lexer<'src> {
}
/// Lex comment: #[^\r\n]
fn lex_comment(&mut self) -> CompileResult<'src, ()> {
fn lex_comment(&mut self) -> CompileResult<'src> {
self.presume('#')?;
while !self.at_eol_or_eof() {
@ -800,7 +801,7 @@ impl<'src> Lexer<'src> {
}
/// Lex whitespace: [ \t]+
fn lex_whitespace(&mut self) -> CompileResult<'src, ()> {
fn lex_whitespace(&mut self) -> CompileResult<'src> {
while self.next_is_whitespace() {
self.advance()?;
}
@ -815,7 +816,7 @@ impl<'src> Lexer<'src> {
/// Backtick: ``[^`]*``
/// Cooked string: "[^"]*" # also processes escape sequences
/// Raw string: '[^']*'
fn lex_string(&mut self) -> CompileResult<'src, ()> {
fn lex_string(&mut self) -> CompileResult<'src> {
let kind = if let Some(kind) = StringKind::from_token_start(self.rest()) {
kind
} else {
@ -975,6 +976,7 @@ mod tests {
ParenL => "(",
ParenR => ")",
Plus => "+",
QuestionMark => "?",
Slash => "/",
Whitespace => " ",

View File

@ -83,9 +83,9 @@ pub use crate::run::run;
#[doc(hidden)]
pub use unindent::unindent;
pub(crate) type CompileResult<'a, T> = Result<T, CompileError<'a>>;
pub(crate) type CompileResult<'a, T = ()> = Result<T, CompileError<'a>>;
pub(crate) type ConfigResult<T> = Result<T, ConfigError>;
pub(crate) type RunResult<'a, T> = Result<T, Error<'a>>;
pub(crate) type RunResult<'a, T = ()> = Result<T, Error<'a>>;
pub(crate) type SearchResult<T> = Result<T, SearchError>;
#[cfg(test)]

View File

@ -21,8 +21,37 @@ impl<'src> Node<'src> for Item<'src> {
Item::Alias(alias) => alias.tree(),
Item::Assignment(assignment) => assignment.tree(),
Item::Comment(comment) => comment.tree(),
Item::Import { relative, .. } => Tree::atom("import").push(format!("{relative}")),
Item::Mod { name, .. } => Tree::atom("mod").push(name.lexeme()),
Item::Import {
relative, optional, ..
} => {
let mut tree = Tree::atom("import");
if *optional {
tree = tree.push("?");
}
tree.push(format!("{relative}"))
}
Item::Module {
name,
optional,
relative,
..
} => {
let mut tree = Tree::atom("mod");
if *optional {
tree = tree.push("?");
}
tree = tree.push(name.lexeme());
if let Some(relative) = relative {
tree = tree.push(format!("{relative}"));
}
tree
}
Item::Recipe(recipe) => recipe.tree(),
Item::Set(set) => set.tree(),
}

View File

@ -150,7 +150,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
}
/// Return an unexpected token error if the next token is not an EOL
fn expect_eol(&mut self) -> CompileResult<'src, ()> {
fn expect_eol(&mut self) -> CompileResult<'src> {
self.accept(Comment)?;
if self.next_is(Eof) {
@ -160,7 +160,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
self.expect(Eol).map(|_| ())
}
fn expect_keyword(&mut self, expected: Keyword) -> CompileResult<'src, ()> {
fn expect_keyword(&mut self, expected: Keyword) -> CompileResult<'src> {
let found = self.advance()?;
if found.kind == Identifier && expected == found.lexeme() {
@ -175,7 +175,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
/// Return an internal error if the next token is not of kind `Identifier`
/// with lexeme `lexeme`.
fn presume_keyword(&mut self, keyword: Keyword) -> CompileResult<'src, ()> {
fn presume_keyword(&mut self, keyword: Keyword) -> CompileResult<'src> {
let next = self.advance()?;
if next.kind != Identifier {
@ -231,7 +231,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
}
/// Return an error if the next token is of kind `forbidden`
fn forbid<F>(&self, forbidden: TokenKind, error: F) -> CompileResult<'src, ()>
fn forbid<F>(&self, forbidden: TokenKind, error: F) -> CompileResult<'src>
where
F: FnOnce(Token) -> CompileError,
{
@ -334,31 +334,43 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
self.presume_keyword(Keyword::Export)?;
items.push(Item::Assignment(self.parse_assignment(true)?));
}
Some(Keyword::Import) if self.next_are(&[Identifier, StringToken]) => {
Some(Keyword::Import)
if self.next_are(&[Identifier, StringToken])
|| self.next_are(&[Identifier, QuestionMark]) =>
{
self.presume_keyword(Keyword::Import)?;
let optional = self.accepted(QuestionMark)?;
let (path, relative) = self.parse_string_literal_token()?;
items.push(Item::Import {
relative: self.parse_string_literal()?,
absolute: None,
optional,
path,
relative,
});
}
Some(Keyword::Mod)
if self.next_are(&[Identifier, Identifier, StringToken])
|| self.next_are(&[Identifier, Identifier, Eof])
|| self.next_are(&[Identifier, Identifier, Eol]) =>
|| self.next_are(&[Identifier, Identifier, Eol])
|| self.next_are(&[Identifier, QuestionMark]) =>
{
self.presume_keyword(Keyword::Mod)?;
let optional = self.accepted(QuestionMark)?;
let name = self.parse_name()?;
let path = if self.next_is(StringToken) {
let relative = if self.next_is(StringToken) {
Some(self.parse_string_literal()?)
} else {
None
};
items.push(Item::Mod {
name,
items.push(Item::Module {
absolute: None,
path,
name,
optional,
relative,
});
}
Some(Keyword::Set)
@ -574,8 +586,10 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
}
}
/// Parse a string literal, e.g. `"FOO"`
fn parse_string_literal(&mut self) -> CompileResult<'src, StringLiteral<'src>> {
/// Parse a string literal, e.g. `"FOO"`, returning the string literal and the string token
fn parse_string_literal_token(
&mut self,
) -> CompileResult<'src, (Token<'src>, StringLiteral<'src>)> {
let token = self.expect(StringToken)?;
let kind = StringKind::from_string_or_backtick(token)?;
@ -620,7 +634,13 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
unindented
};
Ok(StringLiteral { kind, raw, cooked })
Ok((token, StringLiteral { kind, raw, cooked }))
}
/// Parse a string literal, e.g. `"FOO"`
fn parse_string_literal(&mut self) -> CompileResult<'src, StringLiteral<'src>> {
let (_token, string_literal) = self.parse_string_literal_token()?;
Ok(string_literal)
}
/// Parse a name from an identifier token
@ -2000,6 +2020,36 @@ mod tests {
tree: (justfile (import "some/file/path.txt")),
}
test! {
name: optional_import,
text: "import? \"some/file/path.txt\" \n",
tree: (justfile (import ? "some/file/path.txt")),
}
test! {
name: module_with,
text: "mod foo",
tree: (justfile (mod foo )),
}
test! {
name: optional_module,
text: "mod? foo",
tree: (justfile (mod ? foo)),
}
test! {
name: module_with_path,
text: "mod foo \"some/file/path.txt\" \n",
tree: (justfile (mod foo "some/file/path.txt")),
}
test! {
name: optional_module_with_path,
text: "mod? foo \"some/file/path.txt\" \n",
tree: (justfile (mod ? foo "some/file/path.txt")),
}
error! {
name: alias_syntax_multiple_rhs,
input: "alias foo := bar baz",

View File

@ -56,7 +56,7 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
&self,
variable: &Token<'src>,
parameters: &[Parameter],
) -> CompileResult<'src, ()> {
) -> CompileResult<'src> {
let name = variable.lexeme();
let undefined =
!self.assignments.contains_key(name) && !parameters.iter().any(|p| p.name.lexeme() == name);

View File

@ -30,6 +30,7 @@ pub(crate) enum TokenKind {
ParenL,
ParenR,
Plus,
QuestionMark,
Slash,
StringToken,
Text,
@ -72,6 +73,7 @@ impl Display for TokenKind {
ParenL => "'('",
ParenR => "')'",
Plus => "'+'",
QuestionMark => "?",
Slash => "'/'",
StringToken => "string",
Text => "command text",

View File

@ -23,6 +23,10 @@ macro_rules! tree {
$crate::tree::Tree::atom("#")
};
{ ? } => {
$crate::tree::Tree::atom("?")
};
{ + } => {
$crate::tree::Tree::atom("+")
};

View File

@ -23,6 +23,49 @@ fn import_succeeds() {
.run();
}
#[test]
fn missing_import_file_error() {
Test::new()
.justfile(
"
import './import.justfile'
a:
@echo A
",
)
.test_round_trip(false)
.arg("a")
.status(EXIT_FAILURE)
.stderr(
"
error: Could not find source file for import.
--> justfile:1:8
|
1 | import './import.justfile'
| ^^^^^^^^^^^^^^^^^^^
",
)
.run();
}
#[test]
fn missing_optional_imports_are_ignored() {
Test::new()
.justfile(
"
import? './import.justfile'
a:
@echo A
",
)
.test_round_trip(false)
.arg("a")
.stdout("A\n")
.run();
}
#[test]
fn trailing_spaces_after_import_are_ignored() {
Test::new()
@ -169,3 +212,33 @@ fn import_paths_beginning_with_tilde_are_expanded_to_homdir() {
.env("HOME", "foobar")
.run();
}
#[test]
fn imports_dump_correctly() {
Test::new()
.write("import.justfile", "")
.justfile(
"
import './import.justfile'
",
)
.test_round_trip(false)
.arg("--dump")
.stdout("import './import.justfile'\n")
.run();
}
#[test]
fn optional_imports_dump_correctly() {
Test::new()
.write("import.justfile", "")
.justfile(
"
import? './import.justfile'
",
)
.test_round_trip(false)
.arg("--dump")
.stdout("import? './import.justfile'\n")
.run();
}

View File

@ -552,13 +552,13 @@ test! {
???
^^^
"#,
stdout: "",
stderr: "error: Unknown start of token:
--> justfile:10:1
|
10 | ???
10 | ^^^
| ^
",
status: EXIT_FAILURE,

View File

@ -266,6 +266,22 @@ fn modules_are_dumped_correctly() {
.run();
}
#[test]
fn optional_modules_are_dumped_correctly() {
Test::new()
.write("foo.just", "foo:\n @echo FOO")
.justfile(
"
mod? foo
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("--dump")
.stdout("mod? foo\n")
.run();
}
#[test]
fn modules_can_be_in_subdirectory() {
Test::new()
@ -382,6 +398,42 @@ fn missing_module_file_error() {
.run();
}
#[test]
fn missing_optional_modules_do_not_trigger_error() {
Test::new()
.justfile(
"
mod? foo
bar:
@echo BAR
",
)
.test_round_trip(false)
.arg("--unstable")
.stdout("BAR\n")
.run();
}
#[test]
fn missing_optional_modules_do_not_conflict() {
Test::new()
.justfile(
"
mod? foo
mod? foo
mod foo 'baz.just'
",
)
.write("baz.just", "baz:\n @echo BAZ")
.test_round_trip(false)
.arg("--unstable")
.arg("foo")
.arg("baz")
.stdout("BAZ\n")
.run();
}
#[test]
fn list_displays_recipes_in_submodules() {
Test::new()
@ -478,6 +530,22 @@ fn modules_with_paths_are_dumped_correctly() {
.run();
}
#[test]
fn optional_modules_with_paths_are_dumped_correctly() {
Test::new()
.write("commands/foo.just", "foo:\n @echo FOO")
.justfile(
"
mod? foo 'commands/foo.just'
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("--dump")
.stdout("mod? foo 'commands/foo.just'\n")
.run();
}
#[test]
fn recipes_may_be_named_mod() {
Test::new()