//! Justfile summary creation, for testing purposes only. //! //! The contents of this module are not bound by any stability guarantees. //! Breaking changes may be introduced at any time. //! //! The main entry point into this module is the `summary` function, which //! parses a justfile at a given path and produces a `Summary` object, //! which broadly captures the functionality of the parsed justfile, or //! an error message. //! //! This functionality is intended to be used with `janus`, a tool for //! ensuring that changes to just do not inadvertently break or //! change the interpretation of existing justfiles. use std::{collections::BTreeMap, fs, io, path::Path}; use crate::compiler::Compiler; mod full { pub(crate) use crate::{ assignment::Assignment, dependency::Dependency, expression::Expression, fragment::Fragment, justfile::Justfile, line::Line, parameter::Parameter, recipe::Recipe, thunk::Thunk, }; } pub fn summary(path: &Path) -> Result, io::Error> { let text = fs::read_to_string(path)?; match Compiler::compile(&text) { Ok(justfile) => Ok(Ok(Summary::new(justfile))), Err(compilation_error) => Ok(Err(compilation_error.to_string())), } } #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub struct Summary { pub assignments: BTreeMap, pub recipes: BTreeMap, } impl Summary { fn new(justfile: full::Justfile) -> Summary { let mut aliases = BTreeMap::new(); for alias in justfile.aliases.values() { aliases .entry(alias.target.name()) .or_insert_with(Vec::new) .push(alias.name.to_string()); } Summary { recipes: justfile .recipes .into_iter() .map(|(name, recipe)| { ( name.to_string(), Recipe::new(&recipe, aliases.remove(name).unwrap_or_default()), ) }) .collect(), assignments: justfile .assignments .iter() .map(|(name, assignment)| (name.to_string(), Assignment::new(assignment))) .collect(), } } } #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub struct Recipe { pub aliases: Vec, pub dependencies: Vec, pub lines: Vec, pub private: bool, pub quiet: bool, pub shebang: bool, pub parameters: Vec, } impl Recipe { fn new(recipe: &full::Recipe, aliases: Vec) -> Recipe { Recipe { private: recipe.private, shebang: recipe.shebang, quiet: recipe.quiet, dependencies: recipe .dependencies .iter() .map(|dependency| Dependency::new(dependency)) .collect(), lines: recipe.body.iter().map(Line::new).collect(), parameters: recipe.parameters.iter().map(Parameter::new).collect(), aliases, } } } #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub struct Parameter { pub variadic: bool, pub name: String, pub default: Option, } impl Parameter { fn new(parameter: &full::Parameter) -> Parameter { Parameter { variadic: parameter.variadic, name: parameter.name.lexeme().to_owned(), default: parameter.default.as_ref().map(Expression::new), } } } #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub struct Line { pub fragments: Vec, } impl Line { fn new(line: &full::Line) -> Line { Line { fragments: line.fragments.iter().map(Fragment::new).collect(), } } } #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub enum Fragment { Text { text: String }, Expression { expression: Expression }, } impl Fragment { fn new(fragment: &full::Fragment) -> Fragment { match fragment { full::Fragment::Text { token } => Fragment::Text { text: token.lexeme().to_owned(), }, full::Fragment::Interpolation { expression } => Fragment::Expression { expression: Expression::new(expression), }, } } } #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub struct Assignment { pub exported: bool, pub expression: Expression, } impl Assignment { fn new(assignment: &full::Assignment) -> Assignment { Assignment { exported: assignment.export, expression: Expression::new(&assignment.value), } } } #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub enum Expression { Backtick { command: String, }, Call { name: String, arguments: Vec, }, Concatination { lhs: Box, rhs: Box, }, String { text: String, }, Variable { name: String, }, } impl Expression { fn new(expression: &full::Expression) -> Expression { use full::Expression::*; match expression { Backtick { contents, .. } => Expression::Backtick { command: (*contents).to_owned(), }, Call { thunk } => match thunk { full::Thunk::Nullary { name, .. } => Expression::Call { name: name.lexeme().to_owned(), arguments: Vec::new(), }, full::Thunk::Unary { name, arg, .. } => Expression::Call { name: name.lexeme().to_owned(), arguments: vec![Expression::new(arg)], }, full::Thunk::Binary { name, args: [a, b], .. } => Expression::Call { name: name.lexeme().to_owned(), arguments: vec![Expression::new(a), Expression::new(b)], }, }, Concatination { lhs, rhs } => Expression::Concatination { lhs: Box::new(Expression::new(lhs)), rhs: Box::new(Expression::new(rhs)), }, StringLiteral { string_literal } => Expression::String { text: string_literal.cooked.to_string(), }, Variable { name, .. } => Expression::Variable { name: name.lexeme().to_owned(), }, Group { contents } => Expression::new(contents), } } } #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub struct Dependency { pub recipe: String, pub arguments: Vec, } impl Dependency { fn new(dependency: &full::Dependency) -> Dependency { Dependency { recipe: dependency.recipe.name().to_owned(), arguments: dependency.arguments.iter().map(Expression::new).collect(), } } }