Handle line interpolation parsing

This commit is contained in:
Casey Rodarmor 2016-10-23 23:38:49 -07:00
parent f01ef06bf0
commit 9aed7ca129
2 changed files with 102 additions and 10 deletions

View File

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

View File

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