Handle line interpolation parsing
This commit is contained in:
parent
f01ef06bf0
commit
9aed7ca129
80
src/lib.rs
80
src/lib.rs
@ -13,7 +13,7 @@ extern crate tempdir;
|
|||||||
use std::io::prelude::*;
|
use std::io::prelude::*;
|
||||||
|
|
||||||
use std::{fs, fmt, process, io};
|
use std::{fs, fmt, process, io};
|
||||||
use std::collections::{BTreeMap, HashSet};
|
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
@ -55,8 +55,8 @@ struct Recipe<'a> {
|
|||||||
line_number: usize,
|
line_number: usize,
|
||||||
name: &'a str,
|
name: &'a str,
|
||||||
lines: Vec<&'a str>,
|
lines: Vec<&'a str>,
|
||||||
// fragments: Vec<Vec<Fragment<'a>>>,
|
fragments: Vec<Vec<Fragment<'a>>>,
|
||||||
// variables: BTreeSet<&'a str>,
|
variables: BTreeSet<&'a str>,
|
||||||
dependencies: Vec<&'a str>,
|
dependencies: Vec<&'a str>,
|
||||||
dependency_tokens: Vec<Token<'a>>,
|
dependency_tokens: Vec<Token<'a>>,
|
||||||
arguments: Vec<&'a str>,
|
arguments: Vec<&'a str>,
|
||||||
@ -64,12 +64,11 @@ struct Recipe<'a> {
|
|||||||
shebang: bool,
|
shebang: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
#[derive(PartialEq, Debug)]
|
||||||
enum Fragment<'a> {
|
enum Fragment<'a> {
|
||||||
Text{text: &'a str},
|
Text{text: &'a str},
|
||||||
Variable{name: &'a str},
|
Variable{name: &'a str},
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn error_from_signal(recipe: &str, exit_status: process::ExitStatus) -> RunError {
|
fn error_from_signal(recipe: &str, exit_status: process::ExitStatus) -> RunError {
|
||||||
@ -184,13 +183,23 @@ impl<'a> Display for Recipe<'a> {
|
|||||||
for dependency in &self.dependencies {
|
for dependency in &self.dependencies {
|
||||||
try!(write!(f, " {}", dependency))
|
try!(write!(f, " {}", dependency))
|
||||||
}
|
}
|
||||||
for (i, line) in self.lines.iter().enumerate() {
|
|
||||||
|
|
||||||
|
for (i, fragments) in self.fragments.iter().enumerate() {
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
try!(writeln!(f, ""));
|
try!(writeln!(f, ""));
|
||||||
}
|
}
|
||||||
try!(write!(f, " {}", line));
|
for (j, fragment) in fragments.iter().enumerate() {
|
||||||
if i + 1 < self.lines.len() {
|
if j == 0 {
|
||||||
try!(writeln!(f, ""));
|
try!(write!(f, " "));
|
||||||
|
}
|
||||||
|
match *fragment {
|
||||||
|
Fragment::Text{text} => try!(write!(f, "{}", text)),
|
||||||
|
Fragment::Variable{name} => try!(write!(f, "{}{}{}", "{{", name, "}}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i + 1 < self.fragments.len() {
|
||||||
|
try!(write!(f, "\n"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -253,6 +262,8 @@ enum ErrorKind<'a> {
|
|||||||
DuplicateArgument{recipe: &'a str, argument: &'a str},
|
DuplicateArgument{recipe: &'a str, argument: &'a str},
|
||||||
DuplicateRecipe{recipe: &'a str, first: usize},
|
DuplicateRecipe{recipe: &'a str, first: usize},
|
||||||
MixedLeadingWhitespace{whitespace: &'a str},
|
MixedLeadingWhitespace{whitespace: &'a str},
|
||||||
|
UnmatchedInterpolationDelimiter{recipe: &'a str},
|
||||||
|
BadInterpolationVariableName{recipe: &'a str, text: &'a str},
|
||||||
ExtraLeadingWhitespace,
|
ExtraLeadingWhitespace,
|
||||||
InconsistentLeadingWhitespace{expected: &'a str, found: &'a str},
|
InconsistentLeadingWhitespace{expected: &'a str, found: &'a str},
|
||||||
OuterShebang,
|
OuterShebang,
|
||||||
@ -340,6 +351,12 @@ impl<'a> Display for Error<'a> {
|
|||||||
ErrorKind::OuterShebang => {
|
ErrorKind::OuterShebang => {
|
||||||
try!(writeln!(f, "a shebang \"#!\" is reserved syntax outside of recipes"))
|
try!(writeln!(f, "a shebang \"#!\" is reserved syntax outside of recipes"))
|
||||||
}
|
}
|
||||||
|
ErrorKind::UnmatchedInterpolationDelimiter{recipe} => {
|
||||||
|
try!(writeln!(f, "recipe {} contains an unmatched {}", recipe, "{{"))
|
||||||
|
}
|
||||||
|
ErrorKind::BadInterpolationVariableName{recipe, text} => {
|
||||||
|
try!(writeln!(f, "recipe {} contains a bad variable interpolation: {}", recipe, text))
|
||||||
|
}
|
||||||
ErrorKind::UnknownDependency{recipe, unknown} => {
|
ErrorKind::UnknownDependency{recipe, unknown} => {
|
||||||
try!(writeln!(f, "recipe {} has unknown dependency {}", recipe, unknown));
|
try!(writeln!(f, "recipe {} has unknown dependency {}", recipe, unknown));
|
||||||
}
|
}
|
||||||
@ -782,6 +799,7 @@ impl<'a> Parser<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut lines = vec![];
|
let mut lines = vec![];
|
||||||
|
let mut line_tokens = vec![];
|
||||||
let mut shebang = false;
|
let mut shebang = false;
|
||||||
|
|
||||||
if self.accepted(Indent) {
|
if self.accepted(Indent) {
|
||||||
@ -794,8 +812,8 @@ impl<'a> Parser<'a> {
|
|||||||
} else if !shebang && (line.lexeme.starts_with(' ') || line.lexeme.starts_with('\t')) {
|
} else if !shebang && (line.lexeme.starts_with(' ') || line.lexeme.starts_with('\t')) {
|
||||||
return Err(line.error(ErrorKind::ExtraLeadingWhitespace));
|
return Err(line.error(ErrorKind::ExtraLeadingWhitespace));
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push(line.lexeme);
|
lines.push(line.lexeme);
|
||||||
|
line_tokens.push(line);
|
||||||
if !self.peek(Dedent) {
|
if !self.peek(Dedent) {
|
||||||
if let Some(token) = self.expect_eol() {
|
if let Some(token) = self.expect_eol() {
|
||||||
return Err(self.unexpected_token(&token, &[Eol]));
|
return Err(self.unexpected_token(&token, &[Eol]));
|
||||||
@ -813,6 +831,46 @@ impl<'a> Parser<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut fragments = vec![];
|
||||||
|
let mut variables = BTreeSet::new();
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref FRAGMENT: Regex = re(r"^(.*?)\{\{(.*?)\}\}" );
|
||||||
|
static ref UNMATCHED: Regex = re(r"^.*?\{\{" );
|
||||||
|
static ref VARIABLE: Regex = re(r"^[ \t]*([a-z](-?[a-z0-9])*)[ \t]*$");
|
||||||
|
}
|
||||||
|
|
||||||
|
for line in &line_tokens {
|
||||||
|
let mut line_fragments = vec![];
|
||||||
|
let mut rest = line.lexeme;
|
||||||
|
while !rest.is_empty() {
|
||||||
|
if let Some(captures) = FRAGMENT.captures(rest) {
|
||||||
|
let prefix = captures.at(1).unwrap();
|
||||||
|
if !prefix.is_empty() {
|
||||||
|
line_fragments.push(Fragment::Text{text: prefix});
|
||||||
|
}
|
||||||
|
let interior = captures.at(2).unwrap();
|
||||||
|
if let Some(captures) = VARIABLE.captures(interior) {
|
||||||
|
let name = captures.at(1).unwrap();
|
||||||
|
line_fragments.push(Fragment::Variable{name: name});
|
||||||
|
variables.insert(name);
|
||||||
|
} else {
|
||||||
|
return Err(line.error(ErrorKind::BadInterpolationVariableName{
|
||||||
|
recipe: name,
|
||||||
|
text: interior,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
rest = &rest[captures.at(0).unwrap().len()..];
|
||||||
|
} else if UNMATCHED.is_match(rest) {
|
||||||
|
return Err(line.error(ErrorKind::UnmatchedInterpolationDelimiter{recipe: name}));
|
||||||
|
} else {
|
||||||
|
line_fragments.push(Fragment::Text{text: rest});
|
||||||
|
rest = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fragments.push(line_fragments);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Recipe {
|
Ok(Recipe {
|
||||||
line_number: line_number,
|
line_number: line_number,
|
||||||
name: name,
|
name: name,
|
||||||
@ -820,6 +878,8 @@ impl<'a> Parser<'a> {
|
|||||||
dependency_tokens: dependency_tokens,
|
dependency_tokens: dependency_tokens,
|
||||||
arguments: arguments,
|
arguments: arguments,
|
||||||
argument_tokens: argument_tokens,
|
argument_tokens: argument_tokens,
|
||||||
|
fragments: fragments,
|
||||||
|
variables: variables,
|
||||||
lines: lines,
|
lines: lines,
|
||||||
shebang: shebang,
|
shebang: shebang,
|
||||||
})
|
})
|
||||||
|
32
src/tests.rs
32
src/tests.rs
@ -58,6 +58,10 @@ fn parse_summary(input: &str, output: &str) {
|
|||||||
for recipe in justfile.recipes {
|
for recipe in justfile.recipes {
|
||||||
s += &format!("{}\n", recipe.1);
|
s += &format!("{}\n", recipe.1);
|
||||||
}
|
}
|
||||||
|
if s != output {
|
||||||
|
println!("got:\n\"{}\"\n", s);
|
||||||
|
println!("\texpected:\n\"{}\"", output);
|
||||||
|
}
|
||||||
assert_eq!(s, output);
|
assert_eq!(s, output);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,12 +178,14 @@ z:
|
|||||||
hello a b c : x y z #hello
|
hello a b c : x y z #hello
|
||||||
#! blah
|
#! blah
|
||||||
#blarg
|
#blarg
|
||||||
|
{{ hello }}
|
||||||
1
|
1
|
||||||
2
|
2
|
||||||
3
|
3
|
||||||
", "hello a b c: x y z
|
", "hello a b c: x y z
|
||||||
#! blah
|
#! blah
|
||||||
#blarg
|
#blarg
|
||||||
|
{{hello}}
|
||||||
1
|
1
|
||||||
2
|
2
|
||||||
3
|
3
|
||||||
@ -434,3 +440,29 @@ fn bad_recipe_names() {
|
|||||||
bad_name("a: 9a", "9a", 3, 0, 3);
|
bad_name("a: 9a", "9a", 3, 0, 3);
|
||||||
bad_name("a:\nZ:", "Z", 3, 1, 0);
|
bad_name("a:\nZ:", "Z", 3, 1, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bad_interpolation_variable_name() {
|
||||||
|
let text = "a:\n echo {{hello--hello}}";
|
||||||
|
parse_error(text, Error {
|
||||||
|
text: text,
|
||||||
|
index: 4,
|
||||||
|
line: 1,
|
||||||
|
column: 1,
|
||||||
|
width: Some(21),
|
||||||
|
kind: ErrorKind::BadInterpolationVariableName{recipe: "a", text: "hello--hello"}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unmatched_interpolation_delimiter() {
|
||||||
|
let text = "a:\n echo {{";
|
||||||
|
parse_error(text, Error {
|
||||||
|
text: text,
|
||||||
|
index: 4,
|
||||||
|
line: 1,
|
||||||
|
column: 1,
|
||||||
|
width: Some(7),
|
||||||
|
kind: ErrorKind::UnmatchedInterpolationDelimiter{recipe: "a"}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user