//! 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 { crate::{compiler::Compiler, error::Error, loader::Loader}, std::{collections::BTreeMap, io, path::Path}, }; mod full { pub(crate) use crate::{ assignment::Assignment, condition::Condition, conditional_operator::ConditionalOperator, dependency::Dependency, expression::Expression, fragment::Fragment, justfile::Justfile, line::Line, parameter::Parameter, parameter_kind::ParameterKind, recipe::Recipe, thunk::Thunk, }; } pub fn summary(path: &Path) -> Result, io::Error> { let loader = Loader::new(); match Compiler::compile(false, &loader, path) { Ok(compilation) => Ok(Ok(Summary::new(&compilation.justfile))), Err(error) => Ok(Err(if let Error::Compile { compile_error } = error { compile_error.to_string() } else { format!("{error:?}") })), } } #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub struct Summary { pub assignments: BTreeMap, pub recipes: BTreeMap, } impl Summary { fn new(justfile: &full::Justfile) -> Self { 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()); } Self { recipes: justfile .recipes .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_owned(), 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) -> Self { Self { private: recipe.private, shebang: recipe.shebang, quiet: recipe.quiet, dependencies: recipe.dependencies.iter().map(Dependency::new).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 kind: ParameterKind, pub name: String, pub default: Option, } impl Parameter { fn new(parameter: &full::Parameter) -> Self { Self { kind: ParameterKind::new(parameter.kind), name: parameter.name.lexeme().to_owned(), default: parameter.default.as_ref().map(Expression::new), } } } #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] pub enum ParameterKind { Singular, Plus, Star, } impl ParameterKind { fn new(parameter_kind: full::ParameterKind) -> Self { match parameter_kind { full::ParameterKind::Singular => Self::Singular, full::ParameterKind::Plus => Self::Plus, full::ParameterKind::Star => Self::Star, } } } #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub struct Line { pub fragments: Vec, } impl Line { fn new(line: &full::Line) -> Self { Self { 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) -> Self { match fragment { full::Fragment::Text { token } => Self::Text { text: token.lexeme().to_owned(), }, full::Fragment::Interpolation { expression } => Self::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) -> Self { Self { exported: assignment.export, expression: Expression::new(&assignment.value), } } } #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub enum Expression { Assert { condition: Condition, error: Box, }, Backtick { command: String, }, Call { name: String, arguments: Vec, }, Concatenation { lhs: Box, rhs: Box, }, Conditional { lhs: Box, rhs: Box, then: Box, otherwise: Box, operator: ConditionalOperator, }, Join { lhs: Option>, rhs: Box, }, String { text: String, }, Variable { name: String, }, } impl Expression { fn new(expression: &full::Expression) -> Self { use full::Expression::*; match expression { Assert { condition: full::Condition { lhs, rhs, operator }, error, } => Expression::Assert { condition: Condition { lhs: Box::new(Expression::new(lhs)), rhs: Box::new(Expression::new(rhs)), operator: ConditionalOperator::new(*operator), }, error: Box::new(Expression::new(error)), }, Backtick { contents, .. } => Self::Backtick { command: (*contents).clone(), }, Call { thunk } => match thunk { full::Thunk::Nullary { name, .. } => Self::Call { name: name.lexeme().to_owned(), arguments: Vec::new(), }, full::Thunk::Unary { name, arg, .. } => Self::Call { name: name.lexeme().to_owned(), arguments: vec![Self::new(arg)], }, full::Thunk::UnaryOpt { name, args: (a, opt_b), .. } => { let mut arguments = Vec::new(); if let Some(b) = opt_b.as_ref() { arguments.push(Self::new(b)); } arguments.push(Self::new(a)); Self::Call { name: name.lexeme().to_owned(), arguments, } } full::Thunk::Binary { name, args: [a, b], .. } => Self::Call { name: name.lexeme().to_owned(), arguments: vec![Self::new(a), Self::new(b)], }, full::Thunk::BinaryPlus { name, args: ([a, b], rest), .. } => { let mut arguments = vec![Self::new(a), Self::new(b)]; for arg in rest { arguments.push(Self::new(arg)); } Self::Call { name: name.lexeme().to_owned(), arguments, } } full::Thunk::Ternary { name, args: [a, b, c], .. } => Self::Call { name: name.lexeme().to_owned(), arguments: vec![Self::new(a), Self::new(b), Self::new(c)], }, }, Concatenation { lhs, rhs } => Self::Concatenation { lhs: Self::new(lhs).into(), rhs: Self::new(rhs).into(), }, Join { lhs, rhs } => Self::Join { lhs: lhs.as_ref().map(|lhs| Self::new(lhs).into()), rhs: Self::new(rhs).into(), }, Conditional { condition: full::Condition { lhs, rhs, operator }, otherwise, then, } => Self::Conditional { lhs: Self::new(lhs).into(), operator: ConditionalOperator::new(*operator), otherwise: Self::new(otherwise).into(), rhs: Self::new(rhs).into(), then: Self::new(then).into(), }, StringLiteral { string_literal } => Self::String { text: string_literal.cooked.clone(), }, Variable { name, .. } => Self::Variable { name: name.lexeme().to_owned(), }, Group { contents } => Self::new(contents), } } } #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub struct Condition { lhs: Box, rhs: Box, operator: ConditionalOperator, } #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub enum ConditionalOperator { Equality, Inequality, RegexMatch, } impl ConditionalOperator { fn new(operator: full::ConditionalOperator) -> Self { match operator { full::ConditionalOperator::Equality => Self::Equality, full::ConditionalOperator::Inequality => Self::Inequality, full::ConditionalOperator::RegexMatch => Self::RegexMatch, } } } #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub struct Dependency { pub recipe: String, pub arguments: Vec, } impl Dependency { fn new(dependency: &full::Dependency) -> Self { Self { recipe: dependency.recipe.name().to_owned(), arguments: dependency.arguments.iter().map(Expression::new).collect(), } } }