Move !include processing into compiler (#1618)

This commit is contained in:
Greg Shuflin 2023-11-21 11:28:59 -08:00 committed by GitHub
parent ba89f1a40a
commit f745316e88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 387 additions and 347 deletions

View File

@ -8,13 +8,26 @@ 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();
let mut stack = Vec::new();
stack.push(asts.get(root).unwrap());
let mut warnings = Vec::new();
while let Some(ast) = stack.pop() {
for item in &ast.items { for item in &ast.items {
match item { match item {
Item::Alias(alias) => { Item::Alias(alias) => {
@ -36,8 +49,14 @@ impl<'src> Analyzer<'src> {
self.analyze_set(set)?; self.analyze_set(set)?;
self.sets.insert(set.clone()); self.sets.insert(set.clone());
} }
Item::Include { absolute, .. } => {
stack.push(asts.get(absolute.as_ref().unwrap()).unwrap());
} }
} }
}
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
View 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()
}
}

View File

@ -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:"),

View File

@ -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,
}, },

View File

@ -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(&current)?;
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();
asts.insert(root.clone(), ast);
Analyzer::analyze(&asts, &root)
}
}
Ok((ast, justfile)) #[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()
);
} }
} }

View File

@ -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}"),

View File

@ -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);
} }

View File

@ -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}"),
} }
} }
} }

View File

@ -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: "&",

View File

@ -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;

View File

@ -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> {
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(); Ok(self.arena.alloc(src))
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()
);
} }
} }

View File

@ -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}\"")),
} }
} }
} }

View File

@ -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"
},
}
} }

View File

@ -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))

View File

@ -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!(" ");

View File

@ -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(),

View File

@ -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,

View File

@ -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 => "'}'",

View File

@ -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("!=")
}; };
} }

View File

@ -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 | {
| ^ | ^

View File

@ -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();
} }