summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGreg Shuflin <greg@everydayimshuflin.com>2023-11-21 11:28:59 -0800
committerGitHub <noreply@github.com>2023-11-21 11:28:59 -0800
commitf745316e881b96803373f9060d48ddccc226de44 (patch)
tree2c550f8f2debdf2e93ce9ea8758f6504f154099f
parentba89f1a40a1d7fcadc4b8bfa4dc016fa6bbcc13e (diff)
Move !include processing into compiler (#1618)
-rw-r--r--src/analyzer.rs65
-rw-r--r--src/compilation.rs19
-rw-r--r--src/compile_error.rs2
-rw-r--r--src/compile_error_kind.rs4
-rw-r--r--src/compiler.rs136
-rw-r--r--src/error.rs15
-rw-r--r--src/fuzzing.rs2
-rw-r--r--src/item.rs5
-rw-r--r--src/lexer.rs82
-rw-r--r--src/lib.rs5
-rw-r--r--src/loader.rs202
-rw-r--r--src/node.rs1
-rw-r--r--src/parser.rs41
-rw-r--r--src/run.rs8
-rw-r--r--src/subcommand.rs37
-rw-r--r--src/summary.rs24
-rw-r--r--src/testing.rs19
-rw-r--r--src/token_kind.rs2
-rw-r--r--src/tree.rs36
-rw-r--r--tests/byte_order_mark.rs4
-rw-r--r--tests/includes.rs23
21 files changed, 386 insertions, 346 deletions
diff --git a/src/analyzer.rs b/src/analyzer.rs
index 45166213..ff4051c3 100644
--- a/src/analyzer.rs
+++ b/src/analyzer.rs
@@ -8,35 +8,54 @@ pub(crate) struct Analyzer<'src> {
}
impl<'src> Analyzer<'src> {
- pub(crate) fn analyze(ast: &Ast<'src>) -> CompileResult<'src, Justfile<'src>> {
- Analyzer::default().justfile(ast)
+ pub(crate) fn analyze(
+ 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();
- for item in &ast.items {
- match item {
- Item::Alias(alias) => {
- self.analyze_alias(alias)?;
- self.aliases.insert(alias.clone());
- }
- Item::Assignment(assignment) => {
- self.analyze_assignment(assignment)?;
- self.assignments.insert(assignment.clone());
- }
- Item::Comment(_) => (),
- Item::Recipe(recipe) => {
- if recipe.enabled() {
- Self::analyze_recipe(recipe)?;
- recipes.push(recipe);
+ 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 {
+ match item {
+ Item::Alias(alias) => {
+ self.analyze_alias(alias)?;
+ self.aliases.insert(alias.clone());
+ }
+ Item::Assignment(assignment) => {
+ self.analyze_assignment(assignment)?;
+ 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));
@@ -65,7 +84,6 @@ impl<'src> Analyzer<'src> {
}
Ok(Justfile {
- warnings: ast.warnings.clone(),
first: recipes
.values()
.fold(None, |accumulator, next| match accumulator {
@@ -80,6 +98,7 @@ impl<'src> Analyzer<'src> {
assignments: self.assignments,
recipes,
settings,
+ warnings,
})
}
diff --git a/src/compilation.rs b/src/compilation.rs
new file mode 100644
index 00000000..c3bd9d51
--- /dev/null
+++ b/src/compilation.rs
@@ -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()
+ }
+}
diff --git a/src/compile_error.rs b/src/compile_error.rs
index 7a0d6f9c..97406d40 100644
--- a/src/compile_error.rs
+++ b/src/compile_error.rs
@@ -135,6 +135,7 @@ impl Display for CompileError<'_> {
Count("argument", *found),
expected.display(),
),
+ IncludeMissingPath => write!(f, "!include directive has no argument",),
InconsistentLeadingWhitespace { expected, found } => write!(
f,
"Recipe line has inconsistent leading whitespace. Recipe started with `{}` but found \
@@ -202,6 +203,7 @@ impl Display for CompileError<'_> {
UnknownDependency { recipe, 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}`"),
UnknownSetting { setting } => write!(f, "Unknown setting `{setting}`"),
UnknownStartOfToken => write!(f, "Unknown start of token:"),
diff --git a/src/compile_error_kind.rs b/src/compile_error_kind.rs
index acd8b2f7..9cbd68ba 100644
--- a/src/compile_error_kind.rs
+++ b/src/compile_error_kind.rs
@@ -58,6 +58,7 @@ pub(crate) enum CompileErrorKind<'src> {
found: usize,
expected: Range<usize>,
},
+ IncludeMissingPath,
InconsistentLeadingWhitespace {
expected: &'src str,
found: &'src str,
@@ -110,6 +111,9 @@ pub(crate) enum CompileErrorKind<'src> {
recipe: &'src str,
unknown: &'src str,
},
+ UnknownDirective {
+ directive: &'src str,
+ },
UnknownFunction {
function: &'src str,
},
diff --git a/src/compiler.rs b/src/compiler.rs
index 999feead..c035921f 100644
--- a/src/compiler.rs
+++ b/src/compiler.rs
@@ -3,11 +3,141 @@ use super::*;
pub(crate) struct 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 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)
+ }
+}
+
+#[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();
- Ok((ast, justfile))
+ assert_matches!(loader_output, Error::CircularInclude { current, include }
+ if current == tmp.path().join("subdir").join("justfile_b").lexiclean() &&
+ include == tmp.path().join("justfile").lexiclean()
+ );
}
}
diff --git a/src/error.rs b/src/error.rs
index 60548697..d7573cf4 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -91,19 +91,12 @@ pub(crate) enum Error<'src> {
GetConfirmation {
io_error: io::Error,
},
- IncludeMissingPath {
- file: PathBuf,
- line: usize,
- },
InitExists {
justfile: PathBuf,
},
Internal {
message: String,
},
- InvalidDirective {
- line: String,
- },
Io {
recipe: &'src str,
io_error: io::Error,
@@ -338,11 +331,6 @@ impl<'src> ColorDisplay for Error<'src> {
GetConfirmation { 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 } => {
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} \
consider filing an issue: https://github.com/casey/just/issues/new")?;
}
- InvalidDirective { line } => {
- write!(f, "Invalid directive: {line}")?;
- }
Io { recipe, io_error } => {
match io_error.kind() {
io::ErrorKind::NotFound => write!(f, "Recipe `{recipe}` could not be run because just could not find the shell: {io_error}"),
diff --git a/src/fuzzing.rs b/src/fuzzing.rs
index 4b8884d5..c4438eb1 100644
--- a/src/fuzzing.rs
+++ b/src/fuzzing.rs
@@ -1,5 +1,5 @@
use super::*;
pub fn compile(text: &str) {
- let _ = compiler::Compiler::compile(text);
+ let _ = testing::compile(text);
}
diff --git a/src/item.rs b/src/item.rs
index 4fdbddf7..894bae14 100644
--- a/src/item.rs
+++ b/src/item.rs
@@ -8,6 +8,10 @@ pub(crate) enum Item<'src> {
Comment(&'src str),
Recipe(UnresolvedRecipe<'src>),
Set(Set<'src>),
+ Include {
+ relative: &'src str,
+ absolute: Option<PathBuf>,
+ },
}
impl<'src> Display for Item<'src> {
@@ -18,6 +22,7 @@ impl<'src> Display for Item<'src> {
Item::Comment(comment) => write!(f, "{comment}"),
Item::Recipe(recipe) => write!(f, "{}", recipe.color_display(Color::never())),
Item::Set(set) => write!(f, "{set}"),
+ Item::Include { relative, .. } => write!(f, "!include {relative}"),
}
}
}
diff --git a/src/lexer.rs b/src/lexer.rs
index b175a97d..3dca62b9 100644
--- a/src/lexer.rs
+++ b/src/lexer.rs
@@ -470,7 +470,7 @@ impl<'src> Lexer<'src> {
fn lex_normal(&mut self, start: char) -> CompileResult<'src, ()> {
match start {
' ' | '\t' => self.lex_whitespace(),
- '!' => self.lex_digraph('!', '=', BangEquals),
+ '!' => self.lex_bang(),
'#' => self.lex_comment(),
'$' => self.lex_single(Dollar),
'&' => self.lex_digraph('&', '&', AmpersandAmpersand),
@@ -674,6 +674,33 @@ impl<'src> Lexer<'src> {
!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
fn lex_digraph(&mut self, left: char, right: char, token: TokenKind) -> CompileResult<'src, ()> {
self.presume(left)?;
@@ -942,6 +969,7 @@ mod tests {
AmpersandAmpersand => "&&",
Asterisk => "*",
At => "@",
+ Bang => "!",
BangEquals => "!=",
BraceL => "{",
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! {
name: tokenize_space_then_tab,
input: "a:
@@ -2222,12 +2274,12 @@ mod tests {
error! {
name: unexpected_character_after_bang,
- input: "!{",
+ input: "!%",
offset: 1,
line: 0,
column: 1,
width: 1,
- kind: UnexpectedCharacter { expected: '=' },
+ kind: UnknownStartOfToken,
}
error! {
@@ -2245,30 +2297,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! {
name: ampersand_eof,
input: "&",
offset: 1,
diff --git a/src/lib.rs b/src/lib.rs
index fa4b4e60..bd4b8335 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -12,7 +12,7 @@ pub(crate) use {
crate::{
alias::Alias, analyzer::Analyzer, assignment::Assignment,
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,
conditional_operator::ConditionalOperator, config::Config, config_error::ConfigError,
count::Count, delimiter::Delimiter, dependency::Dependency, dump_format::DumpFormat,
@@ -34,7 +34,7 @@ pub(crate) use {
},
std::{
cmp,
- collections::{BTreeMap, BTreeSet},
+ collections::{BTreeMap, BTreeSet, HashMap},
env,
ffi::{OsStr, OsString},
fmt::{self, Debug, Display, Formatter},
@@ -113,6 +113,7 @@ mod binding;
mod color;
mod color_display;
mod command_ext;
+mod compilation;
mod compile_error;
mod compile_error_kind;
mod compiler;
diff --git a/src/loader.rs b/src/loader.rs
index 7ea5612e..b1cc0947 100644
--- a/src/loader.rs
+++ b/src/loader.rs
@@ -1,216 +1,22 @@
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 {
arena: Arena<String>,
- unstable: bool,
}
impl Loader {
- pub(crate) fn new(unstable: bool) -> Self {
+ pub(crate) fn new() -> Self {
Loader {
arena: Arena::new(),
- unstable,
}
}
pub(crate) fn load<'src>(&'src self, path: &Path) -> RunResult<&'src str> {
- let src = self.load_recursive(path, HashSet::new())?;
- 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 {
+ let src = fs::read_to_string(path).map_err(|io_error| Error::Load {
path: path.to_owned(),
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();
+ })?;
- 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()
- );
+ Ok(self.arena.alloc(src))
}
}
diff --git a/src/node.rs b/src/node.rs
index 924bcf34..a95933bb 100644
--- a/src/node.rs
+++ b/src/node.rs
@@ -23,6 +23,7 @@ impl<'src> Node<'src> for Item<'src> {
Item::Comment(comment) => comment.tree(),
Item::Recipe(recipe) => recipe.tree(),
Item::Set(set) => set.tree(),
+ Item::Include { relative, .. } => Tree::atom("include").push(format!("\"{relative}\"")),
}
}
}
diff --git a/src/parser.rs b/src/parser.rs
index 15590c08..613cb11d 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -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)? {
let doc = pop_doc_comment(&mut items, eol_since_last_comment);
items.push(Item::Recipe(self.parse_recipe(
@@ -775,6 +778,24 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
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
fn parse_set(&mut self) -> CompileResult<'src, Set<'src>> {
self.presume_keyword(Keyword::Set)?;
@@ -1958,6 +1979,12 @@ mod tests {
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! {
name: alias_syntax_multiple_rhs,
input: "alias foo := bar baz",
@@ -2048,7 +2075,7 @@ mod tests {
column: 0,
width: 1,
kind: UnexpectedToken {
- expected: vec![At, BracketL, Comment, Eof, Eol, Identifier],
+ expected: vec![At, Bang, BracketL, Comment, Eof, Eol, Identifier],
found: BraceL,
},
}
@@ -2400,4 +2427,16 @@ mod tests {
expected: 3..3,
},
}
+
+ error! {
+ name: unknown_directive,
+ input: "!inclood",
+ offset: 1,
+ line: 0,
+ column: 1,
+ width: 7,
+ kind: UnknownDirective {
+ directive: "inclood"
+ },
+ }
}
diff --git a/src/run.rs b/src/run.rs
index fd8176cc..764440ea 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -20,12 +20,12 @@ pub fn run() -> Result<(), i32> {
let config = Config::from_matches(&matches).map_err(Error::from);
- let (color, verbosity, unstable) = config
+ let (color, verbosity) = config
.as_ref()
- .map(|config| (config.color, config.verbosity, config.unstable))
- .unwrap_or((Color::auto(), Verbosity::default(), false));
+ .map(|config| (config.color, config.verbosity))
+ .unwrap_or((Color::auto(), Verbosity::default()));
- let loader = Loader::new(unstable);
+ let loader = Loader::new();
config
.and_then(|config| config.run(&loader))
diff --git a/src/subcommand.rs b/src/subcommand.rs
index b58cc9a3..99dacec4 100644
--- a/src/subcommand.rs
+++ b/src/subcommand.rs
@@ -65,7 +65,10 @@ impl Subcommand {
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 {
Choose { overrides, chooser } => {
@@ -86,7 +89,7 @@ impl Subcommand {
Ok(())
}
- pub(crate) fn run<'src>(
+ fn run<'src>(
config: &Config,
loader: &'src Loader,
arguments: &[String],
@@ -165,8 +168,8 @@ impl Subcommand {
overrides: &BTreeMap<String, String>,
search: &Search,
) -> Result<(), (Error<'src>, bool)> {
- let (_src, _ast, justfile) =
- Self::compile(config, loader, search).map_err(|err| (err, false))?;
+ let compilation = Self::compile(config, loader, search).map_err(|err| (err, false))?;
+ let justfile = &compilation.justfile;
justfile
.run(config, search, overrides, arguments)
.map_err(|err| (err, justfile.settings.fallback))
@@ -176,18 +179,16 @@ impl Subcommand {
config: &Config,
loader: &'src Loader,
search: &Search,
- ) -> Result<(&'src str, Ast<'src>, Justfile<'src>), Error<'src>> {
- let src = loader.load(&search.justfile)?;
-
- let (ast, justfile) = Compiler::compile(src)?;
+ ) -> Result<Compilation<'src>, Error<'src>> {
+ let compilation = Compiler::compile(config.unstable, loader, &search.justfile)?;
if config.verbosity.loud() {
- for warning in &justfile.warnings {
+ for warning in &compilation.justfile.warnings {
eprintln!("{}", warning.color_display(config.color.stderr()));
}
}
- Ok((src, ast, justfile))
+ Ok(compilation)
}
fn changelog() {
@@ -196,7 +197,7 @@ impl Subcommand {
fn choose<'src>(
config: &Config,
- justfile: Justfile<'src>,
+ justfile: &Justfile<'src>,
search: &Search,
overrides: &BTreeMap<String, String>,
chooser: Option<&str>,
@@ -326,10 +327,10 @@ impl Subcommand {
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 {
DumpFormat::Json => {
- serde_json::to_writer(io::stdout(), &justfile)
+ serde_json::to_writer(io::std