Move !include processing into compiler (#1618)
This commit is contained in:
parent
ba89f1a40a
commit
f745316e88
@ -8,35 +8,54 @@ pub(crate) struct Analyzer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'src> Analyzer<'src> {
|
impl<'src> Analyzer<'src> {
|
||||||
pub(crate) fn analyze(ast: &Ast<'src>) -> CompileResult<'src, Justfile<'src>> {
|
pub(crate) fn analyze(
|
||||||
Analyzer::default().justfile(ast)
|
asts: &HashMap<PathBuf, Ast<'src>>,
|
||||||
|
root: &Path,
|
||||||
|
) -> CompileResult<'src, Justfile<'src>> {
|
||||||
|
Analyzer::default().justfile(asts, root)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn justfile(mut self, ast: &Ast<'src>) -> CompileResult<'src, Justfile<'src>> {
|
fn justfile(
|
||||||
|
mut self,
|
||||||
|
asts: &HashMap<PathBuf, Ast<'src>>,
|
||||||
|
root: &Path,
|
||||||
|
) -> CompileResult<'src, Justfile<'src>> {
|
||||||
let mut recipes = Vec::new();
|
let mut recipes = Vec::new();
|
||||||
|
|
||||||
for item in &ast.items {
|
let mut stack = Vec::new();
|
||||||
match item {
|
stack.push(asts.get(root).unwrap());
|
||||||
Item::Alias(alias) => {
|
|
||||||
self.analyze_alias(alias)?;
|
let mut warnings = Vec::new();
|
||||||
self.aliases.insert(alias.clone());
|
|
||||||
}
|
while let Some(ast) = stack.pop() {
|
||||||
Item::Assignment(assignment) => {
|
for item in &ast.items {
|
||||||
self.analyze_assignment(assignment)?;
|
match item {
|
||||||
self.assignments.insert(assignment.clone());
|
Item::Alias(alias) => {
|
||||||
}
|
self.analyze_alias(alias)?;
|
||||||
Item::Comment(_) => (),
|
self.aliases.insert(alias.clone());
|
||||||
Item::Recipe(recipe) => {
|
}
|
||||||
if recipe.enabled() {
|
Item::Assignment(assignment) => {
|
||||||
Self::analyze_recipe(recipe)?;
|
self.analyze_assignment(assignment)?;
|
||||||
recipes.push(recipe);
|
self.assignments.insert(assignment.clone());
|
||||||
|
}
|
||||||
|
Item::Comment(_) => (),
|
||||||
|
Item::Recipe(recipe) => {
|
||||||
|
if recipe.enabled() {
|
||||||
|
Self::analyze_recipe(recipe)?;
|
||||||
|
recipes.push(recipe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item::Set(set) => {
|
||||||
|
self.analyze_set(set)?;
|
||||||
|
self.sets.insert(set.clone());
|
||||||
|
}
|
||||||
|
Item::Include { absolute, .. } => {
|
||||||
|
stack.push(asts.get(absolute.as_ref().unwrap()).unwrap());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Item::Set(set) => {
|
|
||||||
self.analyze_set(set)?;
|
|
||||||
self.sets.insert(set.clone());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
warnings.extend(ast.warnings.iter().cloned());
|
||||||
}
|
}
|
||||||
|
|
||||||
let settings = Settings::from_setting_iter(self.sets.into_iter().map(|(_, set)| set.value));
|
let settings = Settings::from_setting_iter(self.sets.into_iter().map(|(_, set)| set.value));
|
||||||
@ -65,7 +84,6 @@ impl<'src> Analyzer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ok(Justfile {
|
Ok(Justfile {
|
||||||
warnings: ast.warnings.clone(),
|
|
||||||
first: recipes
|
first: recipes
|
||||||
.values()
|
.values()
|
||||||
.fold(None, |accumulator, next| match accumulator {
|
.fold(None, |accumulator, next| match accumulator {
|
||||||
@ -80,6 +98,7 @@ impl<'src> Analyzer<'src> {
|
|||||||
assignments: self.assignments,
|
assignments: self.assignments,
|
||||||
recipes,
|
recipes,
|
||||||
settings,
|
settings,
|
||||||
|
warnings,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
19
src/compilation.rs
Normal file
19
src/compilation.rs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct Compilation<'src> {
|
||||||
|
pub(crate) asts: HashMap<PathBuf, Ast<'src>>,
|
||||||
|
pub(crate) justfile: Justfile<'src>,
|
||||||
|
pub(crate) root: PathBuf,
|
||||||
|
pub(crate) srcs: HashMap<PathBuf, &'src str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'src> Compilation<'src> {
|
||||||
|
pub(crate) fn root_ast(&self) -> &Ast<'src> {
|
||||||
|
self.asts.get(&self.root).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn root_src(&self) -> &'src str {
|
||||||
|
self.srcs.get(&self.root).unwrap()
|
||||||
|
}
|
||||||
|
}
|
@ -135,6 +135,7 @@ impl Display for CompileError<'_> {
|
|||||||
Count("argument", *found),
|
Count("argument", *found),
|
||||||
expected.display(),
|
expected.display(),
|
||||||
),
|
),
|
||||||
|
IncludeMissingPath => write!(f, "!include directive has no argument",),
|
||||||
InconsistentLeadingWhitespace { expected, found } => write!(
|
InconsistentLeadingWhitespace { expected, found } => write!(
|
||||||
f,
|
f,
|
||||||
"Recipe line has inconsistent leading whitespace. Recipe started with `{}` but found \
|
"Recipe line has inconsistent leading whitespace. Recipe started with `{}` but found \
|
||||||
@ -202,6 +203,7 @@ impl Display for CompileError<'_> {
|
|||||||
UnknownDependency { recipe, unknown } => {
|
UnknownDependency { recipe, unknown } => {
|
||||||
write!(f, "Recipe `{recipe}` has unknown dependency `{unknown}`")
|
write!(f, "Recipe `{recipe}` has unknown dependency `{unknown}`")
|
||||||
}
|
}
|
||||||
|
UnknownDirective { directive } => write!(f, "Unknown directive `!{directive}`"),
|
||||||
UnknownFunction { function } => write!(f, "Call to unknown function `{function}`"),
|
UnknownFunction { function } => write!(f, "Call to unknown function `{function}`"),
|
||||||
UnknownSetting { setting } => write!(f, "Unknown setting `{setting}`"),
|
UnknownSetting { setting } => write!(f, "Unknown setting `{setting}`"),
|
||||||
UnknownStartOfToken => write!(f, "Unknown start of token:"),
|
UnknownStartOfToken => write!(f, "Unknown start of token:"),
|
||||||
|
@ -58,6 +58,7 @@ pub(crate) enum CompileErrorKind<'src> {
|
|||||||
found: usize,
|
found: usize,
|
||||||
expected: Range<usize>,
|
expected: Range<usize>,
|
||||||
},
|
},
|
||||||
|
IncludeMissingPath,
|
||||||
InconsistentLeadingWhitespace {
|
InconsistentLeadingWhitespace {
|
||||||
expected: &'src str,
|
expected: &'src str,
|
||||||
found: &'src str,
|
found: &'src str,
|
||||||
@ -110,6 +111,9 @@ pub(crate) enum CompileErrorKind<'src> {
|
|||||||
recipe: &'src str,
|
recipe: &'src str,
|
||||||
unknown: &'src str,
|
unknown: &'src str,
|
||||||
},
|
},
|
||||||
|
UnknownDirective {
|
||||||
|
directive: &'src str,
|
||||||
|
},
|
||||||
UnknownFunction {
|
UnknownFunction {
|
||||||
function: &'src str,
|
function: &'src str,
|
||||||
},
|
},
|
||||||
|
138
src/compiler.rs
138
src/compiler.rs
@ -3,11 +3,141 @@ use super::*;
|
|||||||
pub(crate) struct Compiler;
|
pub(crate) struct Compiler;
|
||||||
|
|
||||||
impl Compiler {
|
impl Compiler {
|
||||||
pub(crate) fn compile(src: &str) -> CompileResult<(Ast, Justfile)> {
|
pub(crate) fn compile<'src>(
|
||||||
|
unstable: bool,
|
||||||
|
loader: &'src Loader,
|
||||||
|
root: &Path,
|
||||||
|
) -> RunResult<'src, Compilation<'src>> {
|
||||||
|
let mut srcs: HashMap<PathBuf, &str> = HashMap::new();
|
||||||
|
let mut asts: HashMap<PathBuf, Ast> = HashMap::new();
|
||||||
|
|
||||||
|
let mut paths: Vec<PathBuf> = Vec::new();
|
||||||
|
paths.push(root.into());
|
||||||
|
|
||||||
|
while let Some(current) = paths.pop() {
|
||||||
|
let src = loader.load(¤t)?;
|
||||||
|
let tokens = Lexer::lex(src)?;
|
||||||
|
let mut ast = Parser::parse(&tokens)?;
|
||||||
|
|
||||||
|
srcs.insert(current.clone(), src);
|
||||||
|
|
||||||
|
for item in &mut ast.items {
|
||||||
|
if let Item::Include { relative, absolute } = item {
|
||||||
|
if !unstable {
|
||||||
|
return Err(Error::Unstable {
|
||||||
|
message: "The !include directive is currently unstable.".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let include = current.parent().unwrap().join(relative).lexiclean();
|
||||||
|
|
||||||
|
if srcs.contains_key(&include) {
|
||||||
|
return Err(Error::CircularInclude { current, include });
|
||||||
|
}
|
||||||
|
|
||||||
|
*absolute = Some(include.clone());
|
||||||
|
|
||||||
|
paths.push(include);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
asts.insert(current.clone(), ast.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let justfile = Analyzer::analyze(&asts, root)?;
|
||||||
|
|
||||||
|
Ok(Compilation {
|
||||||
|
asts,
|
||||||
|
srcs,
|
||||||
|
justfile,
|
||||||
|
root: root.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn test_compile(src: &str) -> CompileResult<Justfile> {
|
||||||
let tokens = Lexer::lex(src)?;
|
let tokens = Lexer::lex(src)?;
|
||||||
let ast = Parser::parse(&tokens)?;
|
let ast = Parser::parse(&tokens)?;
|
||||||
let justfile = Analyzer::analyze(&ast)?;
|
let root = PathBuf::from("<ROOT>");
|
||||||
|
let mut asts: HashMap<PathBuf, Ast> = HashMap::new();
|
||||||
Ok((ast, justfile))
|
asts.insert(root.clone(), ast);
|
||||||
|
Analyzer::analyze(&asts, &root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use {super::*, temptree::temptree};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn include_justfile() {
|
||||||
|
let justfile_a = r#"
|
||||||
|
# A comment at the top of the file
|
||||||
|
!include ./justfile_b
|
||||||
|
|
||||||
|
#some_recipe: recipe_b
|
||||||
|
some_recipe:
|
||||||
|
echo "some recipe"
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let justfile_b = r#"!include ./subdir/justfile_c
|
||||||
|
|
||||||
|
recipe_b: recipe_c
|
||||||
|
echo "recipe b"
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let justfile_c = r#"recipe_c:
|
||||||
|
echo "recipe c"
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let tmp = temptree! {
|
||||||
|
justfile: justfile_a,
|
||||||
|
justfile_b: justfile_b,
|
||||||
|
subdir: {
|
||||||
|
justfile_c: justfile_c
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let loader = Loader::new();
|
||||||
|
|
||||||
|
let justfile_a_path = tmp.path().join("justfile");
|
||||||
|
let compilation = Compiler::compile(true, &loader, &justfile_a_path).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(compilation.root_src(), justfile_a);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recursive_includes_fail() {
|
||||||
|
let justfile_a = r#"
|
||||||
|
# A comment at the top of the file
|
||||||
|
!include ./subdir/justfile_b
|
||||||
|
|
||||||
|
some_recipe: recipe_b
|
||||||
|
echo "some recipe"
|
||||||
|
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let justfile_b = r#"
|
||||||
|
!include ../justfile
|
||||||
|
|
||||||
|
recipe_b:
|
||||||
|
echo "recipe b"
|
||||||
|
"#;
|
||||||
|
let tmp = temptree! {
|
||||||
|
justfile: justfile_a,
|
||||||
|
subdir: {
|
||||||
|
justfile_b: justfile_b
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let loader = Loader::new();
|
||||||
|
|
||||||
|
let justfile_a_path = tmp.path().join("justfile");
|
||||||
|
let loader_output = Compiler::compile(true, &loader, &justfile_a_path).unwrap_err();
|
||||||
|
|
||||||
|
assert_matches!(loader_output, Error::CircularInclude { current, include }
|
||||||
|
if current == tmp.path().join("subdir").join("justfile_b").lexiclean() &&
|
||||||
|
include == tmp.path().join("justfile").lexiclean()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
15
src/error.rs
15
src/error.rs
@ -91,19 +91,12 @@ pub(crate) enum Error<'src> {
|
|||||||
GetConfirmation {
|
GetConfirmation {
|
||||||
io_error: io::Error,
|
io_error: io::Error,
|
||||||
},
|
},
|
||||||
IncludeMissingPath {
|
|
||||||
file: PathBuf,
|
|
||||||
line: usize,
|
|
||||||
},
|
|
||||||
InitExists {
|
InitExists {
|
||||||
justfile: PathBuf,
|
justfile: PathBuf,
|
||||||
},
|
},
|
||||||
Internal {
|
Internal {
|
||||||
message: String,
|
message: String,
|
||||||
},
|
},
|
||||||
InvalidDirective {
|
|
||||||
line: String,
|
|
||||||
},
|
|
||||||
Io {
|
Io {
|
||||||
recipe: &'src str,
|
recipe: &'src str,
|
||||||
io_error: io::Error,
|
io_error: io::Error,
|
||||||
@ -338,11 +331,6 @@ impl<'src> ColorDisplay for Error<'src> {
|
|||||||
GetConfirmation { io_error } => {
|
GetConfirmation { io_error } => {
|
||||||
write!(f, "Failed to read confirmation from stdin: {io_error}")?;
|
write!(f, "Failed to read confirmation from stdin: {io_error}")?;
|
||||||
}
|
}
|
||||||
IncludeMissingPath { file: justfile, line } => {
|
|
||||||
let line = line.ordinal();
|
|
||||||
let justfile = justfile.display();
|
|
||||||
write!(f, "!include directive on line {line} of `{justfile}` has no argument")?;
|
|
||||||
}
|
|
||||||
InitExists { justfile } => {
|
InitExists { justfile } => {
|
||||||
write!(f, "Justfile `{}` already exists", justfile.display())?;
|
write!(f, "Justfile `{}` already exists", justfile.display())?;
|
||||||
}
|
}
|
||||||
@ -350,9 +338,6 @@ impl<'src> ColorDisplay for Error<'src> {
|
|||||||
write!(f, "Internal runtime error, this may indicate a bug in just: {message} \
|
write!(f, "Internal runtime error, this may indicate a bug in just: {message} \
|
||||||
consider filing an issue: https://github.com/casey/just/issues/new")?;
|
consider filing an issue: https://github.com/casey/just/issues/new")?;
|
||||||
}
|
}
|
||||||
InvalidDirective { line } => {
|
|
||||||
write!(f, "Invalid directive: {line}")?;
|
|
||||||
}
|
|
||||||
Io { recipe, io_error } => {
|
Io { recipe, io_error } => {
|
||||||
match io_error.kind() {
|
match io_error.kind() {
|
||||||
io::ErrorKind::NotFound => write!(f, "Recipe `{recipe}` could not be run because just could not find the shell: {io_error}"),
|
io::ErrorKind::NotFound => write!(f, "Recipe `{recipe}` could not be run because just could not find the shell: {io_error}"),
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub fn compile(text: &str) {
|
pub fn compile(text: &str) {
|
||||||
let _ = compiler::Compiler::compile(text);
|
let _ = testing::compile(text);
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,10 @@ pub(crate) enum Item<'src> {
|
|||||||
Comment(&'src str),
|
Comment(&'src str),
|
||||||
Recipe(UnresolvedRecipe<'src>),
|
Recipe(UnresolvedRecipe<'src>),
|
||||||
Set(Set<'src>),
|
Set(Set<'src>),
|
||||||
|
Include {
|
||||||
|
relative: &'src str,
|
||||||
|
absolute: Option<PathBuf>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'src> Display for Item<'src> {
|
impl<'src> Display for Item<'src> {
|
||||||
@ -18,6 +22,7 @@ impl<'src> Display for Item<'src> {
|
|||||||
Item::Comment(comment) => write!(f, "{comment}"),
|
Item::Comment(comment) => write!(f, "{comment}"),
|
||||||
Item::Recipe(recipe) => write!(f, "{}", recipe.color_display(Color::never())),
|
Item::Recipe(recipe) => write!(f, "{}", recipe.color_display(Color::never())),
|
||||||
Item::Set(set) => write!(f, "{set}"),
|
Item::Set(set) => write!(f, "{set}"),
|
||||||
|
Item::Include { relative, .. } => write!(f, "!include {relative}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
82
src/lexer.rs
82
src/lexer.rs
@ -470,7 +470,7 @@ impl<'src> Lexer<'src> {
|
|||||||
fn lex_normal(&mut self, start: char) -> CompileResult<'src, ()> {
|
fn lex_normal(&mut self, start: char) -> CompileResult<'src, ()> {
|
||||||
match start {
|
match start {
|
||||||
' ' | '\t' => self.lex_whitespace(),
|
' ' | '\t' => self.lex_whitespace(),
|
||||||
'!' => self.lex_digraph('!', '=', BangEquals),
|
'!' => self.lex_bang(),
|
||||||
'#' => self.lex_comment(),
|
'#' => self.lex_comment(),
|
||||||
'$' => self.lex_single(Dollar),
|
'$' => self.lex_single(Dollar),
|
||||||
'&' => self.lex_digraph('&', '&', AmpersandAmpersand),
|
'&' => self.lex_digraph('&', '&', AmpersandAmpersand),
|
||||||
@ -674,6 +674,33 @@ impl<'src> Lexer<'src> {
|
|||||||
!self.open_delimiters.is_empty()
|
!self.open_delimiters.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn lex_bang(&mut self) -> CompileResult<'src, ()> {
|
||||||
|
self.presume('!')?;
|
||||||
|
|
||||||
|
// Try to lex a `!=`
|
||||||
|
if self.accepted('=')? {
|
||||||
|
self.token(BangEquals);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, lex a `!`
|
||||||
|
self.token(Bang);
|
||||||
|
|
||||||
|
if self.next.map(Self::is_identifier_start).unwrap_or_default() {
|
||||||
|
self.lex_identifier()?;
|
||||||
|
|
||||||
|
while !self.at_eol_or_eof() {
|
||||||
|
self.advance()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.current_token_length() > 0 {
|
||||||
|
self.token(Text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Lex a two-character digraph
|
/// Lex a two-character digraph
|
||||||
fn lex_digraph(&mut self, left: char, right: char, token: TokenKind) -> CompileResult<'src, ()> {
|
fn lex_digraph(&mut self, left: char, right: char, token: TokenKind) -> CompileResult<'src, ()> {
|
||||||
self.presume(left)?;
|
self.presume(left)?;
|
||||||
@ -942,6 +969,7 @@ mod tests {
|
|||||||
AmpersandAmpersand => "&&",
|
AmpersandAmpersand => "&&",
|
||||||
Asterisk => "*",
|
Asterisk => "*",
|
||||||
At => "@",
|
At => "@",
|
||||||
|
Bang => "!",
|
||||||
BangEquals => "!=",
|
BangEquals => "!=",
|
||||||
BraceL => "{",
|
BraceL => "{",
|
||||||
BraceR => "}",
|
BraceR => "}",
|
||||||
@ -2051,6 +2079,30 @@ mod tests {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: bang_eof,
|
||||||
|
text: "!",
|
||||||
|
tokens: (Bang),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: character_after_bang,
|
||||||
|
text: "!{",
|
||||||
|
tokens: (Bang, BraceL)
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: identifier_after_bang,
|
||||||
|
text: "!include",
|
||||||
|
tokens: (Bang, Identifier:"include")
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: identifier_after_bang_with_more_stuff,
|
||||||
|
text: "!include some/stuff",
|
||||||
|
tokens: (Bang, Identifier:"include", Text:" some/stuff")
|
||||||
|
}
|
||||||
|
|
||||||
error! {
|
error! {
|
||||||
name: tokenize_space_then_tab,
|
name: tokenize_space_then_tab,
|
||||||
input: "a:
|
input: "a:
|
||||||
@ -2222,12 +2274,12 @@ mod tests {
|
|||||||
|
|
||||||
error! {
|
error! {
|
||||||
name: unexpected_character_after_bang,
|
name: unexpected_character_after_bang,
|
||||||
input: "!{",
|
input: "!%",
|
||||||
offset: 1,
|
offset: 1,
|
||||||
line: 0,
|
line: 0,
|
||||||
column: 1,
|
column: 1,
|
||||||
width: 1,
|
width: 1,
|
||||||
kind: UnexpectedCharacter { expected: '=' },
|
kind: UnknownStartOfToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
error! {
|
error! {
|
||||||
@ -2244,30 +2296,6 @@ mod tests {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
error! {
|
|
||||||
name: bang_eof,
|
|
||||||
input: "!",
|
|
||||||
offset: 1,
|
|
||||||
line: 0,
|
|
||||||
column: 1,
|
|
||||||
width: 0,
|
|
||||||
kind: UnexpectedEndOfToken {
|
|
||||||
expected: '=',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
error! {
|
|
||||||
name: bang_unexpected,
|
|
||||||
input: "!%",
|
|
||||||
offset: 1,
|
|
||||||
line: 0,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
kind: UnexpectedCharacter {
|
|
||||||
expected: '=',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
error! {
|
error! {
|
||||||
name: ampersand_eof,
|
name: ampersand_eof,
|
||||||
input: "&",
|
input: "&",
|
||||||
|
@ -12,7 +12,7 @@ pub(crate) use {
|
|||||||
crate::{
|
crate::{
|
||||||
alias::Alias, analyzer::Analyzer, assignment::Assignment,
|
alias::Alias, analyzer::Analyzer, assignment::Assignment,
|
||||||
assignment_resolver::AssignmentResolver, ast::Ast, attribute::Attribute, binding::Binding,
|
assignment_resolver::AssignmentResolver, ast::Ast, attribute::Attribute, binding::Binding,
|
||||||
color::Color, color_display::ColorDisplay, command_ext::CommandExt,
|
color::Color, color_display::ColorDisplay, command_ext::CommandExt, compilation::Compilation,
|
||||||
compile_error::CompileError, compile_error_kind::CompileErrorKind, compiler::Compiler,
|
compile_error::CompileError, compile_error_kind::CompileErrorKind, compiler::Compiler,
|
||||||
conditional_operator::ConditionalOperator, config::Config, config_error::ConfigError,
|
conditional_operator::ConditionalOperator, config::Config, config_error::ConfigError,
|
||||||
count::Count, delimiter::Delimiter, dependency::Dependency, dump_format::DumpFormat,
|
count::Count, delimiter::Delimiter, dependency::Dependency, dump_format::DumpFormat,
|
||||||
@ -34,7 +34,7 @@ pub(crate) use {
|
|||||||
},
|
},
|
||||||
std::{
|
std::{
|
||||||
cmp,
|
cmp,
|
||||||
collections::{BTreeMap, BTreeSet},
|
collections::{BTreeMap, BTreeSet, HashMap},
|
||||||
env,
|
env,
|
||||||
ffi::{OsStr, OsString},
|
ffi::{OsStr, OsString},
|
||||||
fmt::{self, Debug, Display, Formatter},
|
fmt::{self, Debug, Display, Formatter},
|
||||||
@ -113,6 +113,7 @@ mod binding;
|
|||||||
mod color;
|
mod color;
|
||||||
mod color_display;
|
mod color_display;
|
||||||
mod command_ext;
|
mod command_ext;
|
||||||
|
mod compilation;
|
||||||
mod compile_error;
|
mod compile_error;
|
||||||
mod compile_error_kind;
|
mod compile_error_kind;
|
||||||
mod compiler;
|
mod compiler;
|
||||||
|
202
src/loader.rs
202
src/loader.rs
@ -1,216 +1,22 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
struct LinesWithEndings<'a> {
|
|
||||||
input: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> LinesWithEndings<'a> {
|
|
||||||
fn new(input: &'a str) -> Self {
|
|
||||||
Self { input }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Iterator for LinesWithEndings<'a> {
|
|
||||||
type Item = &'a str;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<&'a str> {
|
|
||||||
if self.input.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let split = self.input.find('\n').map_or(self.input.len(), |i| i + 1);
|
|
||||||
let (line, rest) = self.input.split_at(split);
|
|
||||||
self.input = rest;
|
|
||||||
Some(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct Loader {
|
pub(crate) struct Loader {
|
||||||
arena: Arena<String>,
|
arena: Arena<String>,
|
||||||
unstable: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Loader {
|
impl Loader {
|
||||||
pub(crate) fn new(unstable: bool) -> Self {
|
pub(crate) fn new() -> Self {
|
||||||
Loader {
|
Loader {
|
||||||
arena: Arena::new(),
|
arena: Arena::new(),
|
||||||
unstable,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn load<'src>(&'src self, path: &Path) -> RunResult<&'src str> {
|
pub(crate) fn load<'src>(&'src self, path: &Path) -> RunResult<&'src str> {
|
||||||
let src = self.load_recursive(path, HashSet::new())?;
|
let src = fs::read_to_string(path).map_err(|io_error| Error::Load {
|
||||||
Ok(self.arena.alloc(src))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_file<'a>(path: &Path) -> RunResult<'a, String> {
|
|
||||||
fs::read_to_string(path).map_err(|io_error| Error::Load {
|
|
||||||
path: path.to_owned(),
|
path: path.to_owned(),
|
||||||
io_error,
|
io_error,
|
||||||
})
|
})?;
|
||||||
}
|
|
||||||
|
|
||||||
fn load_recursive(&self, file: &Path, seen: HashSet<PathBuf>) -> RunResult<String> {
|
Ok(self.arena.alloc(src))
|
||||||
let src = Self::load_file(file)?;
|
|
||||||
|
|
||||||
let mut output = String::new();
|
|
||||||
|
|
||||||
let mut seen_content = false;
|
|
||||||
|
|
||||||
for (i, line) in LinesWithEndings::new(&src).enumerate() {
|
|
||||||
if !seen_content && line.starts_with('!') {
|
|
||||||
let include = line
|
|
||||||
.strip_prefix("!include")
|
|
||||||
.ok_or_else(|| Error::InvalidDirective { line: line.into() })?;
|
|
||||||
|
|
||||||
if !self.unstable {
|
|
||||||
return Err(Error::Unstable {
|
|
||||||
message: "The !include directive is currently unstable.".into(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let argument = include.trim();
|
|
||||||
|
|
||||||
if argument.is_empty() {
|
|
||||||
return Err(Error::IncludeMissingPath {
|
|
||||||
file: file.to_owned(),
|
|
||||||
line: i,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let contents = self.process_include(file, Path::new(argument), &seen)?;
|
|
||||||
|
|
||||||
output.push_str(&contents);
|
|
||||||
} else {
|
|
||||||
if !(line.trim().is_empty() || line.trim().starts_with('#')) {
|
|
||||||
seen_content = true;
|
|
||||||
}
|
|
||||||
output.push_str(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_include(
|
|
||||||
&self,
|
|
||||||
file: &Path,
|
|
||||||
include: &Path,
|
|
||||||
seen: &HashSet<PathBuf>,
|
|
||||||
) -> RunResult<String> {
|
|
||||||
let canonical_path = if include.is_relative() {
|
|
||||||
let current_dir = file.parent().ok_or(Error::Internal {
|
|
||||||
message: format!(
|
|
||||||
"Justfile path `{}` has no parent directory",
|
|
||||||
include.display()
|
|
||||||
),
|
|
||||||
})?;
|
|
||||||
current_dir.join(include)
|
|
||||||
} else {
|
|
||||||
include.to_owned()
|
|
||||||
};
|
|
||||||
|
|
||||||
let canonical_path = canonical_path.lexiclean();
|
|
||||||
|
|
||||||
if seen.contains(&canonical_path) {
|
|
||||||
return Err(Error::CircularInclude {
|
|
||||||
current: file.to_owned(),
|
|
||||||
include: canonical_path,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut seen_paths = seen.clone();
|
|
||||||
seen_paths.insert(file.lexiclean());
|
|
||||||
|
|
||||||
self.load_recursive(&canonical_path, seen_paths)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::{Error, Lexiclean, Loader};
|
|
||||||
use temptree::temptree;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn include_justfile() {
|
|
||||||
let justfile_a = r#"
|
|
||||||
# A comment at the top of the file
|
|
||||||
!include ./justfile_b
|
|
||||||
|
|
||||||
some_recipe: recipe_b
|
|
||||||
echo "some recipe"
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let justfile_b = r#"!include ./subdir/justfile_c
|
|
||||||
|
|
||||||
recipe_b: recipe_c
|
|
||||||
echo "recipe b"
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let justfile_c = r#"recipe_c:
|
|
||||||
echo "recipe c"
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let tmp = temptree! {
|
|
||||||
justfile: justfile_a,
|
|
||||||
justfile_b: justfile_b,
|
|
||||||
subdir: {
|
|
||||||
justfile_c: justfile_c
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let full_concatenated_output = r#"
|
|
||||||
# A comment at the top of the file
|
|
||||||
recipe_c:
|
|
||||||
echo "recipe c"
|
|
||||||
|
|
||||||
recipe_b: recipe_c
|
|
||||||
echo "recipe b"
|
|
||||||
|
|
||||||
some_recipe: recipe_b
|
|
||||||
echo "some recipe"
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let loader = Loader::new(true);
|
|
||||||
|
|
||||||
let justfile_a_path = tmp.path().join("justfile");
|
|
||||||
let loader_output = loader.load(&justfile_a_path).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(loader_output, full_concatenated_output);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn recursive_includes_fail() {
|
|
||||||
let justfile_a = r#"
|
|
||||||
# A comment at the top of the file
|
|
||||||
!include ./subdir/justfile_b
|
|
||||||
|
|
||||||
some_recipe: recipe_b
|
|
||||||
echo "some recipe"
|
|
||||||
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let justfile_b = r#"
|
|
||||||
!include ../justfile
|
|
||||||
|
|
||||||
recipe_b:
|
|
||||||
echo "recipe b"
|
|
||||||
"#;
|
|
||||||
let tmp = temptree! {
|
|
||||||
justfile: justfile_a,
|
|
||||||
subdir: {
|
|
||||||
justfile_b: justfile_b
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let loader = Loader::new(true);
|
|
||||||
|
|
||||||
let justfile_a_path = tmp.path().join("justfile");
|
|
||||||
let loader_output = loader.load(&justfile_a_path).unwrap_err();
|
|
||||||
|
|
||||||
assert_matches!(loader_output, Error::CircularInclude { current, include }
|
|
||||||
if current == tmp.path().join("subdir").join("justfile_b").lexiclean() &&
|
|
||||||
include == tmp.path().join("justfile").lexiclean()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ impl<'src> Node<'src> for Item<'src> {
|
|||||||
Item::Comment(comment) => comment.tree(),
|
Item::Comment(comment) => comment.tree(),
|
||||||
Item::Recipe(recipe) => recipe.tree(),
|
Item::Recipe(recipe) => recipe.tree(),
|
||||||
Item::Set(set) => set.tree(),
|
Item::Set(set) => set.tree(),
|
||||||
|
Item::Include { relative, .. } => Tree::atom("include").push(format!("\"{relative}\"")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -350,6 +350,9 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if self.next_is(Bang) {
|
||||||
|
let directive = self.parse_directive()?;
|
||||||
|
items.push(directive);
|
||||||
} else if self.accepted(At)? {
|
} else if self.accepted(At)? {
|
||||||
let doc = pop_doc_comment(&mut items, eol_since_last_comment);
|
let doc = pop_doc_comment(&mut items, eol_since_last_comment);
|
||||||
items.push(Item::Recipe(self.parse_recipe(
|
items.push(Item::Recipe(self.parse_recipe(
|
||||||
@ -775,6 +778,24 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
Ok(value)
|
Ok(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_directive(&mut self) -> CompileResult<'src, Item<'src>> {
|
||||||
|
self.presume(Bang)?;
|
||||||
|
let name = self.expect(Identifier)?;
|
||||||
|
match name.lexeme() {
|
||||||
|
"include" => {
|
||||||
|
if let Some(include_line) = self.accept(Text)? {
|
||||||
|
Ok(Item::Include {
|
||||||
|
relative: include_line.lexeme().trim(),
|
||||||
|
absolute: None,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(self.error(CompileErrorKind::IncludeMissingPath)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
directive => Err(name.error(CompileErrorKind::UnknownDirective { directive })),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse a setting
|
/// Parse a setting
|
||||||
fn parse_set(&mut self) -> CompileResult<'src, Set<'src>> {
|
fn parse_set(&mut self) -> CompileResult<'src, Set<'src>> {
|
||||||
self.presume_keyword(Keyword::Set)?;
|
self.presume_keyword(Keyword::Set)?;
|
||||||
@ -1958,6 +1979,12 @@ mod tests {
|
|||||||
tree: (justfile (assignment a (if b == c d (if b == c d e)))),
|
tree: (justfile (assignment a (if b == c d (if b == c d e)))),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: include_directive,
|
||||||
|
text: "!include some/file/path.txt \n",
|
||||||
|
tree: (justfile (include "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",
|
||||||
@ -2048,7 +2075,7 @@ mod tests {
|
|||||||
column: 0,
|
column: 0,
|
||||||
width: 1,
|
width: 1,
|
||||||
kind: UnexpectedToken {
|
kind: UnexpectedToken {
|
||||||
expected: vec![At, BracketL, Comment, Eof, Eol, Identifier],
|
expected: vec![At, Bang, BracketL, Comment, Eof, Eol, Identifier],
|
||||||
found: BraceL,
|
found: BraceL,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -2400,4 +2427,16 @@ mod tests {
|
|||||||
expected: 3..3,
|
expected: 3..3,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
error! {
|
||||||
|
name: unknown_directive,
|
||||||
|
input: "!inclood",
|
||||||
|
offset: 1,
|
||||||
|
line: 0,
|
||||||
|
column: 1,
|
||||||
|
width: 7,
|
||||||
|
kind: UnknownDirective {
|
||||||
|
directive: "inclood"
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,12 +20,12 @@ pub fn run() -> Result<(), i32> {
|
|||||||
|
|
||||||
let config = Config::from_matches(&matches).map_err(Error::from);
|
let config = Config::from_matches(&matches).map_err(Error::from);
|
||||||
|
|
||||||
let (color, verbosity, unstable) = config
|
let (color, verbosity) = config
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|config| (config.color, config.verbosity, config.unstable))
|
.map(|config| (config.color, config.verbosity))
|
||||||
.unwrap_or((Color::auto(), Verbosity::default(), false));
|
.unwrap_or((Color::auto(), Verbosity::default()));
|
||||||
|
|
||||||
let loader = Loader::new(unstable);
|
let loader = Loader::new();
|
||||||
|
|
||||||
config
|
config
|
||||||
.and_then(|config| config.run(&loader))
|
.and_then(|config| config.run(&loader))
|
||||||
|
@ -65,7 +65,10 @@ impl Subcommand {
|
|||||||
return Self::edit(&search);
|
return Self::edit(&search);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (src, ast, justfile) = Self::compile(config, loader, &search)?;
|
let compilation = Self::compile(config, loader, &search)?;
|
||||||
|
let justfile = &compilation.justfile;
|
||||||
|
let ast = compilation.root_ast();
|
||||||
|
let src = compilation.root_src();
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
Choose { overrides, chooser } => {
|
Choose { overrides, chooser } => {
|
||||||
@ -86,7 +89,7 @@ impl Subcommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn run<'src>(
|
fn run<'src>(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
loader: &'src Loader,
|
loader: &'src Loader,
|
||||||
arguments: &[String],
|
arguments: &[String],
|
||||||
@ -165,8 +168,8 @@ impl Subcommand {
|
|||||||
overrides: &BTreeMap<String, String>,
|
overrides: &BTreeMap<String, String>,
|
||||||
search: &Search,
|
search: &Search,
|
||||||
) -> Result<(), (Error<'src>, bool)> {
|
) -> Result<(), (Error<'src>, bool)> {
|
||||||
let (_src, _ast, justfile) =
|
let compilation = Self::compile(config, loader, search).map_err(|err| (err, false))?;
|
||||||
Self::compile(config, loader, search).map_err(|err| (err, false))?;
|
let justfile = &compilation.justfile;
|
||||||
justfile
|
justfile
|
||||||
.run(config, search, overrides, arguments)
|
.run(config, search, overrides, arguments)
|
||||||
.map_err(|err| (err, justfile.settings.fallback))
|
.map_err(|err| (err, justfile.settings.fallback))
|
||||||
@ -176,18 +179,16 @@ impl Subcommand {
|
|||||||
config: &Config,
|
config: &Config,
|
||||||
loader: &'src Loader,
|
loader: &'src Loader,
|
||||||
search: &Search,
|
search: &Search,
|
||||||
) -> Result<(&'src str, Ast<'src>, Justfile<'src>), Error<'src>> {
|
) -> Result<Compilation<'src>, Error<'src>> {
|
||||||
let src = loader.load(&search.justfile)?;
|
let compilation = Compiler::compile(config.unstable, loader, &search.justfile)?;
|
||||||
|
|
||||||
let (ast, justfile) = Compiler::compile(src)?;
|
|
||||||
|
|
||||||
if config.verbosity.loud() {
|
if config.verbosity.loud() {
|
||||||
for warning in &justfile.warnings {
|
for warning in &compilation.justfile.warnings {
|
||||||
eprintln!("{}", warning.color_display(config.color.stderr()));
|
eprintln!("{}", warning.color_display(config.color.stderr()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((src, ast, justfile))
|
Ok(compilation)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn changelog() {
|
fn changelog() {
|
||||||
@ -196,7 +197,7 @@ impl Subcommand {
|
|||||||
|
|
||||||
fn choose<'src>(
|
fn choose<'src>(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
justfile: Justfile<'src>,
|
justfile: &Justfile<'src>,
|
||||||
search: &Search,
|
search: &Search,
|
||||||
overrides: &BTreeMap<String, String>,
|
overrides: &BTreeMap<String, String>,
|
||||||
chooser: Option<&str>,
|
chooser: Option<&str>,
|
||||||
@ -326,10 +327,10 @@ impl Subcommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dump(config: &Config, ast: Ast, justfile: Justfile) -> Result<(), Error<'static>> {
|
fn dump(config: &Config, ast: &Ast, justfile: &Justfile) -> Result<(), Error<'static>> {
|
||||||
match config.dump_format {
|
match config.dump_format {
|
||||||
DumpFormat::Json => {
|
DumpFormat::Json => {
|
||||||
serde_json::to_writer(io::stdout(), &justfile)
|
serde_json::to_writer(io::stdout(), justfile)
|
||||||
.map_err(|serde_json_error| Error::DumpJson { serde_json_error })?;
|
.map_err(|serde_json_error| Error::DumpJson { serde_json_error })?;
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
@ -360,7 +361,7 @@ impl Subcommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format(config: &Config, search: &Search, src: &str, ast: Ast) -> Result<(), Error<'static>> {
|
fn format(config: &Config, search: &Search, src: &str, ast: &Ast) -> Result<(), Error<'static>> {
|
||||||
config.require_unstable("The `--fmt` command is currently unstable.")?;
|
config.require_unstable("The `--fmt` command is currently unstable.")?;
|
||||||
|
|
||||||
let formatted = ast.to_string();
|
let formatted = ast.to_string();
|
||||||
@ -425,7 +426,7 @@ impl Subcommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list(config: &Config, justfile: Justfile) {
|
fn list(config: &Config, justfile: &Justfile) {
|
||||||
// Construct a target to alias map.
|
// Construct a target to alias map.
|
||||||
let mut recipe_aliases: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
|
let mut recipe_aliases: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
|
||||||
for alias in justfile.aliases.values() {
|
for alias in justfile.aliases.values() {
|
||||||
@ -507,7 +508,7 @@ impl Subcommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show<'src>(config: &Config, name: &str, justfile: Justfile<'src>) -> Result<(), Error<'src>> {
|
fn show<'src>(config: &Config, name: &str, justfile: &Justfile<'src>) -> Result<(), Error<'src>> {
|
||||||
if let Some(alias) = justfile.get_alias(name) {
|
if let Some(alias) = justfile.get_alias(name) {
|
||||||
let recipe = justfile.get_recipe(alias.target.name.lexeme()).unwrap();
|
let recipe = justfile.get_recipe(alias.target.name.lexeme()).unwrap();
|
||||||
println!("{alias}");
|
println!("{alias}");
|
||||||
@ -524,7 +525,7 @@ impl Subcommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn summary(config: &Config, justfile: Justfile) {
|
fn summary(config: &Config, justfile: &Justfile) {
|
||||||
if justfile.count() == 0 {
|
if justfile.count() == 0 {
|
||||||
if config.verbosity.loud() {
|
if config.verbosity.loud() {
|
||||||
eprintln!("Justfile contains no recipes.");
|
eprintln!("Justfile contains no recipes.");
|
||||||
@ -540,7 +541,7 @@ impl Subcommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn variables(justfile: Justfile) {
|
fn variables(justfile: &Justfile) {
|
||||||
for (i, (_, assignment)) in justfile.assignments.iter().enumerate() {
|
for (i, (_, assignment)) in justfile.assignments.iter().enumerate() {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
print!(" ");
|
print!(" ");
|
||||||
|
@ -13,8 +13,8 @@
|
|||||||
//! of existing justfiles.
|
//! of existing justfiles.
|
||||||
|
|
||||||
use {
|
use {
|
||||||
crate::compiler::Compiler,
|
crate::{compiler::Compiler, error::Error, loader::Loader},
|
||||||
std::{collections::BTreeMap, fs, io, path::Path},
|
std::{collections::BTreeMap, io, path::Path},
|
||||||
};
|
};
|
||||||
|
|
||||||
mod full {
|
mod full {
|
||||||
@ -26,11 +26,15 @@ mod full {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn summary(path: &Path) -> Result<Result<Summary, String>, io::Error> {
|
pub fn summary(path: &Path) -> Result<Result<Summary, String>, io::Error> {
|
||||||
let text = fs::read_to_string(path)?;
|
let loader = Loader::new();
|
||||||
|
|
||||||
match Compiler::compile(&text) {
|
match Compiler::compile(false, &loader, path) {
|
||||||
Ok((_, justfile)) => Ok(Ok(Summary::new(justfile))),
|
Ok(compilation) => Ok(Ok(Summary::new(&compilation.justfile))),
|
||||||
Err(compilation_error) => Ok(Err(compilation_error.to_string())),
|
Err(error) => Ok(Err(if let Error::Compile { compile_error } = error {
|
||||||
|
compile_error.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{error:?}")
|
||||||
|
})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,7 +45,7 @@ pub struct Summary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Summary {
|
impl Summary {
|
||||||
fn new(justfile: full::Justfile) -> Summary {
|
fn new(justfile: &full::Justfile) -> Summary {
|
||||||
let mut aliases = BTreeMap::new();
|
let mut aliases = BTreeMap::new();
|
||||||
|
|
||||||
for alias in justfile.aliases.values() {
|
for alias in justfile.aliases.values() {
|
||||||
@ -54,11 +58,11 @@ impl Summary {
|
|||||||
Summary {
|
Summary {
|
||||||
recipes: justfile
|
recipes: justfile
|
||||||
.recipes
|
.recipes
|
||||||
.into_iter()
|
.iter()
|
||||||
.map(|(name, recipe)| {
|
.map(|(name, recipe)| {
|
||||||
(
|
(
|
||||||
name.to_owned(),
|
(*name).to_string(),
|
||||||
Recipe::new(&recipe, aliases.remove(name).unwrap_or_default()),
|
Recipe::new(recipe, aliases.remove(name).unwrap_or_default()),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
use {super::*, crate::compiler::Compiler, pretty_assertions::assert_eq};
|
use {super::*, pretty_assertions::assert_eq};
|
||||||
|
|
||||||
pub(crate) fn compile(text: &str) -> Justfile {
|
pub(crate) fn compile(src: &str) -> Justfile {
|
||||||
match Compiler::compile(text) {
|
Compiler::test_compile(src).expect("expected successful compilation")
|
||||||
Ok((_, justfile)) => justfile,
|
|
||||||
Err(error) => panic!("Expected successful compilation but got error:\n {error}"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn config(args: &[&str]) -> Config {
|
pub(crate) fn config(args: &[&str]) -> Config {
|
||||||
@ -64,7 +61,11 @@ pub(crate) fn analysis_error(
|
|||||||
|
|
||||||
let ast = Parser::parse(&tokens).expect("Parsing failed in analysis test...");
|
let ast = Parser::parse(&tokens).expect("Parsing failed in analysis test...");
|
||||||
|
|
||||||
match Analyzer::analyze(&ast) {
|
let root = PathBuf::from("<ROOT>");
|
||||||
|
let mut asts: HashMap<PathBuf, Ast> = HashMap::new();
|
||||||
|
asts.insert(root.clone(), ast);
|
||||||
|
|
||||||
|
match Analyzer::analyze(&asts, &root) {
|
||||||
Ok(_) => panic!("Analysis unexpectedly succeeded"),
|
Ok(_) => panic!("Analysis unexpectedly succeeded"),
|
||||||
Err(have) => {
|
Err(have) => {
|
||||||
let want = CompileError {
|
let want = CompileError {
|
||||||
@ -97,9 +98,7 @@ macro_rules! run_error {
|
|||||||
let search = $crate::testing::search(&config);
|
let search = $crate::testing::search(&config);
|
||||||
|
|
||||||
if let Subcommand::Run{ overrides, arguments } = &config.subcommand {
|
if let Subcommand::Run{ overrides, arguments } = &config.subcommand {
|
||||||
match $crate::compiler::Compiler::compile(&$crate::unindent::unindent($src))
|
match $crate::testing::compile(&$crate::unindent::unindent($src))
|
||||||
.expect("Expected successful compilation")
|
|
||||||
.1
|
|
||||||
.run(
|
.run(
|
||||||
&config,
|
&config,
|
||||||
&search,
|
&search,
|
||||||
|
@ -6,6 +6,7 @@ pub(crate) enum TokenKind {
|
|||||||
Asterisk,
|
Asterisk,
|
||||||
At,
|
At,
|
||||||
Backtick,
|
Backtick,
|
||||||
|
Bang,
|
||||||
BangEquals,
|
BangEquals,
|
||||||
BraceL,
|
BraceL,
|
||||||
BraceR,
|
BraceR,
|
||||||
@ -48,6 +49,7 @@ impl Display for TokenKind {
|
|||||||
Asterisk => "'*'",
|
Asterisk => "'*'",
|
||||||
At => "'@'",
|
At => "'@'",
|
||||||
Backtick => "backtick",
|
Backtick => "backtick",
|
||||||
|
Bang => "'!'",
|
||||||
BangEquals => "'!='",
|
BangEquals => "'!='",
|
||||||
BraceL => "'{'",
|
BraceL => "'{'",
|
||||||
BraceR => "'}'",
|
BraceR => "'}'",
|
||||||
|
36
src/tree.rs
36
src/tree.rs
@ -7,57 +7,39 @@ use {
|
|||||||
/// Tree type, are only used in the Parser unit tests, providing a concise
|
/// Tree type, are only used in the Parser unit tests, providing a concise
|
||||||
/// notation for representing the expected results of parsing a given string.
|
/// notation for representing the expected results of parsing a given string.
|
||||||
macro_rules! tree {
|
macro_rules! tree {
|
||||||
{
|
{ ($($child:tt)*) } => {
|
||||||
($($child:tt)*)
|
|
||||||
} => {
|
|
||||||
$crate::tree::Tree::List(vec![$(tree!($child),)*])
|
$crate::tree::Tree::List(vec![$(tree!($child),)*])
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{ $atom:ident } => {
|
||||||
$atom:ident
|
|
||||||
} => {
|
|
||||||
$crate::tree::Tree::atom(stringify!($atom))
|
$crate::tree::Tree::atom(stringify!($atom))
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{ $atom:literal } => {
|
||||||
$atom:literal
|
|
||||||
} => {
|
|
||||||
$crate::tree::Tree::atom(format!("\"{}\"", $atom))
|
$crate::tree::Tree::atom(format!("\"{}\"", $atom))
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{ # } => {
|
||||||
#
|
|
||||||
} => {
|
|
||||||
$crate::tree::Tree::atom("#")
|
$crate::tree::Tree::atom("#")
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{ + } => {
|
||||||
+
|
|
||||||
} => {
|
|
||||||
$crate::tree::Tree::atom("+")
|
$crate::tree::Tree::atom("+")
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{ * } => {
|
||||||
*
|
|
||||||
} => {
|
|
||||||
$crate::tree::Tree::atom("*")
|
$crate::tree::Tree::atom("*")
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{ && } => {
|
||||||
&&
|
|
||||||
} => {
|
|
||||||
$crate::tree::Tree::atom("&&")
|
$crate::tree::Tree::atom("&&")
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{ == } => {
|
||||||
==
|
|
||||||
} => {
|
|
||||||
$crate::tree::Tree::atom("==")
|
$crate::tree::Tree::atom("==")
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{ != } => {
|
||||||
!=
|
|
||||||
} => {
|
|
||||||
$crate::tree::Tree::atom("!=")
|
$crate::tree::Tree::atom("!=")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ fn non_leading_byte_order_mark_produces_error() {
|
|||||||
)
|
)
|
||||||
.stderr(
|
.stderr(
|
||||||
"
|
"
|
||||||
error: Expected \'@\', \'[\', comment, end of file, end of line, or identifier, but found byte order mark
|
error: Expected \'@\', '!', \'[\', comment, end of file, end of line, or identifier, but found byte order mark
|
||||||
|
|
|
|
||||||
3 | \u{feff}
|
3 | \u{feff}
|
||||||
| ^
|
| ^
|
||||||
@ -41,7 +41,7 @@ fn dont_mention_byte_order_mark_in_errors() {
|
|||||||
.justfile("{")
|
.justfile("{")
|
||||||
.stderr(
|
.stderr(
|
||||||
"
|
"
|
||||||
error: Expected '@', '[', comment, end of file, end of line, or identifier, but found '{'
|
error: Expected '@', '!', '[', comment, end of file, end of line, or identifier, but found '{'
|
||||||
|
|
|
|
||||||
1 | {
|
1 | {
|
||||||
| ^
|
| ^
|
||||||
|
@ -55,22 +55,35 @@ fn include_directive_with_no_path() {
|
|||||||
.justfile("!include")
|
.justfile("!include")
|
||||||
.arg("--unstable")
|
.arg("--unstable")
|
||||||
.status(EXIT_FAILURE)
|
.status(EXIT_FAILURE)
|
||||||
.stderr_regex("error: !include directive on line 1 of `.*` has no argument\n")
|
.stderr(
|
||||||
|
"
|
||||||
|
error: !include directive has no argument
|
||||||
|
|
|
||||||
|
1 | !include
|
||||||
|
| ^
|
||||||
|
",
|
||||||
|
)
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn trailing_include() {
|
fn include_after_recipe() {
|
||||||
Test::new()
|
Test::new()
|
||||||
|
.tree(tree! {
|
||||||
|
"include.justfile": "
|
||||||
|
a:
|
||||||
|
@echo A
|
||||||
|
",
|
||||||
|
})
|
||||||
.justfile(
|
.justfile(
|
||||||
"
|
"
|
||||||
b:
|
b: a
|
||||||
!include ./include.justfile
|
!include ./include.justfile
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
.arg("--unstable")
|
.arg("--unstable")
|
||||||
.status(EXIT_FAILURE)
|
.test_round_trip(false)
|
||||||
.stderr("error: Expected character `=`\n |\n2 | !include ./include.justfile\n | ^\n")
|
.stdout("A\n")
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user