Added proper errors

This commit is contained in:
Casey Rodarmor 2016-10-02 15:31:28 -07:00
parent 8ec000c159
commit ec2b6c59f5

View File

@ -2,8 +2,10 @@ extern crate regex;
use std::io::prelude::*; use std::io::prelude::*;
use std::{io, fs, env}; use std::{io, fs, env, fmt};
use std::collections::{HashSet, BTreeMap}; use std::collections::{HashSet, BTreeMap};
use std::fmt::Display;
use regex::Regex;
macro_rules! warn { macro_rules! warn {
($($arg:tt)*) => {{ ($($arg:tt)*) => {{
@ -20,63 +22,239 @@ macro_rules! die {
}}; }};
} }
fn re(pattern: &str) -> regex::Regex { trait Slurp {
regex::Regex::new(pattern).unwrap() fn slurp(&mut self) -> Result<String, std::io::Error>;
}
impl Slurp for fs::File {
fn slurp(&mut self) -> Result<String, std::io::Error> {
let mut destination = String::new();
try!(self.read_to_string(&mut destination));
Ok(destination)
}
}
fn re(pattern: &str) -> Regex {
Regex::new(pattern).unwrap()
} }
struct Recipe<'a> { struct Recipe<'a> {
_line: u64, line: usize,
name: &'a str, name: &'a str,
leading_whitespace: &'a str, leading_whitespace: &'a str,
commands: Vec<&'a str>, commands: Vec<&'a str>,
dependencies: HashSet<&'a str>, dependencies: HashSet<&'a str>,
} }
struct Resolver<'a> { struct Error<'a> {
recipes: &'a BTreeMap<&'a str, Recipe<'a>>, text: &'a str,
resolved: HashSet<&'a str>, line: usize,
seen: HashSet<&'a str>, kind: ErrorKind<'a>
stack: Vec<&'a str>,
} }
fn resolve<'a> (recipes: &'a BTreeMap<&'a str, Recipe<'a>>) { enum ErrorKind<'a> {
let mut resolver = Resolver { CircularDependency{circle: Vec<&'a str>},
recipes: recipes, DuplicateDependency{name: &'a str},
resolved: HashSet::new(), DuplicateRecipe{first: usize, name: &'a str},
seen: HashSet::new(), InconsistentLeadingWhitespace{expected: &'a str, found: &'a str},
stack: vec![], Shebang,
}; UnknownDependency{name: &'a str, unknown: &'a str},
Unparsable,
UnparsableDependencies,
}
for (_, recipe) in recipes { fn error<'a>(text: &'a str, line: usize, kind: ErrorKind<'a>)
resolver.resolve(recipe); -> Error<'a>
{
Error {
text: text,
line: line,
kind: kind,
} }
} }
impl<'a> Resolver<'a> { fn show_whitespace(text: &str) -> String {
fn resolve(&mut self, recipe: &'a Recipe) { text.chars().map(|c| match c { '\t' => 't', ' ' => 's', _ => c }).collect()
self.stack.push(recipe.name); }
self.seen.insert(recipe.name);
for dependency_name in &recipe.dependencies { impl<'a> Display for Error<'a> {
match self.recipes.get(dependency_name) { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
Some(dependency) => if !self.resolved.contains(dependency.name) { try!(write!(f, "justfile:{}: ", self.line));
if self.seen.contains(dependency.name) {
let first = self.stack[0]; match self.kind {
self.stack.push(first); ErrorKind::CircularDependency{ref circle} => {
die!("Circular dependency: {}", try!(write!(f, "circular dependency: {}", circle.join(" -> ")));
self.stack.iter() return Ok(());
.skip_while(|name| **name != dependency.name) }
.cloned().collect::<Vec<&str>>().join(" -> ")); ErrorKind::DuplicateDependency{name} => {
try!(writeln!(f, "duplicate dependency: {}", name));
}
ErrorKind::DuplicateRecipe{first, name} => {
try!(write!(f, "duplicate recipe: {} appears on lines {} and {}",
name, first, self.line));
return Ok(());
}
ErrorKind::InconsistentLeadingWhitespace{expected, found} => {
try!(writeln!(f,
"inconsistant leading whitespace: recipe started with {} but found line with {}:",
show_whitespace(expected), show_whitespace(found)
));
}
ErrorKind::Shebang => {
try!(writeln!(f, "shebang \"#!\" is reserved syntax"))
}
ErrorKind::UnknownDependency{name, unknown} => {
try!(writeln!(f, "recipe {} has unknown dependency {}", name, unknown));
}
ErrorKind::Unparsable => {
try!(writeln!(f, "could not parse line:"));
}
ErrorKind::UnparsableDependencies => {
try!(writeln!(f, "could not parse dependencies:"));
}
}
match self.text.lines().nth(self.line) {
Some(line) => try!(write!(f, "{}", line)),
None => die!("internal error: Error has invalid line number: {}", self.line),
}
Ok(())
}
}
struct Justfile<'a> {
_recipes: BTreeMap<&'a str, Recipe<'a>>
}
fn parse<'a>(text: &'a str) -> Result<Justfile, Error> {
let shebang_re = re(r"^\s*#!(.*)$");
let comment_re = re(r"^\s*#[^!].*$");
let command_re = re(r"^(\s+)(.*)$");
let blank_re = re(r"^\s*$");
let label_re = re(r"^([a-z](-[a-z]|[a-z])*):(.*)$");
let name_re = re(r"^[a-z](-[a-z]|[a-z])*$");
let whitespace_re = re(r"\s+");
let mut recipes: BTreeMap<&'a str, Recipe<'a>> = BTreeMap::new();
let mut current_recipe: Option<Recipe> = None;
for (i, line) in text.lines().enumerate() {
if blank_re.is_match(line) {
continue;
} else if shebang_re.is_match(line) {
return Err(error(text, i, ErrorKind::Shebang));
}
if let Some(mut recipe) = current_recipe {
match command_re.captures(line) {
Some(captures) => {
let leading_whitespace = captures.at(1).unwrap();
if recipe.leading_whitespace == "" {
recipe.leading_whitespace = leading_whitespace;
} else if !line.starts_with(recipe.leading_whitespace) {
return Err(error(text, i, ErrorKind::InconsistentLeadingWhitespace{
expected: recipe.leading_whitespace,
found: leading_whitespace,
}));
} }
self.resolve(dependency); let command = captures.at(2).unwrap();
recipe.commands.push(command);
current_recipe = Some(recipe);
continue;
}, },
None => die!("Recipe \"{}\" depends on recipe \"{}\", which doesn't exist.", None => {
recipe.name, dependency_name), recipes.insert(recipe.name, recipe);
current_recipe = None;
},
}
}
if comment_re.is_match(line) {
// ignore
} else if let Some(captures) = label_re.captures(line) {
let name = captures.at(1).unwrap();
if let Some(recipe) = recipes.get(name) {
return Err(error(text, i, ErrorKind::DuplicateRecipe {
first: recipe.line,
name: name,
}));
} }
let rest = captures.at(3).unwrap().trim();
let mut dependencies = HashSet::new();
for part in whitespace_re.split(rest) {
if name_re.is_match(part) {
if dependencies.contains(part) {
return Err(error(text, i, ErrorKind::DuplicateDependency{
name: part,
}));
}
dependencies.insert(part);
} else {
return Err(error(text, i, ErrorKind::UnparsableDependencies));
}
}
current_recipe = Some(Recipe{
line: i,
name: name,
leading_whitespace: "",
commands: vec![],
dependencies: dependencies,
});
} else {
return Err(error(text, i, ErrorKind::Unparsable));
} }
self.resolved.insert(recipe.name);
self.stack.pop();
} }
if let Some(recipe) = current_recipe {
recipes.insert(recipe.name, recipe);
}
let mut resolved = HashSet::new();
let mut seen = HashSet::new();
let mut stack = vec![];
fn resolve<'a>(
text: &'a str,
recipes: &BTreeMap<&str, Recipe<'a>>,
resolved: &mut HashSet<&'a str>,
seen: &mut HashSet<&'a str>,
stack: &mut Vec<&'a str>,
recipe: &Recipe<'a>,
) -> Result<(), Error<'a>> {
stack.push(recipe.name);
seen.insert(recipe.name);
for dependency_name in &recipe.dependencies {
match recipes.get(dependency_name) {
Some(dependency) => if !resolved.contains(dependency.name) {
if seen.contains(dependency.name) {
let first = stack[0];
stack.push(first);
return Err(error(text, recipe.line, ErrorKind::CircularDependency {
circle: stack.iter()
.skip_while(|name| **name != dependency.name)
.cloned().collect()
}));
}
return resolve(text, recipes, resolved, seen, stack, dependency);
},
None => return Err(error(text, recipe.line, ErrorKind::UnknownDependency {
name: recipe.name,
unknown: dependency_name
})),
}
}
resolved.insert(recipe.name);
stack.pop();
Ok(())
}
for (_, ref recipe) in &recipes {
try!(resolve(text, &recipes, &mut resolved, &mut seen, &mut stack, &recipe));
}
Ok(Justfile{_recipes: recipes})
} }
fn main() { fn main() {
@ -100,91 +278,14 @@ fn main() {
} }
} }
let mut contents = String::new(); let text = fs::File::open("justfile")
fs::File::open("justfile")
.unwrap_or_else(|error| die!("Error opening justfile: {}", error)) .unwrap_or_else(|error| die!("Error opening justfile: {}", error))
.read_to_string(&mut contents) .slurp()
.unwrap_or_else(|error| die!("Error reading justfile: {}", error)); .unwrap_or_else(|error| die!("Error reading justfile: {}", error));
let shebang_re = re(r"^\s*#!(.*)$"); let _justfile = parse(&text).unwrap_or_else(|error| die!("{}", error));
let comment_re = re(r"^\s*#[^!].*$");
let command_re = re(r"^(\s+)(.*)$");
let blank_re = re(r"^\s*$");
let label_re = re(r"^([a-z](-[a-z]|[a-z])*):(.*)$");
let name_re = re(r"^[a-z](-[a-z]|[a-z])*$");
let whitespace_re = re(r"\s+");
let mut recipes = BTreeMap::new();
let mut current_recipe: Option<Recipe> = None;
for (i, line) in contents.lines().enumerate() {
if blank_re.is_match(line) {
continue;
} else if shebang_re.is_match(line) {
die!("Unexpected shebang on line {}: {}", i, line);
}
if let Some(mut recipe) = current_recipe {
match command_re.captures(line) {
Some(captures) => {
let leading_whitespace = captures.at(1).unwrap();
if recipe.leading_whitespace == "" {
recipe.leading_whitespace = leading_whitespace;
} else if leading_whitespace != recipe.leading_whitespace {
die!("Command on line {} has inconsistent leading whitespace: {}",
i, line);
}
let command = captures.at(2).unwrap();
recipe.commands.push(command);
current_recipe = Some(recipe);
continue;
},
None => {
recipes.insert(recipe.name, recipe);
current_recipe = None;
},
}
}
if comment_re.is_match(line) {
// ignore
} else if let Some(captures) = label_re.captures(line) {
let name = captures.at(1).unwrap();
let rest = captures.at(3).unwrap().trim();
let mut dependencies = HashSet::new();
for part in whitespace_re.split(rest) {
if name_re.is_match(part) {
if dependencies.contains(part) {
die!("Duplicate dependency \"{}\" on line {}", part, i);
}
dependencies.insert(part);
} else {
die!("Bad label on line {}: {}", i, line);
}
}
if recipes.contains_key(name) {
die!("Duplicate recipe name \"{}\" on line {}.", name, i);
}
current_recipe = Some(Recipe{
_line: i as u64,
name: name,
leading_whitespace: "",
commands: vec![],
dependencies: dependencies,
});
} else {
die!("Error parsing line {} of justfile: {}", i, line);
}
}
if let Some(recipe) = current_recipe {
recipes.insert(recipe.name, recipe);
}
resolve(&recipes);
/*
// let requests: Vec<String> = std::env::args().skip(1).collect(); // let requests: Vec<String> = std::env::args().skip(1).collect();
// for request in requests { // for request in requests {
// println!("{}", request); // println!("{}", request);
@ -196,7 +297,6 @@ fn main() {
// std::env::set_var(format!("ARG{}", i), argument); // std::env::set_var(format!("ARG{}", i), argument);
// } // }
/*
let mut command = std::process::Command::new(make.command()); let mut command = std::process::Command::new(make.command());
command.arg("MAKEFLAGS="); command.arg("MAKEFLAGS=");