Reform Parser (#509)
Just's first parser performed both parsing, i.e the transformation of a token stream according to the language grammar, and a number of consistency checks and analysis passes. This made parsing and analysis quite complex, so this diff introduces a new, much cleaner `Parser`, and moves existing analysis into a dedicated `Analyzer`.
This commit is contained in:
parent
8e084a7042
commit
b2285ce0e0
2
justfile
2
justfile
@ -102,7 +102,7 @@ sloc:
|
||||
! grep --color -En '.{101}' src/*.rs
|
||||
|
||||
replace FROM TO:
|
||||
sd -i '{{FROM}}' '{{TO}}' src/*.rs
|
||||
sd '{{FROM}}' '{{TO}}' src/*.rs
|
||||
|
||||
test-quine:
|
||||
cargo run -- quine
|
||||
|
34
src/alias.rs
34
src/alias.rs
@ -1,15 +1,35 @@
|
||||
use crate::common::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Alias<'a> {
|
||||
pub(crate) name: &'a str,
|
||||
pub(crate) target: &'a str,
|
||||
pub(crate) line_number: usize,
|
||||
pub(crate) private: bool,
|
||||
/// An alias, e.g. `name := target`
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct Alias<'src> {
|
||||
pub(crate) name: Name<'src>,
|
||||
pub(crate) target: Name<'src>,
|
||||
}
|
||||
|
||||
impl Alias<'_> {
|
||||
pub(crate) fn is_private(&self) -> bool {
|
||||
self.name.lexeme().starts_with('_')
|
||||
}
|
||||
|
||||
pub(crate) fn line_number(&self) -> usize {
|
||||
self.name.line
|
||||
}
|
||||
}
|
||||
|
||||
impl<'src> Keyed<'src> for Alias<'src> {
|
||||
fn key(&self) -> &'src str {
|
||||
self.name.lexeme()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Display for Alias<'a> {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "alias {} := {}", self.name, self.target)
|
||||
write!(
|
||||
f,
|
||||
"alias {} := {}",
|
||||
self.name.lexeme(),
|
||||
self.target.lexeme()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -7,20 +7,14 @@ where
|
||||
{
|
||||
aliases: &'b BTreeMap<&'a str, Alias<'a>>,
|
||||
recipes: &'b BTreeMap<&'a str, Recipe<'a>>,
|
||||
alias_tokens: &'b BTreeMap<&'a str, Token<'a>>,
|
||||
}
|
||||
|
||||
impl<'a: 'b, 'b> AliasResolver<'a, 'b> {
|
||||
pub(crate) fn resolve_aliases(
|
||||
aliases: &BTreeMap<&'a str, Alias<'a>>,
|
||||
recipes: &BTreeMap<&'a str, Recipe<'a>>,
|
||||
alias_tokens: &BTreeMap<&'a str, Token<'a>>,
|
||||
) -> CompilationResult<'a, ()> {
|
||||
let resolver = AliasResolver {
|
||||
aliases,
|
||||
recipes,
|
||||
alias_tokens,
|
||||
};
|
||||
let resolver = AliasResolver { aliases, recipes };
|
||||
|
||||
resolver.resolve()?;
|
||||
|
||||
@ -36,20 +30,20 @@ impl<'a: 'b, 'b> AliasResolver<'a, 'b> {
|
||||
}
|
||||
|
||||
fn resolve_alias(&self, alias: &Alias<'a>) -> CompilationResult<'a, ()> {
|
||||
let token = self.alias_tokens.get(&alias.name).unwrap();
|
||||
let token = alias.name.token();
|
||||
// Make sure the alias doesn't conflict with any recipe
|
||||
if let Some(recipe) = self.recipes.get(alias.name) {
|
||||
if let Some(recipe) = self.recipes.get(alias.name.lexeme()) {
|
||||
return Err(token.error(AliasShadowsRecipe {
|
||||
alias: alias.name,
|
||||
recipe_line: recipe.line_number,
|
||||
alias: alias.name.lexeme(),
|
||||
recipe_line: recipe.line_number(),
|
||||
}));
|
||||
}
|
||||
|
||||
// Make sure the target recipe exists
|
||||
if self.recipes.get(alias.target).is_none() {
|
||||
if self.recipes.get(alias.target.lexeme()).is_none() {
|
||||
return Err(token.error(UnknownAliasTarget {
|
||||
alias: alias.name,
|
||||
target: alias.target,
|
||||
alias: alias.name.lexeme(),
|
||||
target: alias.target.lexeme(),
|
||||
}));
|
||||
}
|
||||
|
||||
|
300
src/analyzer.rs
Normal file
300
src/analyzer.rs
Normal file
@ -0,0 +1,300 @@
|
||||
use crate::common::*;
|
||||
|
||||
use CompilationErrorKind::*;
|
||||
|
||||
pub(crate) struct Analyzer<'a> {
|
||||
recipes: Table<'a, Recipe<'a>>,
|
||||
assignments: Table<'a, Assignment<'a>>,
|
||||
aliases: Table<'a, Alias<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Analyzer<'a> {
|
||||
pub(crate) fn analyze(module: Module<'a>) -> CompilationResult<'a, Justfile> {
|
||||
let analyzer = Analyzer::new();
|
||||
|
||||
analyzer.justfile(module)
|
||||
}
|
||||
|
||||
pub(crate) fn new() -> Analyzer<'a> {
|
||||
Analyzer {
|
||||
recipes: empty(),
|
||||
assignments: empty(),
|
||||
aliases: empty(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn justfile(mut self, module: Module<'a>) -> CompilationResult<'a, Justfile<'a>> {
|
||||
for item in module.items {
|
||||
match item {
|
||||
Item::Alias(alias) => {
|
||||
self.analyze_alias(&alias)?;
|
||||
self.aliases.insert(alias);
|
||||
}
|
||||
Item::Assignment(assignment) => {
|
||||
self.analyze_assignment(&assignment)?;
|
||||
self.assignments.insert(assignment);
|
||||
}
|
||||
Item::Recipe(recipe) => {
|
||||
self.analyze_recipe(&recipe)?;
|
||||
self.recipes.insert(recipe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let recipes = self.recipes;
|
||||
let assignments = self.assignments;
|
||||
let aliases = self.aliases;
|
||||
|
||||
AssignmentResolver::resolve_assignments(&assignments)?;
|
||||
|
||||
RecipeResolver::resolve_recipes(&recipes, &assignments)?;
|
||||
|
||||
for recipe in recipes.values() {
|
||||
for parameter in &recipe.parameters {
|
||||
if assignments.contains_key(parameter.name.lexeme()) {
|
||||
return Err(parameter.name.token().error(ParameterShadowsVariable {
|
||||
parameter: parameter.name.lexeme(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
for dependency in &recipe.dependencies {
|
||||
if !recipes[dependency.lexeme()].parameters.is_empty() {
|
||||
return Err(dependency.error(DependencyHasParameters {
|
||||
recipe: recipe.name(),
|
||||
dependency: dependency.lexeme(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AliasResolver::resolve_aliases(&aliases, &recipes)?;
|
||||
|
||||
Ok(Justfile {
|
||||
warnings: module.warnings,
|
||||
recipes,
|
||||
assignments,
|
||||
aliases,
|
||||
})
|
||||
}
|
||||
|
||||
fn analyze_recipe(&self, recipe: &Recipe<'a>) -> CompilationResult<'a, ()> {
|
||||
if let Some(original) = self.recipes.get(recipe.name.lexeme()) {
|
||||
return Err(recipe.name.token().error(DuplicateRecipe {
|
||||
recipe: original.name(),
|
||||
first: original.line_number(),
|
||||
}));
|
||||
}
|
||||
|
||||
let mut parameters = BTreeSet::new();
|
||||
let mut passed_default = false;
|
||||
|
||||
for parameter in &recipe.parameters {
|
||||
if parameters.contains(parameter.name.lexeme()) {
|
||||
return Err(parameter.name.token().error(DuplicateParameter {
|
||||
recipe: recipe.name.lexeme(),
|
||||
parameter: parameter.name.lexeme(),
|
||||
}));
|
||||
}
|
||||
parameters.insert(parameter.name.lexeme());
|
||||
|
||||
if parameter.default.is_some() {
|
||||
passed_default = true;
|
||||
} else if passed_default {
|
||||
return Err(
|
||||
parameter
|
||||
.name
|
||||
.token()
|
||||
.error(RequiredParameterFollowsDefaultParameter {
|
||||
parameter: parameter.name.lexeme(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut dependencies = BTreeSet::new();
|
||||
for dependency in &recipe.dependencies {
|
||||
if dependencies.contains(dependency.lexeme()) {
|
||||
return Err(dependency.token().error(DuplicateDependency {
|
||||
recipe: recipe.name.lexeme(),
|
||||
dependency: dependency.lexeme(),
|
||||
}));
|
||||
}
|
||||
dependencies.insert(dependency.lexeme());
|
||||
}
|
||||
|
||||
let mut continued = false;
|
||||
for line in &recipe.body {
|
||||
if !recipe.shebang && !continued {
|
||||
if let Some(Fragment::Text { token }) = line.fragments.first() {
|
||||
let text = token.lexeme();
|
||||
|
||||
if text.starts_with(' ') || text.starts_with('\t') {
|
||||
return Err(token.error(ExtraLeadingWhitespace));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
continued = line.is_continuation();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn analyze_assignment(&self, assignment: &Assignment<'a>) -> CompilationResult<'a, ()> {
|
||||
if self.assignments.contains_key(assignment.name.lexeme()) {
|
||||
return Err(assignment.name.token().error(DuplicateVariable {
|
||||
variable: assignment.name.lexeme(),
|
||||
}));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn analyze_alias(&self, alias: &Alias<'a>) -> CompilationResult<'a, ()> {
|
||||
let name = alias.name.lexeme();
|
||||
|
||||
if let Some(original) = self.aliases.get(name) {
|
||||
return Err(alias.name.token().error(DuplicateAlias {
|
||||
alias: name,
|
||||
first: original.line_number(),
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
analysis_error! {
|
||||
name: duplicate_alias,
|
||||
input: "alias foo = bar\nalias foo = baz",
|
||||
offset: 22,
|
||||
line: 1,
|
||||
column: 6,
|
||||
width: 3,
|
||||
kind: DuplicateAlias { alias: "foo", first: 0 },
|
||||
}
|
||||
|
||||
analysis_error! {
|
||||
name: unknown_alias_target,
|
||||
input: "alias foo = bar\n",
|
||||
offset: 6,
|
||||
line: 0,
|
||||
column: 6,
|
||||
width: 3,
|
||||
kind: UnknownAliasTarget {alias: "foo", target: "bar"},
|
||||
}
|
||||
|
||||
analysis_error! {
|
||||
name: alias_shadows_recipe_before,
|
||||
input: "bar: \n echo bar\nalias foo = bar\nfoo:\n echo foo",
|
||||
offset: 23,
|
||||
line: 2,
|
||||
column: 6,
|
||||
width: 3,
|
||||
kind: AliasShadowsRecipe {alias: "foo", recipe_line: 3},
|
||||
}
|
||||
|
||||
analysis_error! {
|
||||
name: alias_shadows_recipe_after,
|
||||
input: "foo:\n echo foo\nalias foo = bar\nbar:\n echo bar",
|
||||
offset: 22,
|
||||
line: 2,
|
||||
column: 6,
|
||||
width: 3,
|
||||
kind: AliasShadowsRecipe { alias: "foo", recipe_line: 0 },
|
||||
}
|
||||
|
||||
analysis_error! {
|
||||
name: required_after_default,
|
||||
input: "hello arg='foo' bar:",
|
||||
offset: 16,
|
||||
line: 0,
|
||||
column: 16,
|
||||
width: 3,
|
||||
kind: RequiredParameterFollowsDefaultParameter{parameter: "bar"},
|
||||
}
|
||||
|
||||
analysis_error! {
|
||||
name: duplicate_parameter,
|
||||
input: "a b b:",
|
||||
offset: 4,
|
||||
line: 0,
|
||||
column: 4,
|
||||
width: 1,
|
||||
kind: DuplicateParameter{recipe: "a", parameter: "b"},
|
||||
}
|
||||
|
||||
analysis_error! {
|
||||
name: duplicate_variadic_parameter,
|
||||
input: "a b +b:",
|
||||
offset: 5,
|
||||
line: 0,
|
||||
column: 5,
|
||||
width: 1,
|
||||
kind: DuplicateParameter{recipe: "a", parameter: "b"},
|
||||
}
|
||||
|
||||
analysis_error! {
|
||||
name: parameter_shadows_varible,
|
||||
input: "foo = \"h\"\na foo:",
|
||||
offset: 12,
|
||||
line: 1,
|
||||
column: 2,
|
||||
width: 3,
|
||||
kind: ParameterShadowsVariable{parameter: "foo"},
|
||||
}
|
||||
|
||||
analysis_error! {
|
||||
name: dependency_has_parameters,
|
||||
input: "foo arg:\nb: foo",
|
||||
offset: 12,
|
||||
line: 1,
|
||||
column: 3,
|
||||
width: 3,
|
||||
kind: DependencyHasParameters{recipe: "b", dependency: "foo"},
|
||||
}
|
||||
|
||||
analysis_error! {
|
||||
name: duplicate_dependency,
|
||||
input: "a b c: b c z z",
|
||||
offset: 13,
|
||||
line: 0,
|
||||
column: 13,
|
||||
width: 1,
|
||||
kind: DuplicateDependency{recipe: "a", dependency: "z"},
|
||||
}
|
||||
|
||||
analysis_error! {
|
||||
name: duplicate_recipe,
|
||||
input: "a:\nb:\na:",
|
||||
offset: 6,
|
||||
line: 2,
|
||||
column: 0,
|
||||
width: 1,
|
||||
kind: DuplicateRecipe{recipe: "a", first: 0},
|
||||
}
|
||||
|
||||
analysis_error! {
|
||||
name: duplicate_variable,
|
||||
input: "a = \"0\"\na = \"0\"",
|
||||
offset: 8,
|
||||
line: 1,
|
||||
column: 0,
|
||||
width: 1,
|
||||
kind: DuplicateVariable{variable: "a"},
|
||||
}
|
||||
|
||||
analysis_error! {
|
||||
name: extra_whitespace,
|
||||
input: "a:\n blah\n blarg",
|
||||
offset: 10,
|
||||
line: 2,
|
||||
column: 1,
|
||||
width: 6,
|
||||
kind: ExtraLeadingWhitespace,
|
||||
}
|
||||
}
|
18
src/assignment.rs
Normal file
18
src/assignment.rs
Normal file
@ -0,0 +1,18 @@
|
||||
use crate::common::*;
|
||||
|
||||
/// An assignment, e.g `foo := bar`
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct Assignment<'src> {
|
||||
/// Assignment was prefixed by the `export` keyword
|
||||
pub(crate) export: bool,
|
||||
/// Left-hand side of the assignment
|
||||
pub(crate) name: Name<'src>,
|
||||
/// Right-hand side of the assignment
|
||||
pub(crate) expression: Expression<'src>,
|
||||
}
|
||||
|
||||
impl<'src> Keyed<'src> for Assignment<'src> {
|
||||
fn key(&self) -> &'src str {
|
||||
self.name.lexeme()
|
||||
}
|
||||
}
|
@ -1,31 +1,29 @@
|
||||
use crate::common::*;
|
||||
|
||||
pub(crate) struct AssignmentEvaluator<'a: 'b, 'b> {
|
||||
pub(crate) assignments: &'b BTreeMap<&'a str, Expression<'a>>,
|
||||
pub(crate) assignments: &'b BTreeMap<&'a str, Assignment<'a>>,
|
||||
pub(crate) invocation_directory: &'b Result<PathBuf, String>,
|
||||
pub(crate) dotenv: &'b BTreeMap<String, String>,
|
||||
pub(crate) dry_run: bool,
|
||||
pub(crate) evaluated: BTreeMap<&'a str, String>,
|
||||
pub(crate) exports: &'b BTreeSet<&'a str>,
|
||||
pub(crate) evaluated: BTreeMap<&'a str, (bool, String)>,
|
||||
pub(crate) overrides: &'b BTreeMap<&'b str, &'b str>,
|
||||
pub(crate) quiet: bool,
|
||||
pub(crate) scope: &'b BTreeMap<&'a str, String>,
|
||||
pub(crate) scope: &'b BTreeMap<&'a str, (bool, String)>,
|
||||
pub(crate) shell: &'b str,
|
||||
}
|
||||
|
||||
impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
||||
pub(crate) fn evaluate_assignments(
|
||||
assignments: &BTreeMap<&'a str, Expression<'a>>,
|
||||
assignments: &BTreeMap<&'a str, Assignment<'a>>,
|
||||
invocation_directory: &Result<PathBuf, String>,
|
||||
dotenv: &'b BTreeMap<String, String>,
|
||||
overrides: &BTreeMap<&str, &str>,
|
||||
quiet: bool,
|
||||
shell: &'a str,
|
||||
dry_run: bool,
|
||||
) -> RunResult<'a, BTreeMap<&'a str, String>> {
|
||||
) -> RunResult<'a, BTreeMap<&'a str, (bool, String)>> {
|
||||
let mut evaluator = AssignmentEvaluator {
|
||||
evaluated: empty(),
|
||||
exports: &empty(),
|
||||
scope: &empty(),
|
||||
assignments,
|
||||
invocation_directory,
|
||||
@ -46,13 +44,13 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
||||
pub(crate) fn evaluate_line(
|
||||
&mut self,
|
||||
line: &[Fragment<'a>],
|
||||
arguments: &BTreeMap<&str, Cow<str>>,
|
||||
arguments: &BTreeMap<&'a str, Cow<str>>,
|
||||
) -> RunResult<'a, String> {
|
||||
let mut evaluated = String::new();
|
||||
for fragment in line {
|
||||
match *fragment {
|
||||
Fragment::Text { ref text } => evaluated += text.lexeme(),
|
||||
Fragment::Expression { ref expression } => {
|
||||
match fragment {
|
||||
Fragment::Text { token } => evaluated += token.lexeme(),
|
||||
Fragment::Interpolation { expression } => {
|
||||
evaluated += &self.evaluate_expression(expression, arguments)?;
|
||||
}
|
||||
}
|
||||
@ -65,12 +63,14 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(expression) = self.assignments.get(name) {
|
||||
if let Some(assignment) = self.assignments.get(name) {
|
||||
if let Some(value) = self.overrides.get(name) {
|
||||
self.evaluated.insert(name, value.to_string());
|
||||
self
|
||||
.evaluated
|
||||
.insert(name, (assignment.export, value.to_string()));
|
||||
} else {
|
||||
let value = self.evaluate_expression(expression, &empty())?;
|
||||
self.evaluated.insert(name, value);
|
||||
let value = self.evaluate_expression(&assignment.expression, &empty())?;
|
||||
self.evaluated.insert(name, (assignment.export, value));
|
||||
}
|
||||
} else {
|
||||
return Err(RuntimeError::Internal {
|
||||
@ -84,29 +84,29 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
||||
pub(crate) fn evaluate_expression(
|
||||
&mut self,
|
||||
expression: &Expression<'a>,
|
||||
arguments: &BTreeMap<&str, Cow<str>>,
|
||||
arguments: &BTreeMap<&'a str, Cow<str>>,
|
||||
) -> RunResult<'a, String> {
|
||||
match *expression {
|
||||
match expression {
|
||||
Expression::Variable { name, .. } => {
|
||||
if self.evaluated.contains_key(name) {
|
||||
Ok(self.evaluated[name].clone())
|
||||
} else if self.scope.contains_key(name) {
|
||||
Ok(self.scope[name].clone())
|
||||
} else if self.assignments.contains_key(name) {
|
||||
self.evaluate_assignment(name)?;
|
||||
Ok(self.evaluated[name].clone())
|
||||
} else if arguments.contains_key(name) {
|
||||
Ok(arguments[name].to_string())
|
||||
let variable = name.lexeme();
|
||||
if self.evaluated.contains_key(variable) {
|
||||
Ok(self.evaluated[variable].1.clone())
|
||||
} else if self.scope.contains_key(variable) {
|
||||
Ok(self.scope[variable].1.clone())
|
||||
} else if self.assignments.contains_key(variable) {
|
||||
self.evaluate_assignment(variable)?;
|
||||
Ok(self.evaluated[variable].1.clone())
|
||||
} else if arguments.contains_key(variable) {
|
||||
Ok(arguments[variable].to_string())
|
||||
} else {
|
||||
Err(RuntimeError::Internal {
|
||||
message: format!("attempted to evaluate undefined variable `{}`", name),
|
||||
message: format!("attempted to evaluate undefined variable `{}`", variable),
|
||||
})
|
||||
}
|
||||
}
|
||||
Expression::Call {
|
||||
name,
|
||||
arguments: ref call_arguments,
|
||||
ref token,
|
||||
function,
|
||||
arguments: call_arguments,
|
||||
} => {
|
||||
let call_arguments = call_arguments
|
||||
.iter()
|
||||
@ -116,20 +116,20 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
||||
invocation_directory: &self.invocation_directory,
|
||||
dotenv: self.dotenv,
|
||||
};
|
||||
Function::evaluate(token, name, &context, &call_arguments)
|
||||
Function::evaluate(*function, &context, &call_arguments)
|
||||
}
|
||||
Expression::String { ref cooked_string } => Ok(cooked_string.cooked.to_string()),
|
||||
Expression::Backtick { raw, ref token } => {
|
||||
Expression::StringLiteral { string_literal } => Ok(string_literal.cooked.to_string()),
|
||||
Expression::Backtick { contents, token } => {
|
||||
if self.dry_run {
|
||||
Ok(format!("`{}`", raw))
|
||||
Ok(format!("`{}`", contents))
|
||||
} else {
|
||||
Ok(self.run_backtick(self.dotenv, raw, token)?)
|
||||
Ok(self.run_backtick(self.dotenv, contents, token)?)
|
||||
}
|
||||
}
|
||||
Expression::Concatination { ref lhs, ref rhs } => {
|
||||
Expression::Concatination { lhs, rhs } => {
|
||||
Ok(self.evaluate_expression(lhs, arguments)? + &self.evaluate_expression(rhs, arguments)?)
|
||||
}
|
||||
Expression::Group { ref expression } => self.evaluate_expression(&expression, arguments),
|
||||
Expression::Group { contents } => self.evaluate_expression(contents, arguments),
|
||||
}
|
||||
}
|
||||
|
||||
@ -143,7 +143,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
||||
|
||||
cmd.arg("-cu").arg(raw);
|
||||
|
||||
cmd.export_environment_variables(self.scope, dotenv, self.exports)?;
|
||||
cmd.export_environment_variables(self.scope, dotenv)?;
|
||||
|
||||
cmd.stdin(process::Stdio::inherit());
|
||||
|
||||
@ -163,13 +163,13 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::testing::parse;
|
||||
use crate::testing::compile;
|
||||
|
||||
#[test]
|
||||
fn backtick_code() {
|
||||
match parse("a:\n echo {{`f() { return 100; }; f`}}")
|
||||
match compile("a:\n echo {{`f() { return 100; }; f`}}")
|
||||
.run(&["a"], &Default::default())
|
||||
.unwrap_err()
|
||||
{
|
||||
@ -198,7 +198,7 @@ recipe:
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match parse(text).run(&["recipe"], &config).unwrap_err() {
|
||||
match compile(text).run(&["recipe"], &config).unwrap_err() {
|
||||
RuntimeError::Backtick {
|
||||
token,
|
||||
output_error: OutputError::Code(_),
|
||||
|
@ -3,8 +3,7 @@ use crate::common::*;
|
||||
use CompilationErrorKind::*;
|
||||
|
||||
pub(crate) struct AssignmentResolver<'a: 'b, 'b> {
|
||||
assignments: &'b BTreeMap<&'a str, Expression<'a>>,
|
||||
assignment_tokens: &'b BTreeMap<&'a str, Token<'a>>,
|
||||
assignments: &'b BTreeMap<&'a str, Assignment<'a>>,
|
||||
stack: Vec<&'a str>,
|
||||
seen: BTreeSet<&'a str>,
|
||||
evaluated: BTreeSet<&'a str>,
|
||||
@ -12,15 +11,13 @@ pub(crate) struct AssignmentResolver<'a: 'b, 'b> {
|
||||
|
||||
impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
|
||||
pub(crate) fn resolve_assignments(
|
||||
assignments: &BTreeMap<&'a str, Expression<'a>>,
|
||||
assignment_tokens: &BTreeMap<&'a str, Token<'a>>,
|
||||
assignments: &BTreeMap<&'a str, Assignment<'a>>,
|
||||
) -> CompilationResult<'a, ()> {
|
||||
let mut resolver = AssignmentResolver {
|
||||
stack: empty(),
|
||||
seen: empty(),
|
||||
evaluated: empty(),
|
||||
assignments,
|
||||
assignment_tokens,
|
||||
};
|
||||
|
||||
for name in assignments.keys() {
|
||||
@ -38,13 +35,13 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
|
||||
self.seen.insert(name);
|
||||
self.stack.push(name);
|
||||
|
||||
if let Some(expression) = self.assignments.get(name) {
|
||||
self.resolve_expression(expression)?;
|
||||
if let Some(assignment) = self.assignments.get(name) {
|
||||
self.resolve_expression(&assignment.expression)?;
|
||||
self.evaluated.insert(name);
|
||||
} else {
|
||||
let message = format!("attempted to resolve unknown assignment `{}`", name);
|
||||
return Err(CompilationError {
|
||||
text: "",
|
||||
src: "",
|
||||
offset: 0,
|
||||
line: 0,
|
||||
column: 0,
|
||||
@ -57,43 +54,43 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
|
||||
|
||||
fn resolve_expression(&mut self, expression: &Expression<'a>) -> CompilationResult<'a, ()> {
|
||||
match expression {
|
||||
Expression::Variable { name, ref token } => {
|
||||
if self.evaluated.contains(name) {
|
||||
Expression::Variable { name } => {
|
||||
let variable = name.lexeme();
|
||||
if self.evaluated.contains(variable) {
|
||||
return Ok(());
|
||||
} else if self.seen.contains(name) {
|
||||
let token = &self.assignment_tokens[name];
|
||||
self.stack.push(name);
|
||||
} else if self.seen.contains(variable) {
|
||||
let token = self.assignments[variable].name.token();
|
||||
self.stack.push(variable);
|
||||
return Err(token.error(CircularVariableDependency {
|
||||
variable: name,
|
||||
variable: variable,
|
||||
circle: self.stack.clone(),
|
||||
}));
|
||||
} else if self.assignments.contains_key(name) {
|
||||
self.resolve_assignment(name)?;
|
||||
} else if self.assignments.contains_key(variable) {
|
||||
self.resolve_assignment(variable)?;
|
||||
} else {
|
||||
return Err(token.error(UndefinedVariable { variable: name }));
|
||||
return Err(name.token().error(UndefinedVariable { variable }));
|
||||
}
|
||||
}
|
||||
Expression::Call {
|
||||
ref token,
|
||||
ref arguments,
|
||||
..
|
||||
} => Function::resolve(token, arguments.len())?,
|
||||
Expression::Concatination { ref lhs, ref rhs } => {
|
||||
function,
|
||||
arguments,
|
||||
} => Function::resolve(&function.token(), arguments.len())?,
|
||||
Expression::Concatination { lhs, rhs } => {
|
||||
self.resolve_expression(lhs)?;
|
||||
self.resolve_expression(rhs)?;
|
||||
}
|
||||
Expression::String { .. } | Expression::Backtick { .. } => {}
|
||||
Expression::Group { expression } => self.resolve_expression(expression)?,
|
||||
Expression::StringLiteral { .. } | Expression::Backtick { .. } => {}
|
||||
Expression::Group { contents } => self.resolve_expression(contents)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
error_test! {
|
||||
analysis_error! {
|
||||
name: circular_variable_dependency,
|
||||
input: "a = b\nb = a",
|
||||
offset: 0,
|
||||
@ -103,7 +100,7 @@ mod test {
|
||||
kind: CircularVariableDependency{variable: "a", circle: vec!["a", "b", "a"]},
|
||||
}
|
||||
|
||||
error_test! {
|
||||
analysis_error! {
|
||||
name: self_variable_dependency,
|
||||
input: "a = a",
|
||||
offset: 0,
|
||||
@ -113,7 +110,7 @@ mod test {
|
||||
kind: CircularVariableDependency{variable: "a", circle: vec!["a", "a"]},
|
||||
}
|
||||
|
||||
error_test! {
|
||||
analysis_error! {
|
||||
name: unknown_expression_variable,
|
||||
input: "x = yy",
|
||||
offset: 4,
|
||||
@ -123,7 +120,7 @@ mod test {
|
||||
kind: UndefinedVariable{variable: "yy"},
|
||||
}
|
||||
|
||||
error_test! {
|
||||
analysis_error! {
|
||||
name: unknown_function,
|
||||
input: "a = foo()",
|
||||
offset: 4,
|
||||
@ -132,5 +129,4 @@ mod test {
|
||||
width: 3,
|
||||
kind: UnknownFunction{function: "foo"},
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,31 +3,27 @@ use crate::common::*;
|
||||
pub(crate) trait CommandExt {
|
||||
fn export_environment_variables<'a>(
|
||||
&mut self,
|
||||
scope: &BTreeMap<&'a str, String>,
|
||||
scope: &BTreeMap<&'a str, (bool, String)>,
|
||||
dotenv: &BTreeMap<String, String>,
|
||||
exports: &BTreeSet<&'a str>,
|
||||
) -> RunResult<'a, ()>;
|
||||
}
|
||||
|
||||
impl CommandExt for Command {
|
||||
fn export_environment_variables<'a>(
|
||||
&mut self,
|
||||
scope: &BTreeMap<&'a str, String>,
|
||||
scope: &BTreeMap<&'a str, (bool, String)>,
|
||||
dotenv: &BTreeMap<String, String>,
|
||||
exports: &BTreeSet<&'a str>,
|
||||
) -> RunResult<'a, ()> {
|
||||
for (name, value) in dotenv {
|
||||
self.env(name, value);
|
||||
}
|
||||
for name in exports {
|
||||
if let Some(value) = scope.get(name) {
|
||||
|
||||
for (name, (export, value)) in scope {
|
||||
if *export {
|
||||
self.env(name, value);
|
||||
} else {
|
||||
return Err(RuntimeError::Internal {
|
||||
message: format!("scope does not contain exported variable `{}`", name),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -7,8 +7,10 @@ pub(crate) use std::{
|
||||
env,
|
||||
ffi::OsStr,
|
||||
fmt::{self, Display, Formatter},
|
||||
fs, io, iter,
|
||||
ops::{Range, RangeInclusive},
|
||||
fs,
|
||||
io::{self, Write},
|
||||
iter::{self, FromIterator},
|
||||
ops::{Deref, Range, RangeInclusive},
|
||||
path::{Path, PathBuf},
|
||||
process::{self, Command},
|
||||
str::{self, Chars},
|
||||
@ -23,7 +25,7 @@ pub(crate) use log::warn;
|
||||
pub(crate) use unicode_width::UnicodeWidthChar;
|
||||
|
||||
// modules
|
||||
pub(crate) use crate::search;
|
||||
pub(crate) use crate::{keyword, search};
|
||||
|
||||
// modules used in tests
|
||||
#[cfg(test)]
|
||||
@ -35,39 +37,35 @@ pub(crate) use crate::{
|
||||
write_message_context::write_message_context,
|
||||
};
|
||||
|
||||
// structs and enums
|
||||
// traits
|
||||
pub(crate) use crate::{
|
||||
alias::Alias, alias_resolver::AliasResolver, assignment_evaluator::AssignmentEvaluator,
|
||||
assignment_resolver::AssignmentResolver, color::Color, compilation_error::CompilationError,
|
||||
compilation_error_kind::CompilationErrorKind, config::Config, config_error::ConfigError,
|
||||
count::Count, enclosure::Enclosure, expression::Expression, fragment::Fragment,
|
||||
function::Function, function_context::FunctionContext, functions::Functions,
|
||||
interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, justfile::Justfile,
|
||||
lexer::Lexer, list::List, output_error::OutputError, parameter::Parameter, parser::Parser,
|
||||
platform::Platform, position::Position, recipe::Recipe, recipe_context::RecipeContext,
|
||||
recipe_resolver::RecipeResolver, runtime_error::RuntimeError, search_error::SearchError,
|
||||
shebang::Shebang, show_whitespace::ShowWhitespace, state::State, string_literal::StringLiteral,
|
||||
subcommand::Subcommand, token::Token, token_kind::TokenKind, use_color::UseColor,
|
||||
variables::Variables, verbosity::Verbosity, warning::Warning,
|
||||
command_ext::CommandExt, compilation_result_ext::CompilationResultExt, keyed::Keyed,
|
||||
ordinal::Ordinal, platform_interface::PlatformInterface, range_ext::RangeExt,
|
||||
};
|
||||
|
||||
// structs and enums
|
||||
pub(crate) use crate::{
|
||||
alias::Alias, alias_resolver::AliasResolver, analyzer::Analyzer, assignment::Assignment,
|
||||
assignment_evaluator::AssignmentEvaluator, assignment_resolver::AssignmentResolver, color::Color,
|
||||
compilation_error::CompilationError, compilation_error_kind::CompilationErrorKind,
|
||||
compiler::Compiler, config::Config, config_error::ConfigError, count::Count,
|
||||
enclosure::Enclosure, expression::Expression, fragment::Fragment, function::Function,
|
||||
function_context::FunctionContext, functions::Functions, interrupt_guard::InterruptGuard,
|
||||
interrupt_handler::InterruptHandler, item::Item, justfile::Justfile, lexer::Lexer, line::Line,
|
||||
list::List, module::Module, name::Name, output_error::OutputError, parameter::Parameter,
|
||||
parser::Parser, platform::Platform, position::Position, recipe::Recipe,
|
||||
recipe_context::RecipeContext, recipe_resolver::RecipeResolver, runtime_error::RuntimeError,
|
||||
search_error::SearchError, shebang::Shebang, show_whitespace::ShowWhitespace, state::State,
|
||||
string_literal::StringLiteral, subcommand::Subcommand, table::Table, token::Token,
|
||||
token_kind::TokenKind, use_color::UseColor, variables::Variables, verbosity::Verbosity,
|
||||
warning::Warning,
|
||||
};
|
||||
|
||||
// structs and enums used in tests
|
||||
#[cfg(test)]
|
||||
pub(crate) use crate::{node::Node, tree::Tree};
|
||||
|
||||
// type aliases
|
||||
pub(crate) type CompilationResult<'a, T> = Result<T, CompilationError<'a>>;
|
||||
|
||||
pub(crate) type RunResult<'a, T> = Result<T, RuntimeError<'a>>;
|
||||
|
||||
pub(crate) type ConfigResult<T> = Result<T, ConfigError>;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use std::io::prelude::*;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use crate::command_ext::CommandExt;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use crate::range_ext::RangeExt;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use crate::ordinal::Ordinal;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use crate::platform_interface::PlatformInterface;
|
||||
pub(crate) type RunResult<'a, T> = Result<T, RuntimeError<'a>>;
|
||||
|
@ -2,7 +2,7 @@ use crate::common::*;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct CompilationError<'a> {
|
||||
pub(crate) text: &'a str,
|
||||
pub(crate) src: &'a str,
|
||||
pub(crate) offset: usize,
|
||||
pub(crate) line: usize,
|
||||
pub(crate) column: usize,
|
||||
@ -213,7 +213,7 @@ impl<'a> Display for CompilationError<'a> {
|
||||
write_message_context(
|
||||
f,
|
||||
Color::fmt(f).error(),
|
||||
self.text,
|
||||
self.src,
|
||||
self.offset,
|
||||
self.line,
|
||||
self.column,
|
||||
|
23
src/compilation_result_ext.rs
Normal file
23
src/compilation_result_ext.rs
Normal file
@ -0,0 +1,23 @@
|
||||
use crate::common::*;
|
||||
|
||||
pub(crate) trait CompilationResultExt {
|
||||
fn expected(self, kinds: &[TokenKind]) -> Self;
|
||||
}
|
||||
|
||||
impl<'src, T> CompilationResultExt for CompilationResult<'src, T> {
|
||||
fn expected(mut self, kinds: &[TokenKind]) -> Self {
|
||||
if let Err(CompilationError {
|
||||
kind: CompilationErrorKind::UnexpectedToken {
|
||||
ref mut expected, ..
|
||||
},
|
||||
..
|
||||
}) = &mut self
|
||||
{
|
||||
expected.extend_from_slice(kinds);
|
||||
expected.sort();
|
||||
expected.dedup();
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
13
src/compiler.rs
Normal file
13
src/compiler.rs
Normal file
@ -0,0 +1,13 @@
|
||||
use crate::common::*;
|
||||
|
||||
pub(crate) struct Compiler;
|
||||
|
||||
impl Compiler {
|
||||
pub(crate) fn compile(text: &str) -> CompilationResult<Justfile> {
|
||||
let tokens = Lexer::lex(text)?;
|
||||
|
||||
let ast = Parser::parse(&tokens)?;
|
||||
|
||||
Analyzer::analyze(ast)
|
||||
}
|
||||
}
|
@ -1,55 +1,60 @@
|
||||
use crate::common::*;
|
||||
|
||||
/// An expression. Note that the Just language grammar has both an
|
||||
/// `expression` production of additions (`a + b`) and values, and a
|
||||
/// `value` production of all other value types (for example strings,
|
||||
/// function calls, and parenthetical groups).
|
||||
///
|
||||
/// The parser parses both values and expressions into `Expression`s.
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub(crate) enum Expression<'a> {
|
||||
pub(crate) enum Expression<'src> {
|
||||
/// `contents`
|
||||
Backtick {
|
||||
raw: &'a str,
|
||||
token: Token<'a>,
|
||||
contents: &'src str,
|
||||
token: Token<'src>,
|
||||
},
|
||||
/// `name(arguments)`
|
||||
Call {
|
||||
name: &'a str,
|
||||
token: Token<'a>,
|
||||
arguments: Vec<Expression<'a>>,
|
||||
function: Name<'src>,
|
||||
arguments: Vec<Expression<'src>>,
|
||||
},
|
||||
/// `lhs + rhs`
|
||||
Concatination {
|
||||
lhs: Box<Expression<'a>>,
|
||||
rhs: Box<Expression<'a>>,
|
||||
lhs: Box<Expression<'src>>,
|
||||
rhs: Box<Expression<'src>>,
|
||||
},
|
||||
String {
|
||||
cooked_string: StringLiteral<'a>,
|
||||
},
|
||||
Variable {
|
||||
name: &'a str,
|
||||
token: Token<'a>,
|
||||
},
|
||||
Group {
|
||||
expression: Box<Expression<'a>>,
|
||||
/// `(contents)`
|
||||
Group { contents: Box<Expression<'src>> },
|
||||
/// `"string_literal"` or `'string_literal'`
|
||||
StringLiteral {
|
||||
string_literal: StringLiteral<'src>,
|
||||
},
|
||||
/// `variable`
|
||||
Variable { name: Name<'src> },
|
||||
}
|
||||
|
||||
impl<'a> Expression<'a> {
|
||||
pub(crate) fn variables(&'a self) -> Variables<'a> {
|
||||
impl<'src> Expression<'src> {
|
||||
pub(crate) fn variables<'expression>(&'expression self) -> Variables<'expression, 'src> {
|
||||
Variables::new(self)
|
||||
}
|
||||
|
||||
pub(crate) fn functions(&'a self) -> Functions<'a> {
|
||||
pub(crate) fn functions<'expression>(&'expression self) -> Functions<'expression, 'src> {
|
||||
Functions::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Display for Expression<'a> {
|
||||
impl<'src> Display for Expression<'src> {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
match *self {
|
||||
Expression::Backtick { raw, .. } => write!(f, "`{}`", raw)?,
|
||||
Expression::Concatination { ref lhs, ref rhs } => write!(f, "{} + {}", lhs, rhs)?,
|
||||
Expression::String { ref cooked_string } => write!(f, "{}", cooked_string)?,
|
||||
Expression::Variable { name, .. } => write!(f, "{}", name)?,
|
||||
match self {
|
||||
Expression::Backtick { contents, .. } => write!(f, "`{}`", contents)?,
|
||||
Expression::Concatination { lhs, rhs } => write!(f, "{} + {}", lhs, rhs)?,
|
||||
Expression::StringLiteral { string_literal } => write!(f, "{}", string_literal)?,
|
||||
Expression::Variable { name } => write!(f, "{}", name.lexeme())?,
|
||||
Expression::Call {
|
||||
name,
|
||||
ref arguments,
|
||||
..
|
||||
function,
|
||||
arguments,
|
||||
} => {
|
||||
write!(f, "{}(", name)?;
|
||||
write!(f, "{}(", function.lexeme())?;
|
||||
for (i, argument) in arguments.iter().enumerate() {
|
||||
if i > 0 {
|
||||
write!(f, ", {}", argument)?;
|
||||
@ -59,7 +64,7 @@ impl<'a> Display for Expression<'a> {
|
||||
}
|
||||
write!(f, ")")?;
|
||||
}
|
||||
Expression::Group { ref expression } => write!(f, "({})", expression)?,
|
||||
Expression::Group { contents } => write!(f, "({})", contents)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,16 +1,10 @@
|
||||
use crate::common::*;
|
||||
|
||||
/// A line fragment consisting either of…
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub(crate) enum Fragment<'a> {
|
||||
Text { text: Token<'a> },
|
||||
Expression { expression: Expression<'a> },
|
||||
}
|
||||
|
||||
impl<'a> Fragment<'a> {
|
||||
pub(crate) fn continuation(&self) -> bool {
|
||||
match *self {
|
||||
Fragment::Text { ref text } => text.lexeme().ends_with('\\'),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
pub(crate) enum Fragment<'src> {
|
||||
/// …raw text…
|
||||
Text { token: Token<'src> },
|
||||
/// …an interpolation containing `expression`.
|
||||
Interpolation { expression: Expression<'src> },
|
||||
}
|
||||
|
@ -56,26 +56,26 @@ impl Function {
|
||||
}
|
||||
|
||||
pub(crate) fn evaluate<'a>(
|
||||
token: &Token<'a>,
|
||||
name: &'a str,
|
||||
function_name: Name<'a>,
|
||||
context: &FunctionContext,
|
||||
arguments: &[String],
|
||||
) -> RunResult<'a, String> {
|
||||
let name = function_name.lexeme();
|
||||
if let Some(function) = FUNCTIONS.get(name) {
|
||||
use self::Function::*;
|
||||
let argc = arguments.len();
|
||||
match (function, argc) {
|
||||
(&Nullary(f), 0) => f(context).map_err(|message| RuntimeError::FunctionCall {
|
||||
token: token.clone(),
|
||||
function: function_name,
|
||||
message,
|
||||
}),
|
||||
(&Unary(f), 1) => f(context, &arguments[0]).map_err(|message| RuntimeError::FunctionCall {
|
||||
token: token.clone(),
|
||||
function: function_name,
|
||||
message,
|
||||
}),
|
||||
(&Binary(f), 2) => {
|
||||
f(context, &arguments[0], &arguments[1]).map_err(|message| RuntimeError::FunctionCall {
|
||||
token: token.clone(),
|
||||
function: function_name,
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
@ -1,34 +1,36 @@
|
||||
use crate::common::*;
|
||||
|
||||
pub(crate) struct Functions<'a> {
|
||||
stack: Vec<&'a Expression<'a>>,
|
||||
pub(crate) struct Functions<'expression, 'src> {
|
||||
stack: Vec<&'expression Expression<'src>>,
|
||||
}
|
||||
|
||||
impl<'a> Functions<'a> {
|
||||
pub(crate) fn new(root: &'a Expression<'a>) -> Functions<'a> {
|
||||
impl<'expression, 'src> Functions<'expression, 'src> {
|
||||
pub(crate) fn new(root: &'expression Expression<'src>) -> Functions<'expression, 'src> {
|
||||
Functions { stack: vec![root] }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Functions<'a> {
|
||||
type Item = (&'a Token<'a>, usize);
|
||||
impl<'expression, 'src> Iterator for Functions<'expression, 'src> {
|
||||
type Item = (Token<'src>, usize);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.stack.pop() {
|
||||
None
|
||||
| Some(Expression::String { .. })
|
||||
| Some(Expression::StringLiteral { .. })
|
||||
| Some(Expression::Backtick { .. })
|
||||
| Some(Expression::Variable { .. }) => None,
|
||||
Some(Expression::Call {
|
||||
token, arguments, ..
|
||||
}) => Some((token, arguments.len())),
|
||||
function,
|
||||
arguments,
|
||||
..
|
||||
}) => Some((function.token(), arguments.len())),
|
||||
Some(Expression::Concatination { lhs, rhs }) => {
|
||||
self.stack.push(lhs);
|
||||
self.stack.push(rhs);
|
||||
self.next()
|
||||
}
|
||||
Some(Expression::Group { expression }) => {
|
||||
self.stack.push(expression);
|
||||
Some(Expression::Group { contents }) => {
|
||||
self.stack.push(contents);
|
||||
self.next()
|
||||
}
|
||||
}
|
||||
|
9
src/item.rs
Normal file
9
src/item.rs
Normal file
@ -0,0 +1,9 @@
|
||||
use crate::common::*;
|
||||
|
||||
/// A single top-level item
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Item<'src> {
|
||||
Alias(Alias<'src>),
|
||||
Assignment(Assignment<'src>),
|
||||
Recipe(Recipe<'src>),
|
||||
}
|
448
src/justfile.rs
448
src/justfile.rs
@ -1,11 +1,10 @@
|
||||
use crate::common::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct Justfile<'a> {
|
||||
pub(crate) recipes: BTreeMap<&'a str, Recipe<'a>>,
|
||||
pub(crate) assignments: BTreeMap<&'a str, Expression<'a>>,
|
||||
pub(crate) exports: BTreeSet<&'a str>,
|
||||
pub(crate) aliases: BTreeMap<&'a str, Alias<'a>>,
|
||||
pub(crate) recipes: Table<'a, Recipe<'a>>,
|
||||
pub(crate) assignments: Table<'a, Assignment<'a>>,
|
||||
pub(crate) aliases: Table<'a, Alias<'a>>,
|
||||
pub(crate) warnings: Vec<Warning<'a>>,
|
||||
}
|
||||
|
||||
@ -14,7 +13,7 @@ impl<'a> Justfile<'a> {
|
||||
let mut first: Option<&Recipe> = None;
|
||||
for recipe in self.recipes.values() {
|
||||
if let Some(first_recipe) = first {
|
||||
if recipe.line_number < first_recipe.line_number {
|
||||
if recipe.line_number() < first_recipe.line_number() {
|
||||
first = Some(recipe)
|
||||
}
|
||||
} else {
|
||||
@ -75,7 +74,7 @@ impl<'a> Justfile<'a> {
|
||||
width = cmp::max(name.len(), width);
|
||||
}
|
||||
|
||||
for (name, value) in scope {
|
||||
for (name, (_export, value)) in scope {
|
||||
println!("{0:1$} := \"{2}\"", name, width, value);
|
||||
}
|
||||
return Ok(());
|
||||
@ -94,7 +93,7 @@ impl<'a> Justfile<'a> {
|
||||
let argument_count = cmp::min(tail.len(), recipe.max_arguments());
|
||||
if !argument_range.range_contains(&argument_count) {
|
||||
return Err(RuntimeError::ArgumentCountMismatch {
|
||||
recipe: recipe.name,
|
||||
recipe: recipe.name(),
|
||||
parameters: recipe.parameters.iter().collect(),
|
||||
found: tail.len(),
|
||||
min: recipe.min_arguments(),
|
||||
@ -140,7 +139,7 @@ impl<'a> Justfile<'a> {
|
||||
if let Some(recipe) = self.recipes.get(name) {
|
||||
Some(recipe)
|
||||
} else if let Some(alias) = self.aliases.get(name) {
|
||||
self.recipes.get(alias.target)
|
||||
self.recipes.get(alias.target.lexeme())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -155,12 +154,13 @@ impl<'a> Justfile<'a> {
|
||||
ran: &mut BTreeSet<&'a str>,
|
||||
) -> RunResult<()> {
|
||||
for dependency_name in &recipe.dependencies {
|
||||
if !ran.contains(dependency_name) {
|
||||
self.run_recipe(context, &self.recipes[dependency_name], &[], dotenv, ran)?;
|
||||
let lexeme = dependency_name.lexeme();
|
||||
if !ran.contains(lexeme) {
|
||||
self.run_recipe(context, &self.recipes[lexeme], &[], dotenv, ran)?;
|
||||
}
|
||||
}
|
||||
recipe.run(context, arguments, dotenv, &self.exports)?;
|
||||
ran.insert(recipe.name);
|
||||
recipe.run(context, arguments, dotenv)?;
|
||||
ran.insert(recipe.name());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -168,11 +168,11 @@ impl<'a> Justfile<'a> {
|
||||
impl<'a> Display for Justfile<'a> {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
let mut items = self.recipes.len() + self.assignments.len() + self.aliases.len();
|
||||
for (name, expression) in &self.assignments {
|
||||
if self.exports.contains(name) {
|
||||
for (name, assignment) in &self.assignments {
|
||||
if assignment.export {
|
||||
write!(f, "export ")?;
|
||||
}
|
||||
write!(f, "{} := {}", name, expression)?;
|
||||
write!(f, "{} := {}", name, assignment.expression)?;
|
||||
items -= 1;
|
||||
if items != 0 {
|
||||
write!(f, "\n\n")?;
|
||||
@ -197,15 +197,15 @@ impl<'a> Display for Justfile<'a> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::runtime_error::RuntimeError::*;
|
||||
use crate::testing::parse;
|
||||
use crate::testing::compile;
|
||||
|
||||
#[test]
|
||||
fn unknown_recipes() {
|
||||
match parse("a:\nb:\nc:")
|
||||
match compile("a:\nb:\nc:")
|
||||
.run(&["a", "x", "y", "z"], &Default::default())
|
||||
.unwrap_err()
|
||||
{
|
||||
@ -238,7 +238,7 @@ a:
|
||||
x
|
||||
";
|
||||
|
||||
match parse(text).run(&["a"], &Default::default()).unwrap_err() {
|
||||
match compile(text).run(&["a"], &Default::default()).unwrap_err() {
|
||||
Code {
|
||||
recipe,
|
||||
line_number,
|
||||
@ -254,7 +254,7 @@ a:
|
||||
|
||||
#[test]
|
||||
fn code_error() {
|
||||
match parse("fail:\n @exit 100")
|
||||
match compile("fail:\n @exit 100")
|
||||
.run(&["fail"], &Default::default())
|
||||
.unwrap_err()
|
||||
{
|
||||
@ -277,7 +277,7 @@ a:
|
||||
a return code:
|
||||
@x() { {{return}} {{code + "0"}}; }; x"#;
|
||||
|
||||
match parse(text)
|
||||
match compile(text)
|
||||
.run(&["a", "return", "15"], &Default::default())
|
||||
.unwrap_err()
|
||||
{
|
||||
@ -296,7 +296,7 @@ a return code:
|
||||
|
||||
#[test]
|
||||
fn missing_some_arguments() {
|
||||
match parse("a b c d:")
|
||||
match compile("a b c d:")
|
||||
.run(&["a", "b", "c"], &Default::default())
|
||||
.unwrap_err()
|
||||
{
|
||||
@ -307,7 +307,10 @@ a return code:
|
||||
min,
|
||||
max,
|
||||
} => {
|
||||
let param_names = parameters.iter().map(|p| p.name).collect::<Vec<&str>>();
|
||||
let param_names = parameters
|
||||
.iter()
|
||||
.map(|p| p.name.lexeme())
|
||||
.collect::<Vec<&str>>();
|
||||
assert_eq!(recipe, "a");
|
||||
assert_eq!(param_names, ["b", "c", "d"]);
|
||||
assert_eq!(found, 2);
|
||||
@ -320,7 +323,7 @@ a return code:
|
||||
|
||||
#[test]
|
||||
fn missing_some_arguments_variadic() {
|
||||
match parse("a b c +d:")
|
||||
match compile("a b c +d:")
|
||||
.run(&["a", "B", "C"], &Default::default())
|
||||
.unwrap_err()
|
||||
{
|
||||
@ -331,7 +334,10 @@ a return code:
|
||||
min,
|
||||
max,
|
||||
} => {
|
||||
let param_names = parameters.iter().map(|p| p.name).collect::<Vec<&str>>();
|
||||
let param_names = parameters
|
||||
.iter()
|
||||
.map(|p| p.name.lexeme())
|
||||
.collect::<Vec<&str>>();
|
||||
assert_eq!(recipe, "a");
|
||||
assert_eq!(param_names, ["b", "c", "d"]);
|
||||
assert_eq!(found, 2);
|
||||
@ -344,7 +350,7 @@ a return code:
|
||||
|
||||
#[test]
|
||||
fn missing_all_arguments() {
|
||||
match parse("a b c d:\n echo {{b}}{{c}}{{d}}")
|
||||
match compile("a b c d:\n echo {{b}}{{c}}{{d}}")
|
||||
.run(&["a"], &Default::default())
|
||||
.unwrap_err()
|
||||
{
|
||||
@ -355,7 +361,10 @@ a return code:
|
||||
min,
|
||||
max,
|
||||
} => {
|
||||
let param_names = parameters.iter().map(|p| p.name).collect::<Vec<&str>>();
|
||||
let param_names = parameters
|
||||
.iter()
|
||||
.map(|p| p.name.lexeme())
|
||||
.collect::<Vec<&str>>();
|
||||
assert_eq!(recipe, "a");
|
||||
assert_eq!(param_names, ["b", "c", "d"]);
|
||||
assert_eq!(found, 0);
|
||||
@ -368,7 +377,7 @@ a return code:
|
||||
|
||||
#[test]
|
||||
fn missing_some_defaults() {
|
||||
match parse("a b c d='hello':")
|
||||
match compile("a b c d='hello':")
|
||||
.run(&["a", "b"], &Default::default())
|
||||
.unwrap_err()
|
||||
{
|
||||
@ -379,7 +388,10 @@ a return code:
|
||||
min,
|
||||
max,
|
||||
} => {
|
||||
let param_names = parameters.iter().map(|p| p.name).collect::<Vec<&str>>();
|
||||
let param_names = parameters
|
||||
.iter()
|
||||
.map(|p| p.name.lexeme())
|
||||
.collect::<Vec<&str>>();
|
||||
assert_eq!(recipe, "a");
|
||||
assert_eq!(param_names, ["b", "c", "d"]);
|
||||
assert_eq!(found, 1);
|
||||
@ -392,7 +404,7 @@ a return code:
|
||||
|
||||
#[test]
|
||||
fn missing_all_defaults() {
|
||||
match parse("a b c='r' d='h':")
|
||||
match compile("a b c='r' d='h':")
|
||||
.run(&["a"], &Default::default())
|
||||
.unwrap_err()
|
||||
{
|
||||
@ -403,7 +415,10 @@ a return code:
|
||||
min,
|
||||
max,
|
||||
} => {
|
||||
let param_names = parameters.iter().map(|p| p.name).collect::<Vec<&str>>();
|
||||
let param_names = parameters
|
||||
.iter()
|
||||
.map(|p| p.name.lexeme())
|
||||
.collect::<Vec<&str>>();
|
||||
assert_eq!(recipe, "a");
|
||||
assert_eq!(param_names, ["b", "c", "d"]);
|
||||
assert_eq!(found, 0);
|
||||
@ -419,7 +434,7 @@ a return code:
|
||||
let mut config: Config = Default::default();
|
||||
config.overrides.insert("foo", "bar");
|
||||
config.overrides.insert("baz", "bob");
|
||||
match parse("a:\n echo {{`f() { return 100; }; f`}}")
|
||||
match compile("a:\n echo {{`f() { return 100; }; f`}}")
|
||||
.run(&["a"], &config)
|
||||
.unwrap_err()
|
||||
{
|
||||
@ -447,7 +462,7 @@ wut:
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match parse(text).run(&["wut"], &config).unwrap_err() {
|
||||
match compile(text).run(&["wut"], &config).unwrap_err() {
|
||||
Code {
|
||||
code: _,
|
||||
line_number,
|
||||
@ -459,4 +474,369 @@ wut:
|
||||
other => panic!("expected a recipe code errror, but got: {}", other),
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! test {
|
||||
($name:ident, $input:expr, $expected:expr $(,)*) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
test($input, $expected);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn test(input: &str, expected: &str) {
|
||||
let justfile = compile(input);
|
||||
let actual = format!("{:#}", justfile);
|
||||
assert_eq!(actual, expected);
|
||||
println!("Re-parsing...");
|
||||
let reparsed = compile(&actual);
|
||||
let redumped = format!("{:#}", reparsed);
|
||||
assert_eq!(redumped, actual);
|
||||
}
|
||||
|
||||
test! {
|
||||
parse_empty,
|
||||
"
|
||||
|
||||
# hello
|
||||
|
||||
|
||||
",
|
||||
"",
|
||||
}
|
||||
|
||||
test! {
|
||||
parse_string_default,
|
||||
r#"
|
||||
|
||||
foo a="b\t":
|
||||
|
||||
|
||||
"#,
|
||||
r#"foo a="b\t":"#,
|
||||
}
|
||||
|
||||
test! {
|
||||
parse_multiple,
|
||||
r#"
|
||||
a:
|
||||
b:
|
||||
"#,
|
||||
r#"a:
|
||||
|
||||
b:"#,
|
||||
}
|
||||
|
||||
test! {
|
||||
parse_variadic,
|
||||
r#"
|
||||
|
||||
foo +a:
|
||||
|
||||
|
||||
"#,
|
||||
r#"foo +a:"#,
|
||||
}
|
||||
|
||||
test! {
|
||||
parse_variadic_string_default,
|
||||
r#"
|
||||
|
||||
foo +a="Hello":
|
||||
|
||||
|
||||
"#,
|
||||
r#"foo +a="Hello":"#,
|
||||
}
|
||||
|
||||
test! {
|
||||
parse_raw_string_default,
|
||||
r#"
|
||||
|
||||
foo a='b\t':
|
||||
|
||||
|
||||
"#,
|
||||
r#"foo a='b\t':"#,
|
||||
}
|
||||
|
||||
test! {
|
||||
parse_export,
|
||||
r#"
|
||||
export a := "hello"
|
||||
|
||||
"#,
|
||||
r#"export a := "hello""#,
|
||||
}
|
||||
|
||||
test! {
|
||||
parse_alias_after_target,
|
||||
r#"
|
||||
foo:
|
||||
echo a
|
||||
alias f := foo
|
||||
"#,
|
||||
r#"alias f := foo
|
||||
|
||||
foo:
|
||||
echo a"#
|
||||
}
|
||||
|
||||
test! {
|
||||
parse_alias_before_target,
|
||||
r#"
|
||||
alias f := foo
|
||||
foo:
|
||||
echo a
|
||||
"#,
|
||||
r#"alias f := foo
|
||||
|
||||
foo:
|
||||
echo a"#
|
||||
}
|
||||
|
||||
test! {
|
||||
parse_alias_with_comment,
|
||||
r#"
|
||||
alias f := foo #comment
|
||||
foo:
|
||||
echo a
|
||||
"#,
|
||||
r#"alias f := foo
|
||||
|
||||
foo:
|
||||
echo a"#
|
||||
}
|
||||
|
||||
test! {
|
||||
parse_complex,
|
||||
"
|
||||
x:
|
||||
y:
|
||||
z:
|
||||
foo := \"xx\"
|
||||
bar := foo
|
||||
goodbye := \"y\"
|
||||
hello a b c : x y z #hello
|
||||
#! blah
|
||||
#blarg
|
||||
{{ foo + bar}}abc{{ goodbye\t + \"x\" }}xyz
|
||||
1
|
||||
2
|
||||
3
|
||||
",
|
||||
"bar := foo
|
||||
|
||||
foo := \"xx\"
|
||||
|
||||
goodbye := \"y\"
|
||||
|
||||
hello a b c: x y z
|
||||
#! blah
|
||||
#blarg
|
||||
{{foo + bar}}abc{{goodbye + \"x\"}}xyz
|
||||
1
|
||||
2
|
||||
3
|
||||
|
||||
x:
|
||||
|
||||
y:
|
||||
|
||||
z:"
|
||||
}
|
||||
|
||||
test! {
|
||||
parse_shebang,
|
||||
"
|
||||
practicum := 'hello'
|
||||
install:
|
||||
\t#!/bin/sh
|
||||
\tif [[ -f {{practicum}} ]]; then
|
||||
\t\treturn
|
||||
\tfi
|
||||
",
|
||||
"practicum := 'hello'
|
||||
|
||||
install:
|
||||
#!/bin/sh
|
||||
if [[ -f {{practicum}} ]]; then
|
||||
\treturn
|
||||
fi",
|
||||
}
|
||||
|
||||
test! {
|
||||
parse_simple_shebang,
|
||||
"a:\n #!\n print(1)",
|
||||
"a:\n #!\n print(1)",
|
||||
}
|
||||
|
||||
test! {
|
||||
parse_assignments,
|
||||
r#"a := "0"
|
||||
c := a + b + a + b
|
||||
b := "1"
|
||||
"#,
|
||||
r#"a := "0"
|
||||
|
||||
b := "1"
|
||||
|
||||
c := a + b + a + b"#,
|
||||
}
|
||||
|
||||
test! {
|
||||
parse_assignment_backticks,
|
||||
"a := `echo hello`
|
||||
c := a + b + a + b
|
||||
b := `echo goodbye`",
|
||||
"a := `echo hello`
|
||||
|
||||
b := `echo goodbye`
|
||||
|
||||
c := a + b + a + b",
|
||||
}
|
||||
|
||||
test! {
|
||||
parse_interpolation_backticks,
|
||||
r#"a:
|
||||
echo {{ `echo hello` + "blarg" }} {{ `echo bob` }}"#,
|
||||
r#"a:
|
||||
echo {{`echo hello` + "blarg"}} {{`echo bob`}}"#,
|
||||
}
|
||||
|
||||
test! {
|
||||
eof_test,
|
||||
"x:\ny:\nz:\na b c: x y z",
|
||||
"a b c: x y z\n\nx:\n\ny:\n\nz:",
|
||||
}
|
||||
|
||||
test! {
|
||||
string_quote_escape,
|
||||
r#"a := "hello\"""#,
|
||||
r#"a := "hello\"""#,
|
||||
}
|
||||
|
||||
test! {
|
||||
string_escapes,
|
||||
r#"a := "\n\t\r\"\\""#,
|
||||
r#"a := "\n\t\r\"\\""#,
|
||||
}
|
||||
|
||||
test! {
|
||||
parameters,
|
||||
"a b c:
|
||||
{{b}} {{c}}",
|
||||
"a b c:
|
||||
{{b}} {{c}}",
|
||||
}
|
||||
|
||||
test! {
|
||||
unary_functions,
|
||||
"
|
||||
x := arch()
|
||||
|
||||
a:
|
||||
{{os()}} {{os_family()}}",
|
||||
"x := arch()
|
||||
|
||||
a:
|
||||
{{os()}} {{os_family()}}",
|
||||
}
|
||||
|
||||
test! {
|
||||
env_functions,
|
||||
r#"
|
||||
x := env_var('foo',)
|
||||
|
||||
a:
|
||||
{{env_var_or_default('foo' + 'bar', 'baz',)}} {{env_var(env_var("baz"))}}"#,
|
||||
r#"x := env_var('foo')
|
||||
|
||||
a:
|
||||
{{env_var_or_default('foo' + 'bar', 'baz')}} {{env_var(env_var("baz"))}}"#,
|
||||
}
|
||||
|
||||
test! {
|
||||
parameter_default_string,
|
||||
r#"
|
||||
f x="abc":
|
||||
"#,
|
||||
r#"f x="abc":"#,
|
||||
}
|
||||
|
||||
test! {
|
||||
parameter_default_raw_string,
|
||||
r#"
|
||||
f x='abc':
|
||||
"#,
|
||||
r#"f x='abc':"#,
|
||||
}
|
||||
|
||||
test! {
|
||||
parameter_default_backtick,
|
||||
r#"
|
||||
f x=`echo hello`:
|
||||
"#,
|
||||
r#"f x=`echo hello`:"#,
|
||||
}
|
||||
|
||||
test! {
|
||||
parameter_default_concatination_string,
|
||||
r#"
|
||||
f x=(`echo hello` + "foo"):
|
||||
"#,
|
||||
r#"f x=(`echo hello` + "foo"):"#,
|
||||
}
|
||||
|
||||
test! {
|
||||
parameter_default_concatination_variable,
|
||||
r#"
|
||||
x := "10"
|
||||
f y=(`echo hello` + x) +z="foo":
|
||||
"#,
|
||||
r#"x := "10"
|
||||
|
||||
f y=(`echo hello` + x) +z="foo":"#,
|
||||
}
|
||||
|
||||
test! {
|
||||
parameter_default_multiple,
|
||||
r#"
|
||||
x := "10"
|
||||
f y=(`echo hello` + x) +z=("foo" + "bar"):
|
||||
"#,
|
||||
r#"x := "10"
|
||||
|
||||
f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
|
||||
}
|
||||
|
||||
test! {
|
||||
concatination_in_group,
|
||||
"x := ('0' + '1')",
|
||||
"x := ('0' + '1')",
|
||||
}
|
||||
|
||||
test! {
|
||||
string_in_group,
|
||||
"x := ('0' )",
|
||||
"x := ('0')",
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
test! {
|
||||
escaped_dos_newlines,
|
||||
"@spam:\r
|
||||
\t{ \\\r
|
||||
\t\tfiglet test; \\\r
|
||||
\t\tcargo build --color always 2>&1; \\\r
|
||||
\t\tcargo test --color always -- --color always 2>&1; \\\r
|
||||
\t} | less\r
|
||||
",
|
||||
"@spam:
|
||||
{ \\
|
||||
\tfiglet test; \\
|
||||
\tcargo build --color always 2>&1; \\
|
||||
\tcargo test --color always -- --color always 2>&1; \\
|
||||
} | less",
|
||||
}
|
||||
}
|
||||
|
3
src/keyed.rs
Normal file
3
src/keyed.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub(crate) trait Keyed<'key> {
|
||||
fn key(&self) -> &'key str;
|
||||
}
|
2
src/keyword.rs
Normal file
2
src/keyword.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub(crate) const ALIAS: &str = "alias";
|
||||
pub(crate) const EXPORT: &str = "export";
|
307
src/lexer.rs
307
src/lexer.rs
@ -5,12 +5,15 @@ use TokenKind::*;
|
||||
|
||||
/// Just language lexer
|
||||
///
|
||||
/// `self.next` points to the next character to be lexed, and
|
||||
/// the text between `self.token_start` and `self.token_end` contains
|
||||
/// the current token being lexed.
|
||||
/// The lexer proceeds character-by-character, as opposed to using
|
||||
/// regular expressions to lex tokens or semi-tokens at a time. As a
|
||||
/// result, it is verbose and straightforward. Just used to have a
|
||||
/// regex-based lexer, which was slower and generally godawful. However,
|
||||
/// this should not be taken as a slight against regular expressions,
|
||||
/// the lexer was just idiosyncratically bad.
|
||||
pub(crate) struct Lexer<'a> {
|
||||
/// Source text
|
||||
text: &'a str,
|
||||
src: &'a str,
|
||||
/// Char iterator
|
||||
chars: Chars<'a>,
|
||||
/// Tokens
|
||||
@ -21,19 +24,19 @@ pub(crate) struct Lexer<'a> {
|
||||
token_start: Position,
|
||||
/// Current token end
|
||||
token_end: Position,
|
||||
/// Next character
|
||||
/// Next character to be lexed
|
||||
next: Option<char>,
|
||||
}
|
||||
|
||||
impl<'a> Lexer<'a> {
|
||||
/// Lex `text`
|
||||
pub(crate) fn lex(text: &str) -> CompilationResult<Vec<Token>> {
|
||||
Lexer::new(text).tokenize()
|
||||
pub(crate) fn lex(src: &str) -> CompilationResult<Vec<Token>> {
|
||||
Lexer::new(src).tokenize()
|
||||
}
|
||||
|
||||
/// Create a new Lexer to lex `text`
|
||||
fn new(text: &'a str) -> Lexer<'a> {
|
||||
let mut chars = text.chars();
|
||||
fn new(src: &'a str) -> Lexer<'a> {
|
||||
let mut chars = src.chars();
|
||||
let next = chars.next();
|
||||
|
||||
let start = Position {
|
||||
@ -49,7 +52,7 @@ impl<'a> Lexer<'a> {
|
||||
token_end: start,
|
||||
chars,
|
||||
next,
|
||||
text,
|
||||
src,
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,7 +85,7 @@ impl<'a> Lexer<'a> {
|
||||
|
||||
/// Lexeme of in-progress token
|
||||
fn lexeme(&self) -> &'a str {
|
||||
&self.text[self.token_start.offset..self.token_end.offset]
|
||||
&self.src[self.token_start.offset..self.token_end.offset]
|
||||
}
|
||||
|
||||
/// Length of current token
|
||||
@ -102,7 +105,7 @@ impl<'a> Lexer<'a> {
|
||||
|
||||
/// Un-lexed text
|
||||
fn rest(&self) -> &'a str {
|
||||
&self.text[self.token_end.offset..]
|
||||
&self.src[self.token_end.offset..]
|
||||
}
|
||||
|
||||
/// Check if unlexed text begins with prefix
|
||||
@ -145,7 +148,7 @@ impl<'a> Lexer<'a> {
|
||||
offset: self.token_start.offset,
|
||||
column: self.token_start.column,
|
||||
line: self.token_start.line,
|
||||
text: self.text,
|
||||
src: self.src,
|
||||
length: self.token_end.offset - self.token_start.offset,
|
||||
kind,
|
||||
});
|
||||
@ -158,7 +161,7 @@ impl<'a> Lexer<'a> {
|
||||
fn internal_error(&self, message: impl Into<String>) -> CompilationError<'a> {
|
||||
// Use `self.token_end` as the location of the error
|
||||
CompilationError {
|
||||
text: self.text,
|
||||
src: self.src,
|
||||
offset: self.token_end.offset,
|
||||
line: self.token_end.line,
|
||||
column: self.token_end.column,
|
||||
@ -184,7 +187,7 @@ impl<'a> Lexer<'a> {
|
||||
};
|
||||
|
||||
CompilationError {
|
||||
text: self.text,
|
||||
src: self.src,
|
||||
offset: self.token_start.offset,
|
||||
line: self.token_start.line,
|
||||
column: self.token_start.column,
|
||||
@ -198,7 +201,7 @@ impl<'a> Lexer<'a> {
|
||||
interpolation_start: Position,
|
||||
) -> CompilationError<'a> {
|
||||
CompilationError {
|
||||
text: self.text,
|
||||
src: self.src,
|
||||
offset: interpolation_start.offset,
|
||||
line: interpolation_start.line,
|
||||
column: interpolation_start.column,
|
||||
@ -359,7 +362,7 @@ impl<'a> Lexer<'a> {
|
||||
' ' | '\t' => self.lex_whitespace(),
|
||||
'\'' => self.lex_raw_string(),
|
||||
'"' => self.lex_cooked_string(),
|
||||
'a'..='z' | 'A'..='Z' | '_' => self.lex_name(),
|
||||
'a'..='z' | 'A'..='Z' | '_' => self.lex_identifier(),
|
||||
_ => {
|
||||
self.advance()?;
|
||||
Err(self.error(UnknownStartOfToken))
|
||||
@ -446,7 +449,6 @@ impl<'a> Lexer<'a> {
|
||||
/// Lex token beginning with `start` in indented state
|
||||
fn lex_indented(&mut self) -> CompilationResult<'a, ()> {
|
||||
self.state.push(State::Text);
|
||||
self.token(Line);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -513,8 +515,8 @@ impl<'a> Lexer<'a> {
|
||||
self.lex_double(Eol)
|
||||
}
|
||||
|
||||
/// Lex name: [a-zA-Z_][a-zA-Z0-9_]*
|
||||
fn lex_name(&mut self) -> CompilationResult<'a, ()> {
|
||||
/// Lex identifier: [a-zA-Z_][a-zA-Z0-9_]*
|
||||
fn lex_identifier(&mut self) -> CompilationResult<'a, ()> {
|
||||
while self
|
||||
.next
|
||||
.map(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
||||
@ -523,7 +525,7 @@ impl<'a> Lexer<'a> {
|
||||
self.advance()?;
|
||||
}
|
||||
|
||||
self.token(Name);
|
||||
self.token(Identifier);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -725,19 +727,61 @@ mod tests {
|
||||
Whitespace => " ",
|
||||
|
||||
// Empty lexemes
|
||||
Line | Dedent | Eof => "",
|
||||
Dedent | Eof => "",
|
||||
|
||||
// Variable lexemes
|
||||
Text | StringCooked | StringRaw | Name | Comment | Backtick => {
|
||||
Text | StringCooked | StringRaw | Identifier | Comment | Backtick => {
|
||||
panic!("Token {:?} has no default lexeme", kind)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! error {
|
||||
(
|
||||
name: $name:ident,
|
||||
input: $input:expr,
|
||||
offset: $offset:expr,
|
||||
line: $line:expr,
|
||||
column: $column:expr,
|
||||
width: $width:expr,
|
||||
kind: $kind:expr,
|
||||
) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
error($input, $offset, $line, $column, $width, $kind);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn error(
|
||||
src: &str,
|
||||
offset: usize,
|
||||
line: usize,
|
||||
column: usize,
|
||||
width: usize,
|
||||
kind: CompilationErrorKind,
|
||||
) {
|
||||
let expected = CompilationError {
|
||||
src,
|
||||
offset,
|
||||
line,
|
||||
column,
|
||||
width,
|
||||
kind,
|
||||
};
|
||||
|
||||
match Lexer::lex(src) {
|
||||
Ok(_) => panic!("Lexing succeeded but expected: {}\n{}", expected, src),
|
||||
Err(actual) => {
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test! {
|
||||
name: name_new,
|
||||
text: "foo",
|
||||
tokens: (Name:"foo"),
|
||||
tokens: (Identifier:"foo"),
|
||||
}
|
||||
|
||||
test! {
|
||||
@ -768,9 +812,9 @@ mod tests {
|
||||
name: export_concatination,
|
||||
text: "export foo = 'foo' + 'bar'",
|
||||
tokens: (
|
||||
Name:"export",
|
||||
Identifier:"export",
|
||||
Whitespace,
|
||||
Name:"foo",
|
||||
Identifier:"foo",
|
||||
Whitespace,
|
||||
Equals,
|
||||
Whitespace,
|
||||
@ -786,9 +830,9 @@ mod tests {
|
||||
name: export_complex,
|
||||
text: "export foo = ('foo' + 'bar') + `baz`",
|
||||
tokens: (
|
||||
Name:"export",
|
||||
Identifier:"export",
|
||||
Whitespace,
|
||||
Name:"foo",
|
||||
Identifier:"foo",
|
||||
Whitespace,
|
||||
Equals,
|
||||
Whitespace,
|
||||
@ -821,7 +865,7 @@ mod tests {
|
||||
test! {
|
||||
name: indented_line,
|
||||
text: "foo:\n a",
|
||||
tokens: (Name:"foo", Colon, Eol, Indent:" ", Line, Text:"a", Dedent),
|
||||
tokens: (Identifier:"foo", Colon, Eol, Indent:" ", Text:"a", Dedent),
|
||||
}
|
||||
|
||||
test! {
|
||||
@ -833,19 +877,16 @@ mod tests {
|
||||
c
|
||||
",
|
||||
tokens: (
|
||||
Name:"foo",
|
||||
Identifier:"foo",
|
||||
Colon,
|
||||
Eol,
|
||||
Indent,
|
||||
Line,
|
||||
Text:"a",
|
||||
Eol,
|
||||
Whitespace:" ",
|
||||
Line,
|
||||
Text:"b",
|
||||
Eol,
|
||||
Whitespace:" ",
|
||||
Line,
|
||||
Text:"c",
|
||||
Eol,
|
||||
Dedent,
|
||||
@ -860,15 +901,14 @@ mod tests {
|
||||
b:
|
||||
",
|
||||
tokens: (
|
||||
Name:"foo",
|
||||
Identifier:"foo",
|
||||
Colon,
|
||||
Eol,
|
||||
Indent,
|
||||
Line,
|
||||
Text:"a",
|
||||
Eol,
|
||||
Dedent,
|
||||
Name:"b",
|
||||
Identifier:"b",
|
||||
Colon,
|
||||
Eol,
|
||||
)
|
||||
@ -883,17 +923,15 @@ mod tests {
|
||||
b:
|
||||
",
|
||||
tokens: (
|
||||
Name:"foo",
|
||||
Identifier:"foo",
|
||||
Colon,
|
||||
Eol,
|
||||
Indent:" ",
|
||||
Line,
|
||||
Text:"a",
|
||||
Eol,
|
||||
Line,
|
||||
Eol,
|
||||
Dedent,
|
||||
Name:"b",
|
||||
Identifier:"b",
|
||||
Colon,
|
||||
Eol,
|
||||
),
|
||||
@ -903,11 +941,10 @@ mod tests {
|
||||
name: indented_line_containing_unpaired_carriage_return,
|
||||
text: "foo:\n \r \n",
|
||||
tokens: (
|
||||
Name:"foo",
|
||||
Identifier:"foo",
|
||||
Colon,
|
||||
Eol,
|
||||
Indent:" ",
|
||||
Line,
|
||||
Text:"\r ",
|
||||
Eol,
|
||||
Dedent,
|
||||
@ -931,51 +968,43 @@ mod tests {
|
||||
@mv b c
|
||||
",
|
||||
tokens: (
|
||||
Name:"b",
|
||||
Identifier:"b",
|
||||
Colon,
|
||||
Whitespace,
|
||||
Name:"a",
|
||||
Identifier:"a",
|
||||
Eol,
|
||||
Indent,
|
||||
Line,
|
||||
Text:"@mv a b",
|
||||
Eol,
|
||||
Line,
|
||||
Eol,
|
||||
Dedent,
|
||||
Name:"a",
|
||||
Identifier:"a",
|
||||
Colon,
|
||||
Eol,
|
||||
Indent,
|
||||
Line,
|
||||
Text:"@touch F",
|
||||
Eol,
|
||||
Whitespace:" ",
|
||||
Line,
|
||||
Text:"@touch a",
|
||||
Eol,
|
||||
Line,
|
||||
Eol,
|
||||
Dedent,
|
||||
Name:"d",
|
||||
Identifier:"d",
|
||||
Colon,
|
||||
Whitespace,
|
||||
Name:"c",
|
||||
Identifier:"c",
|
||||
Eol,
|
||||
Indent,
|
||||
Line,
|
||||
Text:"@rm c",
|
||||
Eol,
|
||||
Line,
|
||||
Eol,
|
||||
Dedent,
|
||||
Name:"c",
|
||||
Identifier:"c",
|
||||
Colon,
|
||||
Whitespace,
|
||||
Name:"b",
|
||||
Identifier:"b",
|
||||
Eol,
|
||||
Indent,
|
||||
Line,
|
||||
Text:"@mv b c",
|
||||
Eol,
|
||||
Dedent
|
||||
@ -986,11 +1015,10 @@ mod tests {
|
||||
name: interpolation_empty,
|
||||
text: "hello:\n echo {{}}",
|
||||
tokens: (
|
||||
Name:"hello",
|
||||
Identifier:"hello",
|
||||
Colon,
|
||||
Eol,
|
||||
Indent:" ",
|
||||
Line,
|
||||
Text:"echo ",
|
||||
InterpolationStart,
|
||||
InterpolationEnd,
|
||||
@ -1002,11 +1030,10 @@ mod tests {
|
||||
name: interpolation_expression,
|
||||
text: "hello:\n echo {{`echo hello` + `echo goodbye`}}",
|
||||
tokens: (
|
||||
Name:"hello",
|
||||
Identifier:"hello",
|
||||
Colon,
|
||||
Eol,
|
||||
Indent:" ",
|
||||
Line,
|
||||
Text:"echo ",
|
||||
InterpolationStart,
|
||||
Backtick:"`echo hello`",
|
||||
@ -1028,13 +1055,13 @@ mod tests {
|
||||
test123
|
||||
",
|
||||
tokens: (
|
||||
Name:"foo",
|
||||
Identifier:"foo",
|
||||
Eol,
|
||||
Name:"bar-bob",
|
||||
Identifier:"bar-bob",
|
||||
Eol,
|
||||
Name:"b-bob_asdfAAAA",
|
||||
Identifier:"b-bob_asdfAAAA",
|
||||
Eol,
|
||||
Name:"test123",
|
||||
Identifier:"test123",
|
||||
Eol,
|
||||
),
|
||||
}
|
||||
@ -1043,11 +1070,10 @@ mod tests {
|
||||
name: tokenize_indented_line,
|
||||
text: "foo:\n a",
|
||||
tokens: (
|
||||
Name:"foo",
|
||||
Identifier:"foo",
|
||||
Colon,
|
||||
Eol,
|
||||
Indent:" ",
|
||||
Line,
|
||||
Text:"a",
|
||||
Dedent,
|
||||
),
|
||||
@ -1062,19 +1088,16 @@ mod tests {
|
||||
c
|
||||
",
|
||||
tokens: (
|
||||
Name:"foo",
|
||||
Identifier:"foo",
|
||||
Colon,
|
||||
Eol,
|
||||
Indent,
|
||||
Line,
|
||||
Text:"a",
|
||||
Eol,
|
||||
Whitespace:" ",
|
||||
Line,
|
||||
Text:"b",
|
||||
Eol,
|
||||
Whitespace:" ",
|
||||
Line,
|
||||
Text:"c",
|
||||
Eol,
|
||||
Dedent,
|
||||
@ -1085,7 +1108,7 @@ mod tests {
|
||||
name: tokenize_strings,
|
||||
text: r#"a = "'a'" + '"b"' + "'c'" + '"d"'#echo hello"#,
|
||||
tokens: (
|
||||
Name:"a",
|
||||
Identifier:"a",
|
||||
Whitespace,
|
||||
Equals,
|
||||
Whitespace,
|
||||
@ -1113,15 +1136,14 @@ mod tests {
|
||||
{{hello}}
|
||||
",
|
||||
tokens: (
|
||||
Name:"foo",
|
||||
Identifier:"foo",
|
||||
Colon,
|
||||
Whitespace,
|
||||
Comment:"# some comment",
|
||||
Eol,
|
||||
Indent:" ",
|
||||
Line,
|
||||
InterpolationStart,
|
||||
Name:"hello",
|
||||
Identifier:"hello",
|
||||
InterpolationEnd,
|
||||
Eol,
|
||||
Dedent
|
||||
@ -1135,15 +1157,14 @@ mod tests {
|
||||
# another comment
|
||||
",
|
||||
tokens: (
|
||||
Name:"foo",
|
||||
Identifier:"foo",
|
||||
Colon,
|
||||
Whitespace,
|
||||
Comment:"# more comments",
|
||||
Eol,
|
||||
Indent:" ",
|
||||
Line,
|
||||
InterpolationStart,
|
||||
Name:"hello",
|
||||
Identifier:"hello",
|
||||
InterpolationEnd,
|
||||
Eol,
|
||||
Dedent,
|
||||
@ -1156,19 +1177,18 @@ mod tests {
|
||||
name: tokenize_recipe_complex_interpolation_expression,
|
||||
text: "foo: #lol\n {{a + b + \"z\" + blarg}}",
|
||||
tokens: (
|
||||
Name:"foo",
|
||||
Identifier:"foo",
|
||||
Colon,
|
||||
Whitespace:" ",
|
||||
Comment:"#lol",
|
||||
Eol,
|
||||
Indent:" ",
|
||||
Line,
|
||||
InterpolationStart,
|
||||
Name:"a",
|
||||
Identifier:"a",
|
||||
Whitespace,
|
||||
Plus,
|
||||
Whitespace,
|
||||
Name:"b",
|
||||
Identifier:"b",
|
||||
Whitespace,
|
||||
Plus,
|
||||
Whitespace,
|
||||
@ -1176,7 +1196,7 @@ mod tests {
|
||||
Whitespace,
|
||||
Plus,
|
||||
Whitespace,
|
||||
Name:"blarg",
|
||||
Identifier:"blarg",
|
||||
InterpolationEnd,
|
||||
Dedent,
|
||||
),
|
||||
@ -1186,23 +1206,22 @@ mod tests {
|
||||
name: tokenize_recipe_multiple_interpolations,
|
||||
text: "foo:,#ok\n {{a}}0{{b}}1{{c}}",
|
||||
tokens: (
|
||||
Name:"foo",
|
||||
Identifier:"foo",
|
||||
Colon,
|
||||
Comma,
|
||||
Comment:"#ok",
|
||||
Eol,
|
||||
Indent:" ",
|
||||
Line,
|
||||
InterpolationStart,
|
||||
Name:"a",
|
||||
Identifier:"a",
|
||||
InterpolationEnd,
|
||||
Text:"0",
|
||||
InterpolationStart,
|
||||
Name:"b",
|
||||
Identifier:"b",
|
||||
InterpolationEnd,
|
||||
Text:"1",
|
||||
InterpolationStart,
|
||||
Name:"c",
|
||||
Identifier:"c",
|
||||
InterpolationEnd,
|
||||
Dedent,
|
||||
|
||||
@ -1217,24 +1236,24 @@ mod tests {
|
||||
hello blah blah blah : a b c #whatever
|
||||
",
|
||||
tokens: (
|
||||
Name:"bob",
|
||||
Identifier:"bob",
|
||||
Eol,
|
||||
Eol,
|
||||
Name:"hello",
|
||||
Identifier:"hello",
|
||||
Whitespace,
|
||||
Name:"blah",
|
||||
Identifier:"blah",
|
||||
Whitespace,
|
||||
Name:"blah",
|
||||
Identifier:"blah",
|
||||
Whitespace,
|
||||
Name:"blah",
|
||||
Identifier:"blah",
|
||||
Whitespace,
|
||||
Colon,
|
||||
Whitespace,
|
||||
Name:"a",
|
||||
Identifier:"a",
|
||||
Whitespace,
|
||||
Name:"b",
|
||||
Identifier:"b",
|
||||
Whitespace,
|
||||
Name:"c",
|
||||
Identifier:"c",
|
||||
Whitespace,
|
||||
Comment:"#whatever",
|
||||
Eol,
|
||||
@ -1260,30 +1279,23 @@ mod tests {
|
||||
Eol,
|
||||
Comment:"# this does something",
|
||||
Eol,
|
||||
Name:"hello",
|
||||
Identifier:"hello",
|
||||
Colon,
|
||||
Eol,
|
||||
Indent,
|
||||
Line,
|
||||
Text:"asdf",
|
||||
Eol,
|
||||
Whitespace:" ",
|
||||
Line,
|
||||
Text:"bsdf",
|
||||
Eol,
|
||||
Line,
|
||||
Eol,
|
||||
Whitespace:" ",
|
||||
Line,
|
||||
Text:"csdf",
|
||||
Eol,
|
||||
Line,
|
||||
Eol,
|
||||
Whitespace:" ",
|
||||
Line,
|
||||
Text:"dsdf # whatever",
|
||||
Eol,
|
||||
Line,
|
||||
Eol,
|
||||
Dedent,
|
||||
Comment:"# yolo",
|
||||
@ -1302,18 +1314,17 @@ mod tests {
|
||||
tokens: (
|
||||
Comment:"#",
|
||||
Eol,
|
||||
Name:"A",
|
||||
Identifier:"A",
|
||||
Equals,
|
||||
StringRaw:"'1'",
|
||||
Eol,
|
||||
Name:"echo",
|
||||
Identifier:"echo",
|
||||
Colon,
|
||||
Eol,
|
||||
Indent,
|
||||
Line,
|
||||
Text:"echo ",
|
||||
InterpolationStart,
|
||||
Name:"A",
|
||||
Identifier:"A",
|
||||
InterpolationEnd,
|
||||
Eol,
|
||||
Dedent,
|
||||
@ -1324,11 +1335,10 @@ mod tests {
|
||||
name: tokenize_interpolation_backticks,
|
||||
text: "hello:\n echo {{`echo hello` + `echo goodbye`}}",
|
||||
tokens: (
|
||||
Name:"hello",
|
||||
Identifier:"hello",
|
||||
Colon,
|
||||
Eol,
|
||||
Indent:" ",
|
||||
Line,
|
||||
Text:"echo ",
|
||||
InterpolationStart,
|
||||
Backtick:"`echo hello`",
|
||||
@ -1345,11 +1355,10 @@ mod tests {
|
||||
name: tokenize_empty_interpolation,
|
||||
text: "hello:\n echo {{}}",
|
||||
tokens: (
|
||||
Name:"hello",
|
||||
Identifier:"hello",
|
||||
Colon,
|
||||
Eol,
|
||||
Indent:" ",
|
||||
Line,
|
||||
Text:"echo ",
|
||||
InterpolationStart,
|
||||
InterpolationEnd,
|
||||
@ -1361,7 +1370,7 @@ mod tests {
|
||||
name: tokenize_assignment_backticks,
|
||||
text: "a = `echo hello` + `echo goodbye`",
|
||||
tokens: (
|
||||
Name:"a",
|
||||
Identifier:"a",
|
||||
Whitespace,
|
||||
Equals,
|
||||
Whitespace,
|
||||
@ -1392,42 +1401,33 @@ mod tests {
|
||||
",
|
||||
tokens: (
|
||||
Eol,
|
||||
Name:"hello",
|
||||
Identifier:"hello",
|
||||
Colon,
|
||||
Eol,
|
||||
Indent,
|
||||
Line,
|
||||
Text:"a",
|
||||
Eol,
|
||||
Whitespace:" ",
|
||||
Line,
|
||||
Text:"b",
|
||||
Eol,
|
||||
Line,
|
||||
Eol,
|
||||
Whitespace:" ",
|
||||
Line,
|
||||
Text:"c",
|
||||
Eol,
|
||||
Line,
|
||||
Eol,
|
||||
Whitespace:" ",
|
||||
Line,
|
||||
Text:"d",
|
||||
Eol,
|
||||
Line,
|
||||
Eol,
|
||||
Dedent,
|
||||
Comment:"# hello",
|
||||
Eol,
|
||||
Name:"bob",
|
||||
Identifier:"bob",
|
||||
Colon,
|
||||
Eol,
|
||||
Indent:" ",
|
||||
Line,
|
||||
Text:"frank",
|
||||
Eol,
|
||||
Line,
|
||||
Eol,
|
||||
Dedent,
|
||||
),
|
||||
@ -1437,7 +1437,7 @@ mod tests {
|
||||
name: tokenize_comment,
|
||||
text: "a:=#",
|
||||
tokens: (
|
||||
Name:"a",
|
||||
Identifier:"a",
|
||||
ColonEquals,
|
||||
Comment:"#",
|
||||
),
|
||||
@ -1447,7 +1447,7 @@ mod tests {
|
||||
name: tokenize_comment_with_bang,
|
||||
text: "a:=#foo!",
|
||||
tokens: (
|
||||
Name:"a",
|
||||
Identifier:"a",
|
||||
ColonEquals,
|
||||
Comment:"#foo!",
|
||||
),
|
||||
@ -1470,51 +1470,43 @@ mod tests {
|
||||
@mv b c
|
||||
",
|
||||
tokens: (
|
||||
Name:"b",
|
||||
Identifier:"b",
|
||||
Colon,
|
||||
Whitespace,
|
||||
Name:"a",
|
||||
Identifier:"a",
|
||||
Eol,
|
||||
Indent,
|
||||
Line,
|
||||
Text:"@mv a b",
|
||||
Eol,
|
||||
Line,
|
||||
Eol,
|
||||
Dedent,
|
||||
Name:"a",
|
||||
Identifier:"a",
|
||||
Colon,
|
||||
Eol,
|
||||
Indent,
|
||||
Line,
|
||||
Text:"@touch F",
|
||||
Eol,
|
||||
Whitespace:" ",
|
||||
Line,
|
||||
Text:"@touch a",
|
||||
Eol,
|
||||
Line,
|
||||
Eol,
|
||||
Dedent,
|
||||
Name:"d",
|
||||
Identifier:"d",
|
||||
Colon,
|
||||
Whitespace,
|
||||
Name:"c",
|
||||
Identifier:"c",
|
||||
Eol,
|
||||
Indent,
|
||||
Line,
|
||||
Text:"@rm c",
|
||||
Eol,
|
||||
Line,
|
||||
Eol,
|
||||
Dedent,
|
||||
Name:"c",
|
||||
Identifier:"c",
|
||||
Colon,
|
||||
Whitespace,
|
||||
Name:"b",
|
||||
Identifier:"b",
|
||||
Eol,
|
||||
Indent,
|
||||
Line,
|
||||
Text:"@mv b c",
|
||||
Eol,
|
||||
Dedent,
|
||||
@ -1533,7 +1525,7 @@ mod tests {
|
||||
ParenR,
|
||||
Whitespace,
|
||||
ParenR,
|
||||
Name:"abc",
|
||||
Identifier:"abc",
|
||||
ParenL,
|
||||
Plus,
|
||||
),
|
||||
@ -1554,20 +1546,19 @@ mod tests {
|
||||
name: multiple_recipes,
|
||||
text: "a:\n foo\nb:",
|
||||
tokens: (
|
||||
Name:"a",
|
||||
Identifier:"a",
|
||||
Colon,
|
||||
Eol,
|
||||
Indent:" ",
|
||||
Line,
|
||||
Text:"foo",
|
||||
Eol,
|
||||
Dedent,
|
||||
Name:"b",
|
||||
Identifier:"b",
|
||||
Colon,
|
||||
),
|
||||
}
|
||||
|
||||
error_test! {
|
||||
error! {
|
||||
name: tokenize_space_then_tab,
|
||||
input: "a:
|
||||
0
|
||||
@ -1581,7 +1572,7 @@ mod tests {
|
||||
kind: InconsistentLeadingWhitespace{expected: " ", found: "\t"},
|
||||
}
|
||||
|
||||
error_test! {
|
||||
error! {
|
||||
name: tokenize_tabs_then_tab_space,
|
||||
input: "a:
|
||||
\t\t0
|
||||
@ -1595,7 +1586,7 @@ mod tests {
|
||||
kind: InconsistentLeadingWhitespace{expected: "\t\t", found: "\t "},
|
||||
}
|
||||
|
||||
error_test! {
|
||||
error! {
|
||||
name: tokenize_unknown,
|
||||
input: "~",
|
||||
offset: 0,
|
||||
@ -1605,7 +1596,7 @@ mod tests {
|
||||
kind: UnknownStartOfToken,
|
||||
}
|
||||
|
||||
error_test! {
|
||||
error! {
|
||||
name: unterminated_string_with_escapes,
|
||||
input: r#"a = "\n\t\r\"\\"#,
|
||||
offset: 4,
|
||||
@ -1615,7 +1606,7 @@ mod tests {
|
||||
kind: UnterminatedString,
|
||||
}
|
||||
|
||||
error_test! {
|
||||
error! {
|
||||
name: unterminated_raw_string,
|
||||
input: "r a='asdf",
|
||||
offset: 4,
|
||||
@ -1625,7 +1616,7 @@ mod tests {
|
||||
kind: UnterminatedString,
|
||||
}
|
||||
|
||||
error_test! {
|
||||
error! {
|
||||
name: unterminated_interpolation,
|
||||
input: "foo:\n echo {{
|
||||
",
|
||||
@ -1636,7 +1627,7 @@ mod tests {
|
||||
kind: UnterminatedInterpolation,
|
||||
}
|
||||
|
||||
error_test! {
|
||||
error! {
|
||||
name: unterminated_backtick,
|
||||
input: "`echo",
|
||||
offset: 0,
|
||||
@ -1646,7 +1637,7 @@ mod tests {
|
||||
kind: UnterminatedBacktick,
|
||||
}
|
||||
|
||||
error_test! {
|
||||
error! {
|
||||
name: unpaired_carriage_return,
|
||||
input: "foo\rbar",
|
||||
offset: 3,
|
||||
@ -1656,7 +1647,7 @@ mod tests {
|
||||
kind: UnpairedCarriageReturn,
|
||||
}
|
||||
|
||||
error_test! {
|
||||
error! {
|
||||
name: unknown_start_of_token_ampersand,
|
||||
input: " \r\n&",
|
||||
offset: 3,
|
||||
@ -1666,7 +1657,7 @@ mod tests {
|
||||
kind: UnknownStartOfToken,
|
||||
}
|
||||
|
||||
error_test! {
|
||||
error! {
|
||||
name: unknown_start_of_token_tilde,
|
||||
input: "~",
|
||||
offset: 0,
|
||||
@ -1676,7 +1667,7 @@ mod tests {
|
||||
kind: UnknownStartOfToken,
|
||||
}
|
||||
|
||||
error_test! {
|
||||
error! {
|
||||
name: unterminated_string,
|
||||
input: r#"a = ""#,
|
||||
offset: 4,
|
||||
@ -1686,7 +1677,7 @@ mod tests {
|
||||
kind: UnterminatedString,
|
||||
}
|
||||
|
||||
error_test! {
|
||||
error! {
|
||||
name: mixed_leading_whitespace,
|
||||
input: "a:\n\t echo hello",
|
||||
offset: 3,
|
||||
@ -1696,7 +1687,7 @@ mod tests {
|
||||
kind: MixedLeadingWhitespace{whitespace: "\t "},
|
||||
}
|
||||
|
||||
error_test! {
|
||||
error! {
|
||||
name: unclosed_interpolation_delimiter,
|
||||
input: "a:\n echo {{ foo",
|
||||
offset: 9,
|
||||
|
20
src/lib.rs
20
src/lib.rs
@ -3,7 +3,14 @@ extern crate lazy_static;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
mod testing;
|
||||
pub mod testing;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
pub mod tree;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod node;
|
||||
|
||||
#[cfg(fuzzing)]
|
||||
pub(crate) mod fuzzing;
|
||||
@ -13,6 +20,8 @@ mod die;
|
||||
|
||||
mod alias;
|
||||
mod alias_resolver;
|
||||
mod analyzer;
|
||||
mod assignment;
|
||||
mod assignment_evaluator;
|
||||
mod assignment_resolver;
|
||||
mod color;
|
||||
@ -20,6 +29,8 @@ mod command_ext;
|
||||
mod common;
|
||||
mod compilation_error;
|
||||
mod compilation_error_kind;
|
||||
mod compilation_result_ext;
|
||||
mod compiler;
|
||||
mod config;
|
||||
mod config_error;
|
||||
mod count;
|
||||
@ -33,10 +44,16 @@ mod function_context;
|
||||
mod functions;
|
||||
mod interrupt_guard;
|
||||
mod interrupt_handler;
|
||||
mod item;
|
||||
mod justfile;
|
||||
mod keyed;
|
||||
mod keyword;
|
||||
mod lexer;
|
||||
mod line;
|
||||
mod list;
|
||||
mod load_dotenv;
|
||||
mod module;
|
||||
mod name;
|
||||
mod ordinal;
|
||||
mod output;
|
||||
mod output_error;
|
||||
@ -58,6 +75,7 @@ mod show_whitespace;
|
||||
mod state;
|
||||
mod string_literal;
|
||||
mod subcommand;
|
||||
mod table;
|
||||
mod token;
|
||||
mod token_kind;
|
||||
mod use_color;
|
||||
|
28
src/line.rs
Normal file
28
src/line.rs
Normal file
@ -0,0 +1,28 @@
|
||||
use crate::common::*;
|
||||
|
||||
/// A single line in a recipe body, consisting of any number of
|
||||
/// `Fragment`s.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct Line<'src> {
|
||||
pub(crate) fragments: Vec<Fragment<'src>>,
|
||||
}
|
||||
|
||||
impl<'src> Line<'src> {
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
self.fragments.is_empty()
|
||||
}
|
||||
|
||||
pub(crate) fn is_continuation(&self) -> bool {
|
||||
match self.fragments.last() {
|
||||
Some(Fragment::Text { token }) => token.lexeme().ends_with('\\'),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_shebang(&self) -> bool {
|
||||
match self.fragments.first() {
|
||||
Some(Fragment::Text { token }) => token.lexeme().starts_with("#!"),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
16
src/module.rs
Normal file
16
src/module.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use crate::common::*;
|
||||
|
||||
/// A module, the top-level type produced by the parser. So-named because
|
||||
/// although at present, all justfiles consist of a single module, in the
|
||||
/// future we will likely have multi-module and multi-file justfiles.
|
||||
///
|
||||
/// Not all successful parses result in valid justfiles, so additional
|
||||
/// consistency checks and name resolution are performed by the `Analyzer`,
|
||||
/// which produces a `Justfile` from a `Module`.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Module<'src> {
|
||||
/// Items in the justfile
|
||||
pub(crate) items: Vec<Item<'src>>,
|
||||
/// Non-fatal warnings encountered during parsing
|
||||
pub(crate) warnings: Vec<Warning<'src>>,
|
||||
}
|
52
src/name.rs
Normal file
52
src/name.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use crate::common::*;
|
||||
|
||||
/// A name. This is effectively just a `Token` of kind `Identifier`, but we
|
||||
/// give it its own type for clarity.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
|
||||
pub(crate) struct Name<'src> {
|
||||
pub(crate) offset: usize,
|
||||
pub(crate) length: usize,
|
||||
pub(crate) line: usize,
|
||||
pub(crate) column: usize,
|
||||
pub(crate) src: &'src str,
|
||||
}
|
||||
|
||||
impl<'src> Name<'src> {
|
||||
/// The name's text contents
|
||||
pub(crate) fn lexeme(&self) -> &'src str {
|
||||
&self.src[self.offset..self.offset + self.length]
|
||||
}
|
||||
|
||||
/// Turn this name back into a token
|
||||
pub(crate) fn token(&self) -> Token<'src> {
|
||||
Token {
|
||||
kind: TokenKind::Identifier,
|
||||
offset: self.offset,
|
||||
length: self.length,
|
||||
line: self.line,
|
||||
column: self.column,
|
||||
src: self.src,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_identifier(token: Token<'src>) -> Name {
|
||||
assert_eq!(token.kind, TokenKind::Identifier);
|
||||
Name {
|
||||
offset: token.offset,
|
||||
length: token.length,
|
||||
line: token.line,
|
||||
column: token.column,
|
||||
src: token.src,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn error(&self, kind: CompilationErrorKind<'src>) -> CompilationError<'src> {
|
||||
self.token().error(kind)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Name<'_> {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.lexeme())
|
||||
}
|
||||
}
|
150
src/node.rs
Normal file
150
src/node.rs
Normal file
@ -0,0 +1,150 @@
|
||||
use crate::common::*;
|
||||
|
||||
/// Methods commmon to all AST nodes. Currently only used in parser unit tests.
|
||||
pub(crate) trait Node<'src> {
|
||||
/// Construct an untyped tree of atoms representing this Node. This function,
|
||||
/// and `Tree` type, are only used in parser unit tests.
|
||||
fn tree(&self) -> Tree<'src>;
|
||||
}
|
||||
|
||||
impl<'src> Node<'src> for Module<'src> {
|
||||
fn tree(&self) -> Tree<'src> {
|
||||
Tree::atom("justfile")
|
||||
.extend(self.items.iter().map(|item| item.tree()))
|
||||
.extend(self.warnings.iter().map(|warning| warning.tree()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'src> Node<'src> for Item<'src> {
|
||||
fn tree(&self) -> Tree<'src> {
|
||||
match self {
|
||||
Item::Alias(alias) => alias.tree(),
|
||||
Item::Assignment(assignment) => assignment.tree(),
|
||||
Item::Recipe(recipe) => recipe.tree(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'src> Node<'src> for Alias<'src> {
|
||||
fn tree(&self) -> Tree<'src> {
|
||||
Tree::atom(keyword::ALIAS)
|
||||
.push(self.name.lexeme())
|
||||
.push(self.target.lexeme())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'src> Node<'src> for Assignment<'src> {
|
||||
fn tree(&self) -> Tree<'src> {
|
||||
if self.export {
|
||||
Tree::atom("assignment").push("#").push(keyword::EXPORT)
|
||||
} else {
|
||||
Tree::atom("assignment")
|
||||
}
|
||||
.push(self.name.lexeme())
|
||||
.push(self.expression.tree())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'src> Node<'src> for Expression<'src> {
|
||||
fn tree(&self) -> Tree<'src> {
|
||||
match self {
|
||||
Expression::Concatination { lhs, rhs } => Tree::atom("+").push(lhs.tree()).push(rhs.tree()),
|
||||
Expression::Call {
|
||||
function,
|
||||
arguments,
|
||||
} => Tree::atom("call")
|
||||
.push(function.lexeme())
|
||||
.extend(arguments.iter().map(|argument| argument.tree())),
|
||||
Expression::Variable { name } => Tree::atom(name.lexeme()),
|
||||
Expression::StringLiteral {
|
||||
string_literal: StringLiteral { cooked, .. },
|
||||
} => Tree::string(cooked),
|
||||
Expression::Backtick { contents, .. } => Tree::atom("backtick").push(Tree::string(contents)),
|
||||
Expression::Group { contents } => Tree::List(vec![contents.tree()]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'src> Node<'src> for Recipe<'src> {
|
||||
fn tree(&self) -> Tree<'src> {
|
||||
let mut t = Tree::atom("recipe");
|
||||
|
||||
if self.quiet {
|
||||
t.push_mut("#");
|
||||
t.push_mut("quiet");
|
||||
}
|
||||
|
||||
if let Some(doc) = self.doc {
|
||||
t.push_mut(Tree::string(doc));
|
||||
}
|
||||
|
||||
t.push_mut(self.name.lexeme());
|
||||
|
||||
if !self.parameters.is_empty() {
|
||||
let mut params = Tree::atom("params");
|
||||
|
||||
for parameter in &self.parameters {
|
||||
if parameter.variadic {
|
||||
params.push_mut("+");
|
||||
}
|
||||
|
||||
params.push_mut(parameter.tree());
|
||||
}
|
||||
|
||||
t.push_mut(params);
|
||||
}
|
||||
|
||||
if !self.dependencies.is_empty() {
|
||||
t = t.push(
|
||||
Tree::atom("deps").extend(
|
||||
self
|
||||
.dependencies
|
||||
.iter()
|
||||
.map(|dependency| dependency.lexeme()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if !self.body.is_empty() {
|
||||
t.push_mut(Tree::atom("body").extend(self.body.iter().map(|line| line.tree())));
|
||||
}
|
||||
|
||||
t
|
||||
}
|
||||
}
|
||||
|
||||
impl<'src> Node<'src> for Parameter<'src> {
|
||||
fn tree(&self) -> Tree<'src> {
|
||||
let mut children = Vec::new();
|
||||
children.push(Tree::atom(self.name.lexeme()));
|
||||
|
||||
if let Some(default) = &self.default {
|
||||
children.push(default.tree());
|
||||
}
|
||||
|
||||
Tree::List(children)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'src> Node<'src> for Line<'src> {
|
||||
fn tree(&self) -> Tree<'src> {
|
||||
Tree::list(self.fragments.iter().map(|fragment| fragment.tree()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'src> Node<'src> for Fragment<'src> {
|
||||
fn tree(&self) -> Tree<'src> {
|
||||
match self {
|
||||
Fragment::Text { token } => Tree::string(token.lexeme()),
|
||||
Fragment::Interpolation { expression } => Tree::List(vec![expression.tree()]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'src> Node<'src> for Warning<'src> {
|
||||
fn tree(&self) -> Tree<'src> {
|
||||
match self {
|
||||
Warning::DeprecatedEquals { .. } => Tree::atom("warning").push("deprecated_equals"),
|
||||
}
|
||||
}
|
||||
}
|
@ -25,4 +25,3 @@ impl Display for OutputError {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,20 +1,23 @@
|
||||
use crate::common::*;
|
||||
|
||||
/// A single function parameter
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub(crate) struct Parameter<'a> {
|
||||
pub(crate) default: Option<Expression<'a>>,
|
||||
pub(crate) name: &'a str,
|
||||
pub(crate) token: Token<'a>,
|
||||
pub(crate) struct Parameter<'src> {
|
||||
/// The parameter name
|
||||
pub(crate) name: Name<'src>,
|
||||
/// Parameter is variadic
|
||||
pub(crate) variadic: bool,
|
||||
/// An optional default expression
|
||||
pub(crate) default: Option<Expression<'src>>,
|
||||
}
|
||||
|
||||
impl<'a> Display for Parameter<'a> {
|
||||
impl<'src> Display for Parameter<'src> {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
let color = Color::fmt(f);
|
||||
if self.variadic {
|
||||
write!(f, "{}", color.annotation().paint("+"))?;
|
||||
}
|
||||
write!(f, "{}", color.parameter().paint(self.name))?;
|
||||
write!(f, "{}", color.parameter().paint(self.name.lexeme()))?;
|
||||
if let Some(ref default) = self.default {
|
||||
write!(f, "={}", color.string().paint(&default.to_string()))?;
|
||||
}
|
||||
|
2317
src/parser.rs
2317
src/parser.rs
File diff suppressed because it is too large
Load Diff
@ -23,7 +23,7 @@ where
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
@ -22,14 +22,13 @@ fn error_from_signal(
|
||||
}
|
||||
}
|
||||
|
||||
/// A recipe, e.g. `foo: bar baz`
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub(crate) struct Recipe<'a> {
|
||||
pub(crate) dependencies: Vec<&'a str>,
|
||||
pub(crate) dependency_tokens: Vec<Token<'a>>,
|
||||
pub(crate) dependencies: Vec<Name<'a>>,
|
||||
pub(crate) doc: Option<&'a str>,
|
||||
pub(crate) line_number: usize,
|
||||
pub(crate) lines: Vec<Vec<Fragment<'a>>>,
|
||||
pub(crate) name: &'a str,
|
||||
pub(crate) body: Vec<Line<'a>>,
|
||||
pub(crate) name: Name<'a>,
|
||||
pub(crate) parameters: Vec<Parameter<'a>>,
|
||||
pub(crate) private: bool,
|
||||
pub(crate) quiet: bool,
|
||||
@ -57,12 +56,19 @@ impl<'a> Recipe<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn name(&self) -> &'a str {
|
||||
self.name.lexeme()
|
||||
}
|
||||
|
||||
pub(crate) fn line_number(&self) -> usize {
|
||||
self.name.line
|
||||
}
|
||||
|
||||
pub(crate) fn run(
|
||||
&self,
|
||||
context: &RecipeContext<'a>,
|
||||
arguments: &[&'a str],
|
||||
dotenv: &BTreeMap<String, String>,
|
||||
exports: &BTreeSet<&'a str>,
|
||||
) -> RunResult<'a, ()> {
|
||||
let config = &context.config;
|
||||
|
||||
@ -88,7 +94,6 @@ impl<'a> Recipe<'a> {
|
||||
scope: &context.scope,
|
||||
shell: config.shell,
|
||||
dotenv,
|
||||
exports,
|
||||
};
|
||||
|
||||
let mut rest = arguments;
|
||||
@ -111,13 +116,13 @@ impl<'a> Recipe<'a> {
|
||||
rest = &rest[1..];
|
||||
value
|
||||
};
|
||||
argument_map.insert(parameter.name, value);
|
||||
argument_map.insert(parameter.name.lexeme(), value);
|
||||
}
|
||||
|
||||
if self.shebang {
|
||||
let mut evaluated_lines = vec![];
|
||||
for line in &self.lines {
|
||||
evaluated_lines.push(evaluator.evaluate_line(line, &argument_map)?);
|
||||
for line in &self.body {
|
||||
evaluated_lines.push(evaluator.evaluate_line(&line.fragments, &argument_map)?);
|
||||
}
|
||||
|
||||
if config.dry_run || self.quiet {
|
||||
@ -134,14 +139,14 @@ impl<'a> Recipe<'a> {
|
||||
.prefix("just")
|
||||
.tempdir()
|
||||
.map_err(|error| RuntimeError::TmpdirIoError {
|
||||
recipe: self.name,
|
||||
recipe: self.name(),
|
||||
io_error: error,
|
||||
})?;
|
||||
let mut path = tmp.path().to_path_buf();
|
||||
path.push(self.name);
|
||||
path.push(self.name());
|
||||
{
|
||||
let mut f = fs::File::create(&path).map_err(|error| RuntimeError::TmpdirIoError {
|
||||
recipe: self.name,
|
||||
recipe: self.name(),
|
||||
io_error: error,
|
||||
})?;
|
||||
let mut text = String::new();
|
||||
@ -151,7 +156,7 @@ impl<'a> Recipe<'a> {
|
||||
// add blank lines so that lines in the generated script
|
||||
// have the same line number as the corresponding lines
|
||||
// in the justfile
|
||||
for _ in 1..(self.line_number + 2) {
|
||||
for _ in 1..(self.line_number() + 2) {
|
||||
text += "\n"
|
||||
}
|
||||
for line in &evaluated_lines[1..] {
|
||||
@ -165,14 +170,14 @@ impl<'a> Recipe<'a> {
|
||||
|
||||
f.write_all(text.as_bytes())
|
||||
.map_err(|error| RuntimeError::TmpdirIoError {
|
||||
recipe: self.name,
|
||||
recipe: self.name(),
|
||||
io_error: error,
|
||||
})?;
|
||||
}
|
||||
|
||||
// make the script executable
|
||||
Platform::set_execute_permission(&path).map_err(|error| RuntimeError::TmpdirIoError {
|
||||
recipe: self.name,
|
||||
recipe: self.name(),
|
||||
io_error: error,
|
||||
})?;
|
||||
|
||||
@ -193,12 +198,12 @@ impl<'a> Recipe<'a> {
|
||||
let mut command =
|
||||
Platform::make_shebang_command(&path, interpreter, argument).map_err(|output_error| {
|
||||
RuntimeError::Cygpath {
|
||||
recipe: self.name,
|
||||
recipe: self.name(),
|
||||
output_error,
|
||||
}
|
||||
})?;
|
||||
|
||||
command.export_environment_variables(&context.scope, dotenv, exports)?;
|
||||
command.export_environment_variables(&context.scope, dotenv)?;
|
||||
|
||||
// run it!
|
||||
match InterruptHandler::guard(|| command.status()) {
|
||||
@ -206,18 +211,18 @@ impl<'a> Recipe<'a> {
|
||||
if let Some(code) = exit_status.code() {
|
||||
if code != 0 {
|
||||
return Err(RuntimeError::Code {
|
||||
recipe: self.name,
|
||||
recipe: self.name(),
|
||||
line_number: None,
|
||||
code,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return Err(error_from_signal(self.name, None, exit_status));
|
||||
return Err(error_from_signal(self.name(), None, exit_status));
|
||||
}
|
||||
}
|
||||
Err(io_error) => {
|
||||
return Err(RuntimeError::Shebang {
|
||||
recipe: self.name,
|
||||
recipe: self.name(),
|
||||
command: interpreter.to_string(),
|
||||
argument: argument.map(String::from),
|
||||
io_error,
|
||||
@ -225,8 +230,8 @@ impl<'a> Recipe<'a> {
|
||||
}
|
||||
};
|
||||
} else {
|
||||
let mut lines = self.lines.iter().peekable();
|
||||
let mut line_number = self.line_number + 1;
|
||||
let mut lines = self.body.iter().peekable();
|
||||
let mut line_number = self.line_number() + 1;
|
||||
loop {
|
||||
if lines.peek().is_none() {
|
||||
break;
|
||||
@ -238,8 +243,8 @@ impl<'a> Recipe<'a> {
|
||||
}
|
||||
let line = lines.next().unwrap();
|
||||
line_number += 1;
|
||||
evaluated += &evaluator.evaluate_line(line, &argument_map)?;
|
||||
if line.last().map(Fragment::continuation).unwrap_or(false) {
|
||||
evaluated += &evaluator.evaluate_line(&line.fragments, &argument_map)?;
|
||||
if line.is_continuation() {
|
||||
evaluated.pop();
|
||||
} else {
|
||||
break;
|
||||
@ -280,25 +285,29 @@ impl<'a> Recipe<'a> {
|
||||
cmd.stdout(Stdio::null());
|
||||
}
|
||||
|
||||
cmd.export_environment_variables(&context.scope, dotenv, exports)?;
|
||||
cmd.export_environment_variables(&context.scope, dotenv)?;
|
||||
|
||||
match InterruptHandler::guard(|| cmd.status()) {
|
||||
Ok(exit_status) => {
|
||||
if let Some(code) = exit_status.code() {
|
||||
if code != 0 {
|
||||
return Err(RuntimeError::Code {
|
||||
recipe: self.name,
|
||||
recipe: self.name(),
|
||||
line_number: Some(line_number),
|
||||
code,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return Err(error_from_signal(self.name, Some(line_number), exit_status));
|
||||
return Err(error_from_signal(
|
||||
self.name(),
|
||||
Some(line_number),
|
||||
exit_status,
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(io_error) => {
|
||||
return Err(RuntimeError::IoError {
|
||||
recipe: self.name,
|
||||
recipe: self.name(),
|
||||
io_error,
|
||||
});
|
||||
}
|
||||
@ -309,6 +318,12 @@ impl<'a> Recipe<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'src> Keyed<'src> for Recipe<'src> {
|
||||
fn key(&self) -> &'src str {
|
||||
self.name.lexeme()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Display for Recipe<'a> {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
if let Some(doc) = self.doc {
|
||||
@ -329,20 +344,20 @@ impl<'a> Display for Recipe<'a> {
|
||||
write!(f, " {}", dependency)?;
|
||||
}
|
||||
|
||||
for (i, pieces) in self.lines.iter().enumerate() {
|
||||
for (i, line) in self.body.iter().enumerate() {
|
||||
if i == 0 {
|
||||
writeln!(f)?;
|
||||
}
|
||||
for (j, piece) in pieces.iter().enumerate() {
|
||||
for (j, fragment) in line.fragments.iter().enumerate() {
|
||||
if j == 0 {
|
||||
write!(f, " ")?;
|
||||
}
|
||||
match *piece {
|
||||
Fragment::Text { ref text } => write!(f, "{}", text.lexeme())?,
|
||||
Fragment::Expression { ref expression, .. } => write!(f, "{{{{{}}}}}", expression)?,
|
||||
match fragment {
|
||||
Fragment::Text { token } => write!(f, "{}", token.lexeme())?,
|
||||
Fragment::Interpolation { expression, .. } => write!(f, "{{{{{}}}}}", expression)?,
|
||||
}
|
||||
}
|
||||
if i + 1 < self.lines.len() {
|
||||
if i + 1 < self.body.len() {
|
||||
writeln!(f)?;
|
||||
}
|
||||
}
|
||||
|
@ -2,5 +2,5 @@ use crate::common::*;
|
||||
|
||||
pub(crate) struct RecipeContext<'a> {
|
||||
pub(crate) config: &'a Config<'a>,
|
||||
pub(crate) scope: BTreeMap<&'a str, String>,
|
||||
pub(crate) scope: BTreeMap<&'a str, (bool, String)>,
|
||||
}
|
||||
|
@ -17,22 +17,19 @@ pub(crate) struct RecipeResolver<'a: 'b, 'b> {
|
||||
seen: BTreeSet<&'a str>,
|
||||
resolved: BTreeSet<&'a str>,
|
||||
recipes: &'b BTreeMap<&'a str, Recipe<'a>>,
|
||||
assignments: &'b BTreeMap<&'a str, Expression<'a>>,
|
||||
text: &'a str,
|
||||
assignments: &'b BTreeMap<&'a str, Assignment<'a>>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> RecipeResolver<'a, 'b> {
|
||||
pub(crate) fn resolve_recipes(
|
||||
recipes: &BTreeMap<&'a str, Recipe<'a>>,
|
||||
assignments: &BTreeMap<&'a str, Expression<'a>>,
|
||||
text: &'a str,
|
||||
assignments: &BTreeMap<&'a str, Assignment<'a>>,
|
||||
) -> CompilationResult<'a, ()> {
|
||||
let mut resolver = RecipeResolver {
|
||||
seen: empty(),
|
||||
stack: empty(),
|
||||
resolved: empty(),
|
||||
assignments,
|
||||
text,
|
||||
recipes,
|
||||
};
|
||||
|
||||
@ -48,19 +45,19 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
|
||||
resolver.resolve_function(function, argc)?;
|
||||
}
|
||||
for variable in expression.variables() {
|
||||
resolver.resolve_variable(variable, &[])?;
|
||||
resolver.resolve_variable(&variable, &[])?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for line in &recipe.lines {
|
||||
for fragment in line {
|
||||
if let Fragment::Expression { ref expression, .. } = *fragment {
|
||||
for line in &recipe.body {
|
||||
for fragment in &line.fragments {
|
||||
if let Fragment::Interpolation { expression, .. } = fragment {
|
||||
for (function, argc) in expression.functions() {
|
||||
resolver.resolve_function(function, argc)?;
|
||||
}
|
||||
for variable in expression.variables() {
|
||||
resolver.resolve_variable(variable, &recipe.parameters)?;
|
||||
resolver.resolve_variable(&variable, &recipe.parameters)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -70,27 +67,27 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_function(&self, function: &Token, argc: usize) -> CompilationResult<'a, ()> {
|
||||
Function::resolve(function, argc).map_err(|error| CompilationError {
|
||||
fn resolve_function(&self, function: Token<'a>, argc: usize) -> CompilationResult<'a, ()> {
|
||||
Function::resolve(&function, argc).map_err(|error| CompilationError {
|
||||
offset: error.offset,
|
||||
line: error.line,
|
||||
column: error.column,
|
||||
width: error.width,
|
||||
kind: UnknownFunction {
|
||||
function: &self.text[error.offset..error.offset + error.width],
|
||||
function: &function.src[error.offset..error.offset + error.width],
|
||||
},
|
||||
text: self.text,
|
||||
src: function.src,
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_variable(
|
||||
&self,
|
||||
variable: &Token,
|
||||
variable: &Token<'a>,
|
||||
parameters: &[Parameter],
|
||||
) -> CompilationResult<'a, ()> {
|
||||
let name = variable.lexeme();
|
||||
let undefined =
|
||||
!self.assignments.contains_key(name) && !parameters.iter().any(|p| p.name == name);
|
||||
!self.assignments.contains_key(name) && !parameters.iter().any(|p| p.name.lexeme() == name);
|
||||
if undefined {
|
||||
let error = variable.error(UndefinedVariable { variable: name });
|
||||
return Err(CompilationError {
|
||||
@ -99,9 +96,9 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
|
||||
column: error.column,
|
||||
width: error.width,
|
||||
kind: UndefinedVariable {
|
||||
variable: &self.text[error.offset..error.offset + error.width],
|
||||
variable: &variable.src[error.offset..error.offset + error.width],
|
||||
},
|
||||
text: self.text,
|
||||
src: variable.src,
|
||||
});
|
||||
}
|
||||
|
||||
@ -109,25 +106,29 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
|
||||
}
|
||||
|
||||
fn resolve_recipe(&mut self, recipe: &Recipe<'a>) -> CompilationResult<'a, ()> {
|
||||
if self.resolved.contains(recipe.name) {
|
||||
if self.resolved.contains(recipe.name()) {
|
||||
return Ok(());
|
||||
}
|
||||
self.stack.push(recipe.name);
|
||||
self.seen.insert(recipe.name);
|
||||
for dependency_token in &recipe.dependency_tokens {
|
||||
self.stack.push(recipe.name());
|
||||
self.seen.insert(recipe.name());
|
||||
for dependency_token in recipe
|
||||
.dependencies
|
||||
.iter()
|
||||
.map(|dependency| dependency.token())
|
||||
{
|
||||
match self.recipes.get(dependency_token.lexeme()) {
|
||||
Some(dependency) => {
|
||||
if !self.resolved.contains(dependency.name) {
|
||||
if self.seen.contains(dependency.name) {
|
||||
if !self.resolved.contains(dependency.name()) {
|
||||
if self.seen.contains(dependency.name()) {
|
||||
let first = self.stack[0];
|
||||
self.stack.push(first);
|
||||
return Err(
|
||||
dependency_token.error(CircularRecipeDependency {
|
||||
recipe: recipe.name,
|
||||
recipe: recipe.name(),
|
||||
circle: self
|
||||
.stack
|
||||
.iter()
|
||||
.skip_while(|name| **name != dependency.name)
|
||||
.skip_while(|name| **name != dependency.name())
|
||||
.cloned()
|
||||
.collect(),
|
||||
}),
|
||||
@ -138,23 +139,23 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
|
||||
}
|
||||
None => {
|
||||
return Err(dependency_token.error(UnknownDependency {
|
||||
recipe: recipe.name,
|
||||
recipe: recipe.name(),
|
||||
unknown: dependency_token.lexeme(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.resolved.insert(recipe.name);
|
||||
self.resolved.insert(recipe.name());
|
||||
self.stack.pop();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
error_test! {
|
||||
analysis_error! {
|
||||
name: circular_recipe_dependency,
|
||||
input: "a: b\nb: a",
|
||||
offset: 8,
|
||||
@ -164,7 +165,7 @@ mod test {
|
||||
kind: CircularRecipeDependency{recipe: "b", circle: vec!["a", "b", "a"]},
|
||||
}
|
||||
|
||||
error_test! {
|
||||
analysis_error! {
|
||||
name: self_recipe_dependency,
|
||||
input: "a: a",
|
||||
offset: 3,
|
||||
@ -174,7 +175,7 @@ mod test {
|
||||
kind: CircularRecipeDependency{recipe: "a", circle: vec!["a", "a"]},
|
||||
}
|
||||
|
||||
error_test! {
|
||||
analysis_error! {
|
||||
name: unknown_dependency,
|
||||
input: "a: b",
|
||||
offset: 3,
|
||||
@ -184,7 +185,7 @@ mod test {
|
||||
kind: UnknownDependency{recipe: "a", unknown: "b"},
|
||||
}
|
||||
|
||||
error_test! {
|
||||
analysis_error! {
|
||||
name: unknown_interpolation_variable,
|
||||
input: "x:\n {{ hello}}",
|
||||
offset: 9,
|
||||
@ -194,7 +195,7 @@ mod test {
|
||||
kind: UndefinedVariable{variable: "hello"},
|
||||
}
|
||||
|
||||
error_test! {
|
||||
analysis_error! {
|
||||
name: unknown_second_interpolation_variable,
|
||||
input: "wtf=\"x\"\nx:\n echo\n foo {{wtf}} {{ lol }}",
|
||||
offset: 33,
|
||||
@ -204,7 +205,7 @@ mod test {
|
||||
kind: UndefinedVariable{variable: "lol"},
|
||||
}
|
||||
|
||||
error_test! {
|
||||
analysis_error! {
|
||||
name: unknown_function_in_interpolation,
|
||||
input: "a:\n echo {{bar()}}",
|
||||
offset: 11,
|
||||
@ -214,7 +215,7 @@ mod test {
|
||||
kind: UnknownFunction{function: "bar"},
|
||||
}
|
||||
|
||||
error_test! {
|
||||
analysis_error! {
|
||||
name: unknown_function_in_default,
|
||||
input: "a f=baz():",
|
||||
offset: 4,
|
||||
@ -224,7 +225,7 @@ mod test {
|
||||
kind: UnknownFunction{function: "baz"},
|
||||
}
|
||||
|
||||
error_test! {
|
||||
analysis_error! {
|
||||
name: unknown_variable_in_default,
|
||||
input: "a f=foo:",
|
||||
offset: 4,
|
||||
|
16
src/run.rs
16
src/run.rs
@ -131,7 +131,7 @@ pub fn run() -> Result<(), i32> {
|
||||
}
|
||||
}
|
||||
|
||||
let justfile = match Parser::parse(&text) {
|
||||
let justfile = match Compiler::compile(&text) {
|
||||
Err(error) => {
|
||||
if config.color.stderr().active() {
|
||||
eprintln!("{:#}", error);
|
||||
@ -177,15 +177,15 @@ pub fn run() -> Result<(), i32> {
|
||||
// Construct a target to alias map.
|
||||
let mut recipe_aliases: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
|
||||
for alias in justfile.aliases.values() {
|
||||
if alias.private {
|
||||
if alias.is_private() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !recipe_aliases.contains_key(alias.target) {
|
||||
recipe_aliases.insert(alias.target, vec![alias.name]);
|
||||
if !recipe_aliases.contains_key(alias.target.lexeme()) {
|
||||
recipe_aliases.insert(alias.target.lexeme(), vec![alias.name.lexeme()]);
|
||||
} else {
|
||||
let aliases = recipe_aliases.get_mut(alias.target).unwrap();
|
||||
aliases.push(alias.name);
|
||||
let aliases = recipe_aliases.get_mut(alias.target.lexeme()).unwrap();
|
||||
aliases.push(alias.name.lexeme());
|
||||
}
|
||||
}
|
||||
|
||||
@ -262,7 +262,7 @@ pub fn run() -> Result<(), i32> {
|
||||
|
||||
if let Subcommand::Show { name } = config.subcommand {
|
||||
if let Some(alias) = justfile.get_alias(name) {
|
||||
let recipe = justfile.get_recipe(alias.target).unwrap();
|
||||
let recipe = justfile.get_recipe(alias.target.lexeme()).unwrap();
|
||||
println!("{}", alias);
|
||||
println!("{}", recipe);
|
||||
return Ok(());
|
||||
@ -291,7 +291,7 @@ pub fn run() -> Result<(), i32> {
|
||||
Count("argument", min_arguments),
|
||||
);
|
||||
}
|
||||
vec![recipe.name]
|
||||
vec![recipe.name()]
|
||||
} else {
|
||||
die!("Justfile contains no recipes.");
|
||||
};
|
||||
|
@ -26,7 +26,7 @@ pub(crate) enum RuntimeError<'a> {
|
||||
dotenv_error: dotenv::Error,
|
||||
},
|
||||
FunctionCall {
|
||||
token: Token<'a>,
|
||||
function: Name<'a>,
|
||||
message: String,
|
||||
},
|
||||
Internal {
|
||||
@ -91,7 +91,7 @@ impl<'a> Display for RuntimeError<'a> {
|
||||
let message = color.message();
|
||||
write!(f, "{} {}", error.paint("error:"), message.prefix())?;
|
||||
|
||||
let mut error_token = None;
|
||||
let mut error_token: Option<Token> = None;
|
||||
|
||||
match *self {
|
||||
UnknownRecipes {
|
||||
@ -235,16 +235,16 @@ impl<'a> Display for RuntimeError<'a> {
|
||||
writeln!(f, "Failed to load .env: {}", dotenv_error)?;
|
||||
}
|
||||
FunctionCall {
|
||||
ref token,
|
||||
ref function,
|
||||
ref message,
|
||||
} => {
|
||||
writeln!(
|
||||
f,
|
||||
"Call to function `{}` failed: {}",
|
||||
token.lexeme(),
|
||||
function.lexeme(),
|
||||
message
|
||||
)?;
|
||||
error_token = Some(token);
|
||||
error_token = Some(function.token());
|
||||
}
|
||||
Shebang {
|
||||
recipe,
|
||||
@ -332,15 +332,15 @@ impl<'a> Display for RuntimeError<'a> {
|
||||
} => match *output_error {
|
||||
OutputError::Code(code) => {
|
||||
writeln!(f, "Backtick failed with exit code {}", code)?;
|
||||
error_token = Some(token);
|
||||
error_token = Some(*token);
|
||||
}
|
||||
OutputError::Signal(signal) => {
|
||||
writeln!(f, "Backtick was terminated by signal {}", signal)?;
|
||||
error_token = Some(token);
|
||||
error_token = Some(*token);
|
||||
}
|
||||
OutputError::Unknown => {
|
||||
writeln!(f, "Backtick failed for an unknown reason")?;
|
||||
error_token = Some(token);
|
||||
error_token = Some(*token);
|
||||
}
|
||||
OutputError::Io(ref io_error) => {
|
||||
match io_error.kind() {
|
||||
@ -361,7 +361,7 @@ impl<'a> Display for RuntimeError<'a> {
|
||||
io_error
|
||||
),
|
||||
}?;
|
||||
error_token = Some(token);
|
||||
error_token = Some(*token);
|
||||
}
|
||||
OutputError::Utf8(ref utf8_error) => {
|
||||
writeln!(
|
||||
@ -369,7 +369,7 @@ impl<'a> Display for RuntimeError<'a> {
|
||||
"Backtick succeeded but stdout was not utf8: {}",
|
||||
utf8_error
|
||||
)?;
|
||||
error_token = Some(token);
|
||||
error_token = Some(*token);
|
||||
}
|
||||
},
|
||||
Internal { ref message } => {
|
||||
@ -388,7 +388,7 @@ impl<'a> Display for RuntimeError<'a> {
|
||||
write_message_context(
|
||||
f,
|
||||
Color::fmt(f).error(),
|
||||
token.text,
|
||||
token.src,
|
||||
token.offset,
|
||||
token.line,
|
||||
token.column,
|
||||
|
@ -31,7 +31,7 @@ impl<'a> Shebang<'a> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
mod tests {
|
||||
use super::Shebang;
|
||||
|
||||
#[test]
|
||||
|
@ -1,59 +1,12 @@
|
||||
use crate::common::*;
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub(crate) struct StringLiteral<'a> {
|
||||
pub(crate) raw: &'a str,
|
||||
pub(crate) cooked: Cow<'a, str>,
|
||||
pub(crate) struct StringLiteral<'src> {
|
||||
pub(crate) raw: &'src str,
|
||||
pub(crate) cooked: Cow<'src, str>,
|
||||
}
|
||||
|
||||
impl<'a> StringLiteral<'a> {
|
||||
pub(crate) fn new(token: &Token<'a>) -> CompilationResult<'a, StringLiteral<'a>> {
|
||||
let raw = &token.lexeme()[1..token.lexeme().len() - 1];
|
||||
|
||||
if let TokenKind::StringRaw = token.kind {
|
||||
Ok(StringLiteral {
|
||||
cooked: Cow::Borrowed(raw),
|
||||
raw,
|
||||
})
|
||||
} else if let TokenKind::StringCooked = token.kind {
|
||||
let mut cooked = String::new();
|
||||
let mut escape = false;
|
||||
for c in raw.chars() {
|
||||
if escape {
|
||||
match c {
|
||||
'n' => cooked.push('\n'),
|
||||
'r' => cooked.push('\r'),
|
||||
't' => cooked.push('\t'),
|
||||
'\\' => cooked.push('\\'),
|
||||
'"' => cooked.push('"'),
|
||||
other => {
|
||||
return Err(
|
||||
token.error(CompilationErrorKind::InvalidEscapeSequence { character: other }),
|
||||
);
|
||||
}
|
||||
}
|
||||
escape = false;
|
||||
continue;
|
||||
}
|
||||
if c == '\\' {
|
||||
escape = true;
|
||||
continue;
|
||||
}
|
||||
cooked.push(c);
|
||||
}
|
||||
Ok(StringLiteral {
|
||||
raw,
|
||||
cooked: Cow::Owned(cooked),
|
||||
})
|
||||
} else {
|
||||
Err(token.error(CompilationErrorKind::Internal {
|
||||
message: "cook_string() called on non-string token".to_string(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Display for StringLiteral<'a> {
|
||||
impl Display for StringLiteral<'_> {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match self.cooked {
|
||||
Cow::Borrowed(raw) => write!(f, "'{}'", raw),
|
||||
|
@ -18,12 +18,19 @@ use std::{
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use crate::{expression, fragment, justfile::Justfile, parameter, parser::Parser, recipe};
|
||||
use crate::compiler::Compiler;
|
||||
|
||||
mod full {
|
||||
pub(crate) use crate::{
|
||||
assignment::Assignment, expression::Expression, fragment::Fragment, justfile::Justfile,
|
||||
line::Line, parameter::Parameter, recipe::Recipe,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn summary(path: &Path) -> Result<Result<Summary, String>, io::Error> {
|
||||
let text = fs::read_to_string(path)?;
|
||||
|
||||
match Parser::parse(&text) {
|
||||
match Compiler::compile(&text) {
|
||||
Ok(justfile) => Ok(Ok(Summary::new(justfile))),
|
||||
Err(compilation_error) => Ok(Err(compilation_error.to_string())),
|
||||
}
|
||||
@ -36,14 +43,12 @@ pub struct Summary {
|
||||
}
|
||||
|
||||
impl Summary {
|
||||
fn new(justfile: Justfile) -> Summary {
|
||||
let exports = justfile.exports;
|
||||
|
||||
fn new(justfile: full::Justfile) -> Summary {
|
||||
let mut aliases = BTreeMap::new();
|
||||
|
||||
for alias in justfile.aliases.values() {
|
||||
aliases
|
||||
.entry(alias.target)
|
||||
.entry(alias.target.lexeme())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(alias.name.to_string());
|
||||
}
|
||||
@ -62,12 +67,7 @@ impl Summary {
|
||||
assignments: justfile
|
||||
.assignments
|
||||
.into_iter()
|
||||
.map(|(name, expression)| {
|
||||
(
|
||||
name.to_string(),
|
||||
Assignment::new(name, expression, &exports),
|
||||
)
|
||||
})
|
||||
.map(|(name, assignment)| (name.to_string(), Assignment::new(assignment)))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
@ -85,13 +85,17 @@ pub struct Recipe {
|
||||
}
|
||||
|
||||
impl Recipe {
|
||||
fn new(recipe: recipe::Recipe, aliases: Vec<String>) -> Recipe {
|
||||
fn new(recipe: full::Recipe, aliases: Vec<String>) -> Recipe {
|
||||
Recipe {
|
||||
private: recipe.private,
|
||||
shebang: recipe.shebang,
|
||||
quiet: recipe.quiet,
|
||||
dependencies: recipe.dependencies.into_iter().map(str::to_owned).collect(),
|
||||
lines: recipe.lines.into_iter().map(Line::new).collect(),
|
||||
dependencies: recipe
|
||||
.dependencies
|
||||
.into_iter()
|
||||
.map(|name| name.lexeme().to_string())
|
||||
.collect(),
|
||||
lines: recipe.body.into_iter().map(Line::new).collect(),
|
||||
parameters: recipe.parameters.into_iter().map(Parameter::new).collect(),
|
||||
aliases,
|
||||
}
|
||||
@ -106,10 +110,10 @@ pub struct Parameter {
|
||||
}
|
||||
|
||||
impl Parameter {
|
||||
fn new(parameter: parameter::Parameter) -> Parameter {
|
||||
fn new(parameter: full::Parameter) -> Parameter {
|
||||
Parameter {
|
||||
variadic: parameter.variadic,
|
||||
name: parameter.name.to_owned(),
|
||||
name: parameter.name.lexeme().to_owned(),
|
||||
default: parameter.default.map(Expression::new),
|
||||
}
|
||||
}
|
||||
@ -121,9 +125,9 @@ pub struct Line {
|
||||
}
|
||||
|
||||
impl Line {
|
||||
fn new(fragments: Vec<fragment::Fragment>) -> Line {
|
||||
fn new(line: full::Line) -> Line {
|
||||
Line {
|
||||
fragments: fragments.into_iter().map(Fragment::new).collect(),
|
||||
fragments: line.fragments.into_iter().map(Fragment::new).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -135,12 +139,12 @@ pub enum Fragment {
|
||||
}
|
||||
|
||||
impl Fragment {
|
||||
fn new(fragment: fragment::Fragment) -> Fragment {
|
||||
fn new(fragment: full::Fragment) -> Fragment {
|
||||
match fragment {
|
||||
fragment::Fragment::Text { text } => Fragment::Text {
|
||||
text: text.lexeme().to_owned(),
|
||||
full::Fragment::Text { token } => Fragment::Text {
|
||||
text: token.lexeme().to_owned(),
|
||||
},
|
||||
fragment::Fragment::Expression { expression } => Fragment::Expression {
|
||||
full::Fragment::Interpolation { expression } => Fragment::Expression {
|
||||
expression: Expression::new(expression),
|
||||
},
|
||||
}
|
||||
@ -154,10 +158,10 @@ pub struct Assignment {
|
||||
}
|
||||
|
||||
impl Assignment {
|
||||
fn new(name: &str, expression: expression::Expression, exports: &BTreeSet<&str>) -> Assignment {
|
||||
fn new(assignment: full::Assignment) -> Assignment {
|
||||
Assignment {
|
||||
exported: exports.contains(name),
|
||||
expression: Expression::new(expression),
|
||||
exported: assignment.export,
|
||||
expression: Expression::new(assignment.expression),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -184,29 +188,30 @@ pub enum Expression {
|
||||
}
|
||||
|
||||
impl Expression {
|
||||
fn new(expression: expression::Expression) -> Expression {
|
||||
use expression::Expression::*;
|
||||
fn new(expression: full::Expression) -> Expression {
|
||||
use full::Expression::*;
|
||||
match expression {
|
||||
Backtick { raw, .. } => Expression::Backtick {
|
||||
command: raw.to_owned(),
|
||||
Backtick { contents, .. } => Expression::Backtick {
|
||||
command: contents.to_owned(),
|
||||
},
|
||||
Call {
|
||||
name, arguments, ..
|
||||
function,
|
||||
arguments,
|
||||
} => Expression::Call {
|
||||
name: name.to_owned(),
|
||||
name: function.lexeme().to_owned(),
|
||||
arguments: arguments.into_iter().map(Expression::new).collect(),
|
||||
},
|
||||
Concatination { lhs, rhs } => Expression::Concatination {
|
||||
lhs: Box::new(Expression::new(*lhs)),
|
||||
rhs: Box::new(Expression::new(*rhs)),
|
||||
},
|
||||
String { cooked_string } => Expression::String {
|
||||
text: cooked_string.cooked.to_string(),
|
||||
StringLiteral { string_literal } => Expression::String {
|
||||
text: string_literal.cooked.to_string(),
|
||||
},
|
||||
Variable { name, .. } => Expression::Variable {
|
||||
name: name.to_owned(),
|
||||
name: name.lexeme().to_owned(),
|
||||
},
|
||||
Group { expression } => Expression::new(*expression),
|
||||
Group { contents } => Expression::new(*contents),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
46
src/table.rs
Normal file
46
src/table.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use crate::common::*;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct Table<'key, V: Keyed<'key>> {
|
||||
map: BTreeMap<&'key str, V>,
|
||||
}
|
||||
|
||||
impl<'key, V: Keyed<'key>> Table<'key, V> {
|
||||
pub(crate) fn insert(&mut self, value: V) {
|
||||
self.map.insert(value.key(), value);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'key, V: Keyed<'key>> FromIterator<V> for Table<'key, V> {
|
||||
fn from_iter<I: IntoIterator<Item = V>>(iter: I) -> Self {
|
||||
Table {
|
||||
map: iter.into_iter().map(|value| (value.key(), value)).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'key, V: Keyed<'key>> Deref for Table<'key, V> {
|
||||
type Target = BTreeMap<&'key str, V>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.map
|
||||
}
|
||||
}
|
||||
|
||||
impl<'key, V: Keyed<'key>> IntoIterator for Table<'key, V> {
|
||||
type Item = (&'key str, V);
|
||||
type IntoIter = std::collections::btree_map::IntoIter<&'key str, V>;
|
||||
|
||||
fn into_iter(self) -> std::collections::btree_map::IntoIter<&'key str, V> {
|
||||
self.map.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'table, V: Keyed<'table> + 'table> IntoIterator for &'table Table<'table, V> {
|
||||
type Item = (&'table &'table str, &'table V);
|
||||
type IntoIter = std::collections::btree_map::Iter<'table, &'table str, V>;
|
||||
|
||||
fn into_iter(self) -> std::collections::btree_map::Iter<'table, &'table str, V> {
|
||||
self.map.iter()
|
||||
}
|
||||
}
|
@ -1,15 +1,15 @@
|
||||
use crate::common::*;
|
||||
|
||||
pub(crate) fn parse(text: &str) -> Justfile {
|
||||
match Parser::parse(text) {
|
||||
pub(crate) fn compile(text: &str) -> Justfile {
|
||||
match Compiler::compile(text) {
|
||||
Ok(justfile) => justfile,
|
||||
Err(error) => panic!("Expected successful parse but got error:\n {}", error),
|
||||
Err(error) => panic!("Expected successful compilation but got error:\n {}", error),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) use test_utilities::{tempdir, unindent};
|
||||
|
||||
macro_rules! error_test {
|
||||
macro_rules! analysis_error {
|
||||
(
|
||||
name: $name:ident,
|
||||
input: $input:expr,
|
||||
@ -21,15 +21,21 @@ macro_rules! error_test {
|
||||
) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let text: &str = $input;
|
||||
let offset: usize = $offset;
|
||||
let column: usize = $column;
|
||||
let width: usize = $width;
|
||||
let line: usize = $line;
|
||||
let kind: CompilationErrorKind = $kind;
|
||||
$crate::testing::error($input, $offset, $line, $column, $width, $kind);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn error(
|
||||
src: &str,
|
||||
offset: usize,
|
||||
line: usize,
|
||||
column: usize,
|
||||
width: usize,
|
||||
kind: CompilationErrorKind,
|
||||
) {
|
||||
let expected = CompilationError {
|
||||
text,
|
||||
src,
|
||||
offset,
|
||||
line,
|
||||
column,
|
||||
@ -37,13 +43,39 @@ macro_rules! error_test {
|
||||
kind,
|
||||
};
|
||||
|
||||
match Parser::parse(text) {
|
||||
Ok(_) => panic!("Compilation succeeded but expected: {}\n{}", expected, text),
|
||||
let tokens = Lexer::lex(src).expect("Lexing failed in parse test...");
|
||||
|
||||
let module = Parser::parse(&tokens).expect("Parsing failed in analysis test...");
|
||||
|
||||
match Analyzer::analyze(module) {
|
||||
Ok(_) => panic!("Analysis succeeded but expected: {}\n{}", expected, src),
|
||||
Err(actual) => {
|
||||
use pretty_assertions::assert_eq;
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readme_test() {
|
||||
let mut justfiles = vec![];
|
||||
let mut current = None;
|
||||
|
||||
for line in fs::read_to_string("README.adoc").unwrap().lines() {
|
||||
if let Some(mut justfile) = current {
|
||||
if line == "```" {
|
||||
justfiles.push(justfile);
|
||||
current = None;
|
||||
} else {
|
||||
justfile += line;
|
||||
justfile += "\n";
|
||||
current = Some(justfile);
|
||||
}
|
||||
} else if line == "```make" {
|
||||
current = Some(String::new());
|
||||
}
|
||||
}
|
||||
|
||||
for justfile in justfiles {
|
||||
compile(&justfile);
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,18 @@
|
||||
use crate::common::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub(crate) struct Token<'a> {
|
||||
pub(crate) offset: usize,
|
||||
pub(crate) length: usize,
|
||||
pub(crate) line: usize,
|
||||
pub(crate) column: usize,
|
||||
pub(crate) text: &'a str,
|
||||
pub(crate) src: &'a str,
|
||||
pub(crate) kind: TokenKind,
|
||||
}
|
||||
|
||||
impl<'a> Token<'a> {
|
||||
pub(crate) fn lexeme(&self) -> &'a str {
|
||||
&self.text[self.offset..self.offset + self.length]
|
||||
&self.src[self.offset..self.offset + self.length]
|
||||
}
|
||||
|
||||
pub(crate) fn error(&self, kind: CompilationErrorKind<'a>) -> CompilationError<'a> {
|
||||
@ -20,7 +20,7 @@ impl<'a> Token<'a> {
|
||||
column: self.column,
|
||||
offset: self.offset,
|
||||
line: self.line,
|
||||
text: self.text,
|
||||
src: self.src,
|
||||
width: self.length,
|
||||
kind,
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::common::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Ord, PartialOrd, Eq)]
|
||||
pub(crate) enum TokenKind {
|
||||
At,
|
||||
Backtick,
|
||||
@ -12,16 +12,15 @@ pub(crate) enum TokenKind {
|
||||
Eof,
|
||||
Eol,
|
||||
Equals,
|
||||
Identifier,
|
||||
Indent,
|
||||
InterpolationEnd,
|
||||
InterpolationStart,
|
||||
Line,
|
||||
Name,
|
||||
ParenL,
|
||||
ParenR,
|
||||
Plus,
|
||||
StringRaw,
|
||||
StringCooked,
|
||||
StringRaw,
|
||||
Text,
|
||||
Whitespace,
|
||||
}
|
||||
@ -43,16 +42,15 @@ impl Display for TokenKind {
|
||||
Eof => "end of file",
|
||||
Eol => "end of line",
|
||||
Equals => "'='",
|
||||
Identifier => "identifier",
|
||||
Indent => "indent",
|
||||
InterpolationEnd => "'}}'",
|
||||
InterpolationStart => "'{{'",
|
||||
Line => "command",
|
||||
Name => "name",
|
||||
ParenL => "'('",
|
||||
ParenR => "')'",
|
||||
Plus => "'+'",
|
||||
StringRaw => "raw string",
|
||||
StringCooked => "cooked string",
|
||||
StringRaw => "raw string",
|
||||
Text => "command text",
|
||||
Whitespace => "whitespace",
|
||||
}
|
||||
|
130
src/tree.rs
Normal file
130
src/tree.rs
Normal file
@ -0,0 +1,130 @@
|
||||
use crate::common::*;
|
||||
|
||||
use std::mem;
|
||||
|
||||
/// Construct a `Tree` from a symbolic expression literal. This macro, and the
|
||||
/// Tree type, are only used in the Parser unit tests, as a concise notation
|
||||
/// representing the expected results of parsing a given string.
|
||||
macro_rules! tree {
|
||||
{
|
||||
($($child:tt)*)
|
||||
} => {
|
||||
$crate::tree::Tree::List(vec![$(tree!($child),)*])
|
||||
};
|
||||
|
||||
{
|
||||
$atom:ident
|
||||
} => {
|
||||
$crate::tree::Tree::atom(stringify!($atom))
|
||||
};
|
||||
|
||||
{
|
||||
$atom:literal
|
||||
} => {
|
||||
$crate::tree::Tree::atom(format!("\"{}\"", $atom))
|
||||
};
|
||||
|
||||
{
|
||||
#
|
||||
} => {
|
||||
$crate::tree::Tree::atom("#")
|
||||
};
|
||||
|
||||
{
|
||||
+
|
||||
} => {
|
||||
$crate::tree::Tree::atom("+")
|
||||
};
|
||||
}
|
||||
|
||||
/// A `Tree` is either…
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) enum Tree<'text> {
|
||||
/// …an atom containing text, or…
|
||||
Atom(Cow<'text, str>),
|
||||
/// …a list containing zero or more `Tree`s.
|
||||
List(Vec<Tree<'text>>),
|
||||
}
|
||||
|
||||
impl<'text> Tree<'text> {
|
||||
/// Construct an Atom from a text scalar
|
||||
pub(crate) fn atom(text: impl Into<Cow<'text, str>>) -> Tree<'text> {
|
||||
Tree::Atom(text.into())
|
||||
}
|
||||
|
||||
/// Construct a List from an iterable of trees
|
||||
pub(crate) fn list(children: impl IntoIterator<Item = Tree<'text>>) -> Tree<'text> {
|
||||
Tree::List(children.into_iter().collect())
|
||||
}
|
||||
|
||||
/// Convenience function to create an atom containing quoted text
|
||||
pub(crate) fn string(contents: impl AsRef<str>) -> Tree<'text> {
|
||||
Tree::atom(format!("\"{}\"", contents.as_ref()))
|
||||
}
|
||||
|
||||
/// Push a child node into self, turning it into a List if it was an Atom
|
||||
pub(crate) fn push(self, tree: impl Into<Tree<'text>>) -> Tree<'text> {
|
||||
match self {
|
||||
Tree::List(mut children) => {
|
||||
children.push(tree.into());
|
||||
Tree::List(children)
|
||||
}
|
||||
Tree::Atom(text) => Tree::List(vec![Tree::Atom(text), tree.into()]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extend a self with a tail of Trees, turning self into a List if it
|
||||
/// was an Atom
|
||||
pub(crate) fn extend<I, T>(self, tail: I) -> Tree<'text>
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<Tree<'text>>,
|
||||
{
|
||||
// Tree::List(children.into_iter().collect())
|
||||
let mut head = match self {
|
||||
Tree::List(children) => children,
|
||||
Tree::Atom(text) => vec![Tree::Atom(text)],
|
||||
};
|
||||
|
||||
for child in tail {
|
||||
head.push(child.into());
|
||||
}
|
||||
|
||||
Tree::List(head)
|
||||
}
|
||||
|
||||
/// Like `push`, but modify self in-place
|
||||
pub(crate) fn push_mut(&mut self, tree: impl Into<Tree<'text>>) {
|
||||
let tree = mem::replace(self, Tree::List(Vec::new())).push(tree.into());
|
||||
mem::replace(self, tree);
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Tree<'_> {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Tree::List(children) => {
|
||||
write!(f, "(")?;
|
||||
|
||||
for (i, child) in children.iter().enumerate() {
|
||||
if i > 0 {
|
||||
write!(f, " ")?;
|
||||
}
|
||||
write!(f, "{}", child)?;
|
||||
}
|
||||
|
||||
write!(f, ")")
|
||||
}
|
||||
Tree::Atom(text) => write!(f, "{}", text),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'text, T> From<T> for Tree<'text>
|
||||
where
|
||||
T: Into<Cow<'text, str>>,
|
||||
{
|
||||
fn from(text: T) -> Tree<'text> {
|
||||
Tree::Atom(text.into())
|
||||
}
|
||||
}
|
@ -1,32 +1,32 @@
|
||||
use crate::common::*;
|
||||
|
||||
pub(crate) struct Variables<'a> {
|
||||
stack: Vec<&'a Expression<'a>>,
|
||||
pub(crate) struct Variables<'expression, 'src> {
|
||||
stack: Vec<&'expression Expression<'src>>,
|
||||
}
|
||||
|
||||
impl<'a> Variables<'a> {
|
||||
pub(crate) fn new(root: &'a Expression<'a>) -> Variables<'a> {
|
||||
impl<'expression, 'src> Variables<'expression, 'src> {
|
||||
pub(crate) fn new(root: &'expression Expression<'src>) -> Variables<'expression, 'src> {
|
||||
Variables { stack: vec![root] }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Variables<'a> {
|
||||
type Item = &'a Token<'a>;
|
||||
impl<'expression, 'src> Iterator for Variables<'expression, 'src> {
|
||||
type Item = Token<'src>;
|
||||
|
||||
fn next(&mut self) -> Option<&'a Token<'a>> {
|
||||
fn next(&mut self) -> Option<Token<'src>> {
|
||||
match self.stack.pop() {
|
||||
None
|
||||
| Some(Expression::String { .. })
|
||||
| Some(Expression::StringLiteral { .. })
|
||||
| Some(Expression::Backtick { .. })
|
||||
| Some(Expression::Call { .. }) => None,
|
||||
Some(Expression::Variable { token, .. }) => Some(token),
|
||||
Some(Expression::Variable { name, .. }) => Some(name.token()),
|
||||
Some(Expression::Concatination { lhs, rhs }) => {
|
||||
self.stack.push(lhs);
|
||||
self.stack.push(rhs);
|
||||
self.next()
|
||||
}
|
||||
Some(Expression::Group { expression }) => {
|
||||
self.stack.push(expression);
|
||||
Some(Expression::Group { contents }) => {
|
||||
self.stack.push(contents);
|
||||
self.next()
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,13 @@ use crate::common::*;
|
||||
|
||||
use Warning::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Warning<'a> {
|
||||
DeprecatedEquals { equals: Token<'a> },
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) enum Warning<'src> {
|
||||
DeprecatedEquals { equals: Token<'src> },
|
||||
}
|
||||
|
||||
impl Warning<'_> {
|
||||
fn context(&self) -> Option<&Token> {
|
||||
impl<'src> Warning<'src> {
|
||||
fn context(&self) -> Option<&Token<'src>> {
|
||||
match self {
|
||||
DeprecatedEquals { equals } => Some(equals),
|
||||
}
|
||||
@ -42,7 +42,7 @@ impl Display for Warning<'_> {
|
||||
write_message_context(
|
||||
f,
|
||||
Color::fmt(f).warning(),
|
||||
token.text,
|
||||
token.src,
|
||||
token.offset,
|
||||
token.line,
|
||||
token.column,
|
||||
|
@ -1503,7 +1503,7 @@ integration_test! {
|
||||
justfile: "foo: 'bar'",
|
||||
args: ("foo"),
|
||||
stdout: "",
|
||||
stderr: "error: Expected name, end of line, or end of file, but found raw string
|
||||
stderr: "error: Expected end of file, end of line, or identifier, but found raw string
|
||||
|
|
||||
1 | foo: 'bar'
|
||||
| ^^^^^
|
||||
@ -1516,7 +1516,7 @@ integration_test! {
|
||||
justfile: "foo 'bar'",
|
||||
args: ("foo"),
|
||||
stdout: "",
|
||||
stderr: "error: Expected name, '+', ':', or ':=', but found raw string
|
||||
stderr: "error: Expected ':', ':=', identifier, or '+', but found raw string
|
||||
|
|
||||
1 | foo 'bar'
|
||||
| ^^^^^
|
||||
|
Loading…
Reference in New Issue
Block a user