Optional modules and imports (#1797)
This commit is contained in:
parent
177bb682b8
commit
e2c0d86bdd
14
GRAMMAR.md
14
GRAMMAR.md
@ -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 '}'
|
||||
|
@ -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(),
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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.")?,
|
||||
|
40
src/item.rs
40
src/item.rs
@ -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}")?;
|
||||
}
|
||||
|
||||
|
@ -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());
|
||||
|
46
src/lexer.rs
46
src/lexer.rs
@ -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 => " ",
|
||||
|
||||
|
@ -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)]
|
||||
|
33
src/node.rs
33
src/node.rs
@ -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(),
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
|
@ -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",
|
||||
|
@ -23,6 +23,10 @@ macro_rules! tree {
|
||||
$crate::tree::Tree::atom("#")
|
||||
};
|
||||
|
||||
{ ? } => {
|
||||
$crate::tree::Tree::atom("?")
|
||||
};
|
||||
|
||||
{ + } => {
|
||||
$crate::tree::Tree::atom("+")
|
||||
};
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -552,13 +552,13 @@ test! {
|
||||
|
||||
|
||||
|
||||
???
|
||||
^^^
|
||||
"#,
|
||||
stdout: "",
|
||||
stderr: "error: Unknown start of token:
|
||||
--> justfile:10:1
|
||||
|
|
||||
10 | ???
|
||||
10 | ^^^
|
||||
| ^
|
||||
",
|
||||
status: EXIT_FAILURE,
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user