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
|
justfile : item* EOF
|
||||||
|
|
||||||
item : recipe
|
item : alias
|
||||||
| alias
|
|
||||||
| assignment
|
| assignment
|
||||||
| export
|
|
||||||
| setting
|
|
||||||
| eol
|
| eol
|
||||||
|
| export
|
||||||
|
| import
|
||||||
|
| module
|
||||||
|
| recipe
|
||||||
|
| setting
|
||||||
|
|
||||||
eol : NEWLINE
|
eol : NEWLINE
|
||||||
| COMMENT NEWLINE
|
| COMMENT NEWLINE
|
||||||
@ -72,6 +74,10 @@ setting : 'set' 'allow-duplicate-recipes' boolean?
|
|||||||
| 'set' 'windows-powershell' boolean?
|
| 'set' 'windows-powershell' boolean?
|
||||||
| 'set' 'windows-shell' ':=' '[' string (',' string)* ','? ']'
|
| 'set' 'windows-shell' ':=' '[' string (',' string)* ','? ']'
|
||||||
|
|
||||||
|
import : 'import' '?'? string?
|
||||||
|
|
||||||
|
module : 'mod' '?'? NAME string?
|
||||||
|
|
||||||
boolean : ':=' ('true' | 'false')
|
boolean : ':=' ('true' | 'false')
|
||||||
|
|
||||||
expression : 'if' condition '{' expression '}' 'else' '{' expression '}'
|
expression : 'if' condition '{' expression '}' 'else' '{' expression '}'
|
||||||
|
@ -38,7 +38,7 @@ impl<'src> Analyzer<'src> {
|
|||||||
let mut define = |name: Name<'src>,
|
let mut define = |name: Name<'src>,
|
||||||
second_type: &'static str,
|
second_type: &'static str,
|
||||||
duplicates_allowed: bool|
|
duplicates_allowed: bool|
|
||||||
-> CompileResult<'src, ()> {
|
-> CompileResult<'src> {
|
||||||
if let Some((first_type, original)) = definitions.get(name.lexeme()) {
|
if let Some((first_type, original)) = definitions.get(name.lexeme()) {
|
||||||
if !(*first_type == second_type && duplicates_allowed) {
|
if !(*first_type == second_type && duplicates_allowed) {
|
||||||
let (original, redefinition) = if name.line < original.line {
|
let (original, redefinition) = if name.line < original.line {
|
||||||
@ -75,17 +75,18 @@ impl<'src> Analyzer<'src> {
|
|||||||
}
|
}
|
||||||
Item::Comment(_) => (),
|
Item::Comment(_) => (),
|
||||||
Item::Import { absolute, .. } => {
|
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, .. } => {
|
||||||
define(*name, "module", false)?;
|
if let Some(absolute) = absolute {
|
||||||
modules.insert(
|
define(*name, "module", false)?;
|
||||||
name.to_string(),
|
modules.insert(
|
||||||
(
|
name.to_string(),
|
||||||
*name,
|
(*name, Self::analyze(loaded, paths, asts, absolute)?),
|
||||||
Self::analyze(loaded, paths, asts, absolute.as_ref().unwrap())?,
|
);
|
||||||
),
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Item::Recipe(recipe) => {
|
Item::Recipe(recipe) => {
|
||||||
if recipe.enabled() {
|
if recipe.enabled() {
|
||||||
@ -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 parameters = BTreeSet::new();
|
||||||
let mut passed_default = false;
|
let mut passed_default = false;
|
||||||
|
|
||||||
@ -198,7 +199,7 @@ impl<'src> Analyzer<'src> {
|
|||||||
Ok(())
|
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()) {
|
if self.assignments.contains_key(assignment.name.lexeme()) {
|
||||||
return Err(assignment.name.token().error(DuplicateVariable {
|
return Err(assignment.name.token().error(DuplicateVariable {
|
||||||
variable: assignment.name.lexeme(),
|
variable: assignment.name.lexeme(),
|
||||||
@ -207,7 +208,7 @@ impl<'src> Analyzer<'src> {
|
|||||||
Ok(())
|
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();
|
let name = alias.name.lexeme();
|
||||||
|
|
||||||
for attr in &alias.attributes {
|
for attr in &alias.attributes {
|
||||||
@ -222,7 +223,7 @@ impl<'src> Analyzer<'src> {
|
|||||||
Ok(())
|
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()) {
|
if let Some(original) = self.sets.get(set.name.lexeme()) {
|
||||||
return Err(set.name.error(DuplicateSet {
|
return Err(set.name.error(DuplicateSet {
|
||||||
setting: original.name.lexeme(),
|
setting: original.name.lexeme(),
|
||||||
|
@ -9,7 +9,7 @@ pub(crate) struct AssignmentResolver<'src: 'run, 'run> {
|
|||||||
impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
|
impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
|
||||||
pub(crate) fn resolve_assignments(
|
pub(crate) fn resolve_assignments(
|
||||||
assignments: &'run Table<'src, Assignment<'src>>,
|
assignments: &'run Table<'src, Assignment<'src>>,
|
||||||
) -> CompileResult<'src, ()> {
|
) -> CompileResult<'src> {
|
||||||
let mut resolver = Self {
|
let mut resolver = Self {
|
||||||
stack: Vec::new(),
|
stack: Vec::new(),
|
||||||
evaluated: BTreeSet::new(),
|
evaluated: BTreeSet::new(),
|
||||||
@ -23,7 +23,7 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
|
|||||||
Ok(())
|
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) {
|
if self.evaluated.contains(name) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@ -52,7 +52,7 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_expression(&mut self, expression: &Expression<'src>) -> CompileResult<'src, ()> {
|
fn resolve_expression(&mut self, expression: &Expression<'src>) -> CompileResult<'src> {
|
||||||
match expression {
|
match expression {
|
||||||
Expression::Variable { name } => {
|
Expression::Variable { name } => {
|
||||||
let variable = name.lexeme();
|
let variable = name.lexeme();
|
||||||
|
@ -27,10 +27,11 @@ impl Compiler {
|
|||||||
|
|
||||||
for item in &mut ast.items {
|
for item in &mut ast.items {
|
||||||
match item {
|
match item {
|
||||||
Item::Mod {
|
Item::Module {
|
||||||
name,
|
|
||||||
absolute,
|
absolute,
|
||||||
path,
|
name,
|
||||||
|
optional,
|
||||||
|
relative,
|
||||||
} => {
|
} => {
|
||||||
if !unstable {
|
if !unstable {
|
||||||
return Err(Error::Unstable {
|
return Err(Error::Unstable {
|
||||||
@ -40,29 +41,49 @@ impl Compiler {
|
|||||||
|
|
||||||
let parent = current.parent().unwrap();
|
let parent = current.parent().unwrap();
|
||||||
|
|
||||||
let import = if let Some(path) = path {
|
let import = if let Some(relative) = relative {
|
||||||
parent.join(Self::expand_tilde(&path.cooked)?)
|
let path = parent.join(Self::expand_tilde(&relative.cooked)?);
|
||||||
|
|
||||||
|
if path.is_file() {
|
||||||
|
Some(path)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Self::find_module_file(parent, *name)?
|
Self::find_module_file(parent, *name)?
|
||||||
};
|
};
|
||||||
|
|
||||||
if srcs.contains_key(&import) {
|
if let Some(import) = import {
|
||||||
return Err(Error::CircularImport { current, 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 });
|
||||||
}
|
}
|
||||||
*absolute = Some(import.clone());
|
|
||||||
stack.push((import, depth + 1));
|
|
||||||
}
|
}
|
||||||
Item::Import { relative, absolute } => {
|
Item::Import {
|
||||||
|
relative,
|
||||||
|
absolute,
|
||||||
|
optional,
|
||||||
|
path,
|
||||||
|
} => {
|
||||||
let import = current
|
let import = current
|
||||||
.parent()
|
.parent()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.join(Self::expand_tilde(&relative.cooked)?)
|
.join(Self::expand_tilde(&relative.cooked)?)
|
||||||
.lexiclean();
|
.lexiclean();
|
||||||
if srcs.contains_key(&import) {
|
|
||||||
return Err(Error::CircularImport { current, import });
|
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 });
|
||||||
}
|
}
|
||||||
*absolute = Some(import.clone());
|
|
||||||
stack.push((import, depth + 1));
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@ -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")]
|
let mut candidates = vec![format!("{module}.just"), format!("{module}/mod.just")]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|path| parent.join(path).is_file())
|
.filter(|path| parent.join(path).is_file())
|
||||||
@ -112,8 +133,8 @@ impl Compiler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match candidates.as_slice() {
|
match candidates.as_slice() {
|
||||||
[] => Err(Error::MissingModuleFile { module }),
|
[] => Ok(None),
|
||||||
[file] => Ok(parent.join(file).lexiclean()),
|
[file] => Ok(Some(parent.join(file).lexiclean())),
|
||||||
found => Err(Error::AmbiguousModuleFile {
|
found => Err(Error::AmbiguousModuleFile {
|
||||||
found: found.into(),
|
found: found.into(),
|
||||||
module,
|
module,
|
||||||
|
@ -110,6 +110,9 @@ pub(crate) enum Error<'src> {
|
|||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
io_error: io::Error,
|
io_error: io::Error,
|
||||||
},
|
},
|
||||||
|
MissingImportFile {
|
||||||
|
path: Token<'src>,
|
||||||
|
},
|
||||||
MissingModuleFile {
|
MissingModuleFile {
|
||||||
module: Name<'src>,
|
module: Name<'src>,
|
||||||
},
|
},
|
||||||
@ -181,6 +184,7 @@ impl<'src> Error<'src> {
|
|||||||
Self::Backtick { token, .. } => Some(*token),
|
Self::Backtick { token, .. } => Some(*token),
|
||||||
Self::Compile { compile_error } => Some(compile_error.context()),
|
Self::Compile { compile_error } => Some(compile_error.context()),
|
||||||
Self::FunctionCall { function, .. } => Some(function.token()),
|
Self::FunctionCall { function, .. } => Some(function.token()),
|
||||||
|
Self::MissingImportFile { path } => Some(*path),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -369,6 +373,7 @@ impl<'src> ColorDisplay for Error<'src> {
|
|||||||
let path = path.display();
|
let path = path.display();
|
||||||
write!(f, "Failed to read justfile at `{path}`: {io_error}")?;
|
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}`.")?,
|
MissingModuleFile { module } => write!(f, "Could not find source file for module `{module}`.")?,
|
||||||
NoChoosableRecipes => write!(f, "Justfile contains no choosable recipes.")?,
|
NoChoosableRecipes => write!(f, "Justfile contains no choosable recipes.")?,
|
||||||
NoDefaultRecipe => write!(f, "Justfile contains no default recipe.")?,
|
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>),
|
Assignment(Assignment<'src>),
|
||||||
Comment(&'src str),
|
Comment(&'src str),
|
||||||
Import {
|
Import {
|
||||||
|
absolute: Option<PathBuf>,
|
||||||
|
optional: bool,
|
||||||
|
path: Token<'src>,
|
||||||
relative: StringLiteral<'src>,
|
relative: StringLiteral<'src>,
|
||||||
absolute: Option<PathBuf>,
|
|
||||||
},
|
},
|
||||||
Mod {
|
Module {
|
||||||
name: Name<'src>,
|
|
||||||
absolute: Option<PathBuf>,
|
absolute: Option<PathBuf>,
|
||||||
path: Option<StringLiteral<'src>>,
|
name: Name<'src>,
|
||||||
|
optional: bool,
|
||||||
|
relative: Option<StringLiteral<'src>>,
|
||||||
},
|
},
|
||||||
Recipe(UnresolvedRecipe<'src>),
|
Recipe(UnresolvedRecipe<'src>),
|
||||||
Set(Set<'src>),
|
Set(Set<'src>),
|
||||||
@ -25,11 +28,32 @@ impl<'src> Display for Item<'src> {
|
|||||||
Item::Alias(alias) => write!(f, "{alias}"),
|
Item::Alias(alias) => write!(f, "{alias}"),
|
||||||
Item::Assignment(assignment) => write!(f, "{assignment}"),
|
Item::Assignment(assignment) => write!(f, "{assignment}"),
|
||||||
Item::Comment(comment) => write!(f, "{comment}"),
|
Item::Comment(comment) => write!(f, "{comment}"),
|
||||||
Item::Import { relative, .. } => write!(f, "import {relative}"),
|
Item::Import {
|
||||||
Item::Mod { name, path, .. } => {
|
relative, optional, ..
|
||||||
write!(f, "mod {name}")?;
|
} => {
|
||||||
|
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}")?;
|
write!(f, " {path}")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,7 +116,7 @@ impl<'src> Justfile<'src> {
|
|||||||
search: &Search,
|
search: &Search,
|
||||||
overrides: &BTreeMap<String, String>,
|
overrides: &BTreeMap<String, String>,
|
||||||
arguments: &[String],
|
arguments: &[String],
|
||||||
) -> RunResult<'src, ()> {
|
) -> RunResult<'src> {
|
||||||
let unknown_overrides = overrides
|
let unknown_overrides = overrides
|
||||||
.keys()
|
.keys()
|
||||||
.filter(|name| !self.assignments.contains_key(name.as_str()))
|
.filter(|name| !self.assignments.contains_key(name.as_str()))
|
||||||
@ -393,7 +393,7 @@ impl<'src> Justfile<'src> {
|
|||||||
dotenv: &BTreeMap<String, String>,
|
dotenv: &BTreeMap<String, String>,
|
||||||
search: &Search,
|
search: &Search,
|
||||||
ran: &mut BTreeSet<Vec<String>>,
|
ran: &mut BTreeSet<Vec<String>>,
|
||||||
) -> RunResult<'src, ()> {
|
) -> RunResult<'src> {
|
||||||
let mut invocation = vec![recipe.name().to_owned()];
|
let mut invocation = vec![recipe.name().to_owned()];
|
||||||
for argument in arguments {
|
for argument in arguments {
|
||||||
invocation.push((*argument).to_string());
|
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`
|
/// Advance over the character in `self.next`, updating `self.token_end`
|
||||||
/// accordingly.
|
/// accordingly.
|
||||||
fn advance(&mut self) -> CompileResult<'src, ()> {
|
fn advance(&mut self) -> CompileResult<'src> {
|
||||||
match self.next {
|
match self.next {
|
||||||
Some(c) => {
|
Some(c) => {
|
||||||
let len_utf8 = c.len_utf8();
|
let len_utf8 = c.len_utf8();
|
||||||
@ -97,7 +97,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Advance over N characters.
|
/// Advance over N characters.
|
||||||
fn skip(&mut self, n: usize) -> CompileResult<'src, ()> {
|
fn skip(&mut self, n: usize) -> CompileResult<'src> {
|
||||||
for _ in 0..n {
|
for _ in 0..n {
|
||||||
self.advance()?;
|
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) {
|
if !self.next_is(c) {
|
||||||
return Err(self.internal_error(format!("Lexer presumed character `{c}`")));
|
return Err(self.internal_error(format!("Lexer presumed character `{c}`")));
|
||||||
}
|
}
|
||||||
@ -134,7 +134,7 @@ impl<'src> Lexer<'src> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn presume_str(&mut self, s: &str) -> CompileResult<'src, ()> {
|
fn presume_str(&mut self, s: &str) -> CompileResult<'src> {
|
||||||
for c in s.chars() {
|
for c in s.chars() {
|
||||||
self.presume(c)?;
|
self.presume(c)?;
|
||||||
}
|
}
|
||||||
@ -328,7 +328,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Handle blank lines and indentation
|
/// Handle blank lines and indentation
|
||||||
fn lex_line_start(&mut self) -> CompileResult<'src, ()> {
|
fn lex_line_start(&mut self) -> CompileResult<'src> {
|
||||||
enum Indentation<'src> {
|
enum Indentation<'src> {
|
||||||
// Line only contains whitespace
|
// Line only contains whitespace
|
||||||
Blank,
|
Blank,
|
||||||
@ -478,7 +478,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lex token beginning with `start` outside of a recipe body
|
/// 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 {
|
match start {
|
||||||
' ' | '\t' => self.lex_whitespace(),
|
' ' | '\t' => self.lex_whitespace(),
|
||||||
'!' if self.rest().starts_with("!include") => Err(self.error(Include)),
|
'!' 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(Comma),
|
||||||
'/' => self.lex_single(Slash),
|
'/' => self.lex_single(Slash),
|
||||||
':' => self.lex_colon(),
|
':' => self.lex_colon(),
|
||||||
'\\' => self.lex_escape(),
|
|
||||||
'=' => self.lex_choices('=', &[('=', EqualsEquals), ('~', EqualsTilde)], Equals),
|
'=' => self.lex_choices('=', &[('=', EqualsEquals), ('~', EqualsTilde)], Equals),
|
||||||
|
'?' => self.lex_single(QuestionMark),
|
||||||
'@' => self.lex_single(At),
|
'@' => self.lex_single(At),
|
||||||
'[' => self.lex_delimiter(BracketL),
|
'[' => self.lex_delimiter(BracketL),
|
||||||
|
'\\' => self.lex_escape(),
|
||||||
'\n' | '\r' => self.lex_eol(),
|
'\n' | '\r' => self.lex_eol(),
|
||||||
'\u{feff}' => self.lex_single(ByteOrderMark),
|
'\u{feff}' => self.lex_single(ByteOrderMark),
|
||||||
']' => self.lex_delimiter(BracketR),
|
']' => self.lex_delimiter(BracketR),
|
||||||
@ -516,7 +517,7 @@ impl<'src> Lexer<'src> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
interpolation_start: Token<'src>,
|
interpolation_start: Token<'src>,
|
||||||
start: char,
|
start: char,
|
||||||
) -> CompileResult<'src, ()> {
|
) -> CompileResult<'src> {
|
||||||
if self.rest_starts_with("}}") {
|
if self.rest_starts_with("}}") {
|
||||||
// end current interpolation
|
// end current interpolation
|
||||||
if self.interpolation_stack.pop().is_none() {
|
if self.interpolation_stack.pop().is_none() {
|
||||||
@ -539,7 +540,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lex token while in recipe body
|
/// Lex token while in recipe body
|
||||||
fn lex_body(&mut self) -> CompileResult<'src, ()> {
|
fn lex_body(&mut self) -> CompileResult<'src> {
|
||||||
enum Terminator {
|
enum Terminator {
|
||||||
Newline,
|
Newline,
|
||||||
NewlineCarriageReturn,
|
NewlineCarriageReturn,
|
||||||
@ -602,14 +603,14 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lex a single-character token
|
/// 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.advance()?;
|
||||||
self.token(kind);
|
self.token(kind);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lex a double-character token
|
/// 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.advance()?;
|
self.advance()?;
|
||||||
self.token(kind);
|
self.token(kind);
|
||||||
@ -624,7 +625,7 @@ impl<'src> Lexer<'src> {
|
|||||||
first: char,
|
first: char,
|
||||||
choices: &[(char, TokenKind)],
|
choices: &[(char, TokenKind)],
|
||||||
otherwise: TokenKind,
|
otherwise: TokenKind,
|
||||||
) -> CompileResult<'src, ()> {
|
) -> CompileResult<'src> {
|
||||||
self.presume(first)?;
|
self.presume(first)?;
|
||||||
|
|
||||||
for (second, then) in choices {
|
for (second, then) in choices {
|
||||||
@ -640,7 +641,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lex an opening or closing delimiter
|
/// 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::*;
|
use Delimiter::*;
|
||||||
|
|
||||||
match kind {
|
match kind {
|
||||||
@ -669,7 +670,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Pop a delimiter from the open delimiter stack and error if incorrect type
|
/// 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() {
|
match self.open_delimiters.pop() {
|
||||||
Some((open, _)) if open == close => Ok(()),
|
Some((open, _)) if open == close => Ok(()),
|
||||||
Some((open, open_line)) => Err(self.error(MismatchedClosingDelimiter {
|
Some((open, open_line)) => Err(self.error(MismatchedClosingDelimiter {
|
||||||
@ -687,7 +688,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 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)?;
|
||||||
|
|
||||||
if self.accepted(right)? {
|
if self.accepted(right)? {
|
||||||
@ -710,7 +711,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lex a token starting with ':'
|
/// Lex a token starting with ':'
|
||||||
fn lex_colon(&mut self) -> CompileResult<'src, ()> {
|
fn lex_colon(&mut self) -> CompileResult<'src> {
|
||||||
self.presume(':')?;
|
self.presume(':')?;
|
||||||
|
|
||||||
if self.accepted('=')? {
|
if self.accepted('=')? {
|
||||||
@ -724,7 +725,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lex an token starting with '\' escape
|
/// Lex an token starting with '\' escape
|
||||||
fn lex_escape(&mut self) -> CompileResult<'src, ()> {
|
fn lex_escape(&mut self) -> CompileResult<'src> {
|
||||||
self.presume('\\')?;
|
self.presume('\\')?;
|
||||||
|
|
||||||
// Treat newline escaped with \ as whitespace
|
// Treat newline escaped with \ as whitespace
|
||||||
@ -749,7 +750,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lex a carriage return and line feed
|
/// 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('\r')? {
|
||||||
if !self.accepted('\n')? {
|
if !self.accepted('\n')? {
|
||||||
return Err(self.error(UnpairedCarriageReturn));
|
return Err(self.error(UnpairedCarriageReturn));
|
||||||
@ -770,7 +771,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lex name: [a-zA-Z_][a-zA-Z0-9_]*
|
/// 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()?;
|
self.advance()?;
|
||||||
|
|
||||||
while let Some(c) = self.next {
|
while let Some(c) = self.next {
|
||||||
@ -787,7 +788,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lex comment: #[^\r\n]
|
/// Lex comment: #[^\r\n]
|
||||||
fn lex_comment(&mut self) -> CompileResult<'src, ()> {
|
fn lex_comment(&mut self) -> CompileResult<'src> {
|
||||||
self.presume('#')?;
|
self.presume('#')?;
|
||||||
|
|
||||||
while !self.at_eol_or_eof() {
|
while !self.at_eol_or_eof() {
|
||||||
@ -800,7 +801,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lex whitespace: [ \t]+
|
/// Lex whitespace: [ \t]+
|
||||||
fn lex_whitespace(&mut self) -> CompileResult<'src, ()> {
|
fn lex_whitespace(&mut self) -> CompileResult<'src> {
|
||||||
while self.next_is_whitespace() {
|
while self.next_is_whitespace() {
|
||||||
self.advance()?;
|
self.advance()?;
|
||||||
}
|
}
|
||||||
@ -815,7 +816,7 @@ impl<'src> Lexer<'src> {
|
|||||||
/// Backtick: ``[^`]*``
|
/// Backtick: ``[^`]*``
|
||||||
/// Cooked string: "[^"]*" # also processes escape sequences
|
/// Cooked string: "[^"]*" # also processes escape sequences
|
||||||
/// Raw string: '[^']*'
|
/// 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()) {
|
let kind = if let Some(kind) = StringKind::from_token_start(self.rest()) {
|
||||||
kind
|
kind
|
||||||
} else {
|
} else {
|
||||||
@ -975,6 +976,7 @@ mod tests {
|
|||||||
ParenL => "(",
|
ParenL => "(",
|
||||||
ParenR => ")",
|
ParenR => ")",
|
||||||
Plus => "+",
|
Plus => "+",
|
||||||
|
QuestionMark => "?",
|
||||||
Slash => "/",
|
Slash => "/",
|
||||||
Whitespace => " ",
|
Whitespace => " ",
|
||||||
|
|
||||||
|
@ -83,9 +83,9 @@ pub use crate::run::run;
|
|||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use unindent::unindent;
|
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 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>;
|
pub(crate) type SearchResult<T> = Result<T, SearchError>;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[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::Alias(alias) => alias.tree(),
|
||||||
Item::Assignment(assignment) => assignment.tree(),
|
Item::Assignment(assignment) => assignment.tree(),
|
||||||
Item::Comment(comment) => comment.tree(),
|
Item::Comment(comment) => comment.tree(),
|
||||||
Item::Import { relative, .. } => Tree::atom("import").push(format!("{relative}")),
|
Item::Import {
|
||||||
Item::Mod { name, .. } => Tree::atom("mod").push(name.lexeme()),
|
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::Recipe(recipe) => recipe.tree(),
|
||||||
Item::Set(set) => set.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
|
/// 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)?;
|
self.accept(Comment)?;
|
||||||
|
|
||||||
if self.next_is(Eof) {
|
if self.next_is(Eof) {
|
||||||
@ -160,7 +160,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
self.expect(Eol).map(|_| ())
|
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()?;
|
let found = self.advance()?;
|
||||||
|
|
||||||
if found.kind == Identifier && expected == found.lexeme() {
|
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`
|
/// Return an internal error if the next token is not of kind `Identifier`
|
||||||
/// with lexeme `lexeme`.
|
/// 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()?;
|
let next = self.advance()?;
|
||||||
|
|
||||||
if next.kind != Identifier {
|
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`
|
/// 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
|
where
|
||||||
F: FnOnce(Token) -> CompileError,
|
F: FnOnce(Token) -> CompileError,
|
||||||
{
|
{
|
||||||
@ -334,31 +334,43 @@ 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]) => {
|
Some(Keyword::Import)
|
||||||
|
if self.next_are(&[Identifier, StringToken])
|
||||||
|
|| self.next_are(&[Identifier, QuestionMark]) =>
|
||||||
|
{
|
||||||
self.presume_keyword(Keyword::Import)?;
|
self.presume_keyword(Keyword::Import)?;
|
||||||
|
let optional = self.accepted(QuestionMark)?;
|
||||||
|
let (path, relative) = self.parse_string_literal_token()?;
|
||||||
items.push(Item::Import {
|
items.push(Item::Import {
|
||||||
relative: self.parse_string_literal()?,
|
|
||||||
absolute: None,
|
absolute: None,
|
||||||
|
optional,
|
||||||
|
path,
|
||||||
|
relative,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Some(Keyword::Mod)
|
Some(Keyword::Mod)
|
||||||
if self.next_are(&[Identifier, Identifier, StringToken])
|
if self.next_are(&[Identifier, Identifier, StringToken])
|
||||||
|| self.next_are(&[Identifier, Identifier, Eof])
|
|| 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)?;
|
self.presume_keyword(Keyword::Mod)?;
|
||||||
|
|
||||||
|
let optional = self.accepted(QuestionMark)?;
|
||||||
|
|
||||||
let name = self.parse_name()?;
|
let name = self.parse_name()?;
|
||||||
|
|
||||||
let path = if self.next_is(StringToken) {
|
let relative = if self.next_is(StringToken) {
|
||||||
Some(self.parse_string_literal()?)
|
Some(self.parse_string_literal()?)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
items.push(Item::Mod {
|
items.push(Item::Module {
|
||||||
name,
|
|
||||||
absolute: None,
|
absolute: None,
|
||||||
path,
|
name,
|
||||||
|
optional,
|
||||||
|
relative,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Some(Keyword::Set)
|
Some(Keyword::Set)
|
||||||
@ -574,8 +586,10 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a string literal, e.g. `"FOO"`
|
/// Parse a string literal, e.g. `"FOO"`, returning the string literal and the string token
|
||||||
fn parse_string_literal(&mut self) -> CompileResult<'src, StringLiteral<'src>> {
|
fn parse_string_literal_token(
|
||||||
|
&mut self,
|
||||||
|
) -> CompileResult<'src, (Token<'src>, StringLiteral<'src>)> {
|
||||||
let token = self.expect(StringToken)?;
|
let token = self.expect(StringToken)?;
|
||||||
|
|
||||||
let kind = StringKind::from_string_or_backtick(token)?;
|
let kind = StringKind::from_string_or_backtick(token)?;
|
||||||
@ -620,7 +634,13 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
unindented
|
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
|
/// Parse a name from an identifier token
|
||||||
@ -2000,6 +2020,36 @@ mod tests {
|
|||||||
tree: (justfile (import "some/file/path.txt")),
|
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! {
|
error! {
|
||||||
name: alias_syntax_multiple_rhs,
|
name: alias_syntax_multiple_rhs,
|
||||||
input: "alias foo := bar baz",
|
input: "alias foo := bar baz",
|
||||||
|
@ -56,7 +56,7 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
|
|||||||
&self,
|
&self,
|
||||||
variable: &Token<'src>,
|
variable: &Token<'src>,
|
||||||
parameters: &[Parameter],
|
parameters: &[Parameter],
|
||||||
) -> CompileResult<'src, ()> {
|
) -> CompileResult<'src> {
|
||||||
let name = variable.lexeme();
|
let name = variable.lexeme();
|
||||||
let undefined =
|
let undefined =
|
||||||
!self.assignments.contains_key(name) && !parameters.iter().any(|p| p.name.lexeme() == name);
|
!self.assignments.contains_key(name) && !parameters.iter().any(|p| p.name.lexeme() == name);
|
||||||
|
@ -30,6 +30,7 @@ pub(crate) enum TokenKind {
|
|||||||
ParenL,
|
ParenL,
|
||||||
ParenR,
|
ParenR,
|
||||||
Plus,
|
Plus,
|
||||||
|
QuestionMark,
|
||||||
Slash,
|
Slash,
|
||||||
StringToken,
|
StringToken,
|
||||||
Text,
|
Text,
|
||||||
@ -72,6 +73,7 @@ impl Display for TokenKind {
|
|||||||
ParenL => "'('",
|
ParenL => "'('",
|
||||||
ParenR => "')'",
|
ParenR => "')'",
|
||||||
Plus => "'+'",
|
Plus => "'+'",
|
||||||
|
QuestionMark => "?",
|
||||||
Slash => "'/'",
|
Slash => "'/'",
|
||||||
StringToken => "string",
|
StringToken => "string",
|
||||||
Text => "command text",
|
Text => "command text",
|
||||||
|
@ -23,6 +23,10 @@ macro_rules! tree {
|
|||||||
$crate::tree::Tree::atom("#")
|
$crate::tree::Tree::atom("#")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
{ ? } => {
|
||||||
|
$crate::tree::Tree::atom("?")
|
||||||
|
};
|
||||||
|
|
||||||
{ + } => {
|
{ + } => {
|
||||||
$crate::tree::Tree::atom("+")
|
$crate::tree::Tree::atom("+")
|
||||||
};
|
};
|
||||||
|
@ -23,6 +23,49 @@ fn import_succeeds() {
|
|||||||
.run();
|
.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]
|
#[test]
|
||||||
fn trailing_spaces_after_import_are_ignored() {
|
fn trailing_spaces_after_import_are_ignored() {
|
||||||
Test::new()
|
Test::new()
|
||||||
@ -169,3 +212,33 @@ fn import_paths_beginning_with_tilde_are_expanded_to_homdir() {
|
|||||||
.env("HOME", "foobar")
|
.env("HOME", "foobar")
|
||||||
.run();
|
.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: "",
|
stdout: "",
|
||||||
stderr: "error: Unknown start of token:
|
stderr: "error: Unknown start of token:
|
||||||
--> justfile:10:1
|
--> justfile:10:1
|
||||||
|
|
|
|
||||||
10 | ???
|
10 | ^^^
|
||||||
| ^
|
| ^
|
||||||
",
|
",
|
||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
|
@ -266,6 +266,22 @@ fn modules_are_dumped_correctly() {
|
|||||||
.run();
|
.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]
|
#[test]
|
||||||
fn modules_can_be_in_subdirectory() {
|
fn modules_can_be_in_subdirectory() {
|
||||||
Test::new()
|
Test::new()
|
||||||
@ -382,6 +398,42 @@ fn missing_module_file_error() {
|
|||||||
.run();
|
.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]
|
#[test]
|
||||||
fn list_displays_recipes_in_submodules() {
|
fn list_displays_recipes_in_submodules() {
|
||||||
Test::new()
|
Test::new()
|
||||||
@ -478,6 +530,22 @@ fn modules_with_paths_are_dumped_correctly() {
|
|||||||
.run();
|
.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]
|
#[test]
|
||||||
fn recipes_may_be_named_mod() {
|
fn recipes_may_be_named_mod() {
|
||||||
Test::new()
|
Test::new()
|
||||||
|
Loading…
Reference in New Issue
Block a user