use {super::*, CompileErrorKind::*};
#[derive(Default)]
pub(crate) struct Analyzer<'run, 'src> {
aliases: Table<'src, Alias<'src>>,
assignments: Vec<&'run Binding<'src, Expression<'src>>>,
functions: Vec<&'run FunctionDefinition<'src>>,
modules: Table<'src, Justfile<'src>>,
recipes: Vec<&'run Recipe<'src, UnresolvedDependency<'src>>>,
sets: Table<'src, Set<'src>>,
unexports: BTreeSet<String>,
warnings: Vec<Warning>,
}
impl<'run, 'src> Analyzer<'run, 'src> {
pub(crate) fn analyze(
asts: &'run HashMap<(Modulepath, PathBuf), Ast<'src>>,
config: &Config,
doc: Option<String>,
groups: &[StringLiteral<'src>],
loaded: &[PathBuf],
module_path: &Modulepath,
name: Option<Name<'src>>,
overrides: &mut HashMap<Number, String>,
paths: &HashMap<PathBuf, PathBuf>,
private: bool,
root: &Path,
) -> CompileResult<'src, Justfile<'src>> {
Self::default().justfile(
asts,
config,
doc,
groups,
loaded,
module_path,
name,
overrides,
paths,
private,
root,
)
}
fn justfile(
mut self,
asts: &'run HashMap<(Modulepath, PathBuf), Ast<'src>>,
config: &Config,
doc: Option<String>,
groups: &[StringLiteral<'src>],
loaded: &[PathBuf],
module_path: &Modulepath,
name: Option<Name<'src>>,
overrides: &mut HashMap<Number, String>,
paths: &HashMap<PathBuf, PathBuf>,
private: bool,
root: &Path,
) -> CompileResult<'src, Justfile<'src>> {
let mut absent_modules = BTreeSet::new();
let mut definitions = HashMap::new();
let mut imports = HashSet::new();
let mut list_features = Vec::new();
let mut module_docs: Vec<(&str, Expression)> = Vec::new();
let mut unstable_features = BTreeSet::new();
let mut stack = Vec::new();
let ast = asts.get(&(module_path.clone(), root.to_owned())).unwrap();
stack.push(ast);
while let Some(ast) = stack.pop() {
unstable_features.extend(&ast.unstable_features);
list_features.extend(&ast.list_features);
for item in &ast.items {
if !item.is_enabled() {
continue;
}
match item {
Item::Alias(alias) => {
Self::define(&mut definitions, alias.name, ItemKind::Alias, false)?;
self.aliases.insert(alias.clone());
}
Item::Assignment(assignment) => {
self.assignments.push(assignment);
}
Item::Comment(_) => (),
Item::Function(function) => {
self.functions.push(function);
}
Item::Import { absolute, .. } => {
if let Some(absolute) = absolute
&& imports.insert(absolute)
{
stack.push(asts.get(&(module_path.clone(), absolute.clone())).unwrap());
}
}
Item::Module {
absolute,
attributes,
doc,
name,
optional,
..
} => {
if let Some(absolute) = absolute {
Self::define(&mut definitions, *name, ItemKind::Module, false)?;
self.modules.insert(Self::analyze(
asts,
config,
doc.clone(),
&attributes.groups(),
loaded,
&module_path.join(name.lexeme()),
Some(*name),
overrides,
paths,
attributes.private(),
absolute,
)?);
if let Some(Attribute::Doc(Some(expression))) = attributes.get(AttributeKind::Doc) {
module_docs.push((name.lexeme(), expression.clone()));
}
} else if *optional {
absent_modules.insert(name.lexeme().to_string());
}
}
Item::Newline => {}
Item::Recipe(recipe) => {
Self::analyze_recipe(recipe)?;
self.recipes.push(recipe);
}
Item::Setting(set) => {
self.analyze_set(set)?;
self.sets.insert(set.clone());
}
Item::Unexport { name, .. } => {
if !self.unexports.insert(name.lexeme().to_string()) {
return Err(name.error(DuplicateUnexport {
variable: name.lexeme(),
}));
}
}
}
}
self.warnings.extend(ast.warnings.iter().cloned());
}
let mut allow_duplicate_variables = false;
for (_name, set) in &self.sets {
if let Setting::AllowDuplicateVariables(value) = set.value {
allow_duplicate_variables = value;
}
}
let mut assignments: Table<'src, Assignment<'src>> = Table::default();
for assignment in self.assignments {
let variable = assignment.name.lexeme();
if !allow_duplicate_variables && assignments.contains_key(variable) {
return Err(assignment.name.error(DuplicateVariable { variable }));
}
if assignments
.get(variable)
.is_none_or(|original| assignment.file_depth <= original.file_depth)
{
assignments.insert(assignment.clone());
}
if self.unexports.contains(variable) {
return Err(assignment.name.error(ExportUnexported { variable }));
}
}
let mut functions = Table::<FunctionDefinition>::default();
for function in self.functions {
let name = function.name.lexeme();
if let Some(first) = functions.get(name) {
return Err(function.name.error(Redefinition {
first_type: ItemKind::Function,
second_type: ItemKind::Function,
name,
first: first.name.line,
}));
}
let mut parameters = BTreeSet::new();
for (parameter, _) in &function.parameters {
if !parameters.insert(parameter.lexeme()) {
return Err(parameter.error(DuplicateFunctionParameter {
function: name,
parameter: parameter.lexeme(),
}));
}
}
functions.insert(function.clone());
}
for ((path, name), value) in &config.overrides {
if *path == ast.module_path
&& let Some(assignment) = assignments.get(name)
{
overrides.insert(assignment.number, value.clone());
}
}
let (bindings, evaluation_order) =
VariableResolver::resolve_assignments(&mut assignments, &mut functions)?;
let variable_resolver = VariableResolver::new(&assignments, bindings, &functions, overrides);
let mut variable_references = HashSet::new();
for set in self.sets.values_mut() {
for expression in set.value.expressions_mut() {
variable_resolver.resolve_expression(
expression,
&ExpressionContext::new(),
&mut variable_references,
)?;
}
}
let scope = Scope::root();
let mut evaluator = {
let lists = self
.sets
.values()
.any(|set| matches!(set.value, Setting::Lists(true)));
let mut collect_references = |expression| {
variable_resolver.collect_references(
expression,
&ExpressionContext::new(),
&mut variable_references,
&mut HashSet::new(),
);
};
for attribute in self.recipes.iter().flat_map(|recipe| &recipe.attributes) {
match attribute {
Attribute::Arg {
help_property,
pattern_property,
..
} => {
if let Some((_, expression)) = help_property {
collect_references(expression);
}
if let Some((_, expression)) = pattern_property {
collect_references(expression);
}
}
Attribute::Doc(Some(expression)) => {
collect_references(expression);
}
_ => {}
}
}
for (_name, expression) in &module_docs {
collect_references(expression);
}
Evaluator::evaluate_const_assignments(
&assignments,
&evaluation_order,
overrides,
&scope,
&variable_references,
lists,
)?
};
let settings = evaluator.evaluate_sets(self.sets)?;
if !settings.lists {
for (feature, token) in list_features {
if feature.function() && functions.contains_key(token.lexeme()) {
continue;
}
return Err(token.error(CompileErrorKind::ListFeature(feature)));
}
}
for (name, mut expression) in module_docs {
variable_resolver.resolve_expression(
&mut expression,
&ExpressionContext::new(),
&mut variable_references,
)?;
let value = evaluator.evaluate_value_const(&expression)?;
self.modules.get_mut(name).unwrap().doc = if value.is_empty() {
None
} else {
Some(value.join())
};
}
let mut deduplicated_recipes = Table::<'src, UnresolvedRecipe<'src>>::default();
for recipe in self.recipes {
Self::define(
&mut definitions,
recipe.name,
ItemKind::Recipe,
settings.allow_duplicate_recipes,
)?;
if deduplicated_recipes
.get(recipe.name.lexeme())
.is_none_or(|original| recipe.file_depth <= original.file_depth)
{
deduplicated_recipes.insert(recipe.clone());
}
if !recipe.is_script(&settings) {
let mut continued = false;
for line in &recipe.body {
let comment = !continued && settings.ignore_comments && line.is_comment();
if !continued {
let sigils = line.sigils(&settings);
if sigils.contains(&Sigil::Guard) && sigils.contains(&Sigil::Infallible) {
let Fragment::Text { token } = line.fragments.first().unwrap() else {
unreachable!();
};
return Err(token.error(GuardAndInfallibleSigil));
}
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 = !comment && line.is_continuation();
}
for attribute in [AttributeKind::Cache, AttributeKind::Extension] {
if let Some(attribute) = recipe.attributes.get(attribute) {
return Err(recipe.name.error(InvalidShellRecipeAttribute {
attribute: Box::new(attribute.clone()),
recipe: recipe.name.lexeme(),
}));
}
}
}
}
let (recipes, disabled_recipes) = RecipeResolver::resolve_recipes(
&absent_modules,
&mut evaluator,
&ast.module_path,
&self.modules,
&settings,
deduplicated_recipes,
&variable_resolver,
)?;
let mut recipe_aliases = Table::new();
let mut module_aliases = Table::new();
let mut disabled_aliases = Table::new();
for alias in self.aliases.into_values() {
if let Some(resolution) =
Resolution::resolve_module(&alias.target, &absent_modules, &self.modules)
{
match resolution {
Resolution::Resolved(target) => {
module_aliases.insert(Alias {
attributes: alias.attributes,
name: alias.name,
target,
});
}
Resolution::Disabled(modules) => {
disabled_aliases.insert(Disabled {
modules,
name: alias.name,
});
}
}
} else if let Some(resolution) = Resolution::resolve_recipe(
&alias.target,
&absent_modules,
&disabled_recipes,
&self.modules,
&recipes,
) {
match resolution {
Resolution::Resolved(target) => {
recipe_aliases.insert(alias.resolve(target));
}
Resolution::Disabled(modules) => {
disabled_aliases.insert(Disabled {
modules,
name: alias.name,
});
}
}
} else {
return Err(alias.name.error(UnknownAliasTarget {
alias: alias.name.lexeme(),
target: alias.target,
}));
}
}
let mut assignment_references = HashMap::new();
for assignment in assignments.values() {
let mut references = HashSet::from([assignment.number]);
if !overrides.contains_key(&assignment.number) {
variable_resolver.collect_references(
&assignment.value,
&ExpressionContext::new(),
&mut references,
&mut HashSet::new(),
);
}
assignment_references.insert(assignment.number, references);
}
let source = root.to_owned();
let root = paths.get(root).unwrap();
let mut default = None::<Arc<Recipe>>;
for recipe in recipes.values() {
if recipe.attributes.contains(AttributeKind::Default) {
if let Some(previous) = &default {
let recipe = if previous.line_number() > recipe.line_number() {
previous
} else {
recipe
};
return Err(recipe.name.error(CompileErrorKind::DuplicateDefault {
recipe: recipe.name.lexeme(),
}));
}
default = Some(Arc::clone(recipe));
}
}
let default = default.or_else(|| {
recipes
.values()
.filter(|recipe| recipe.name.path == root)
.fold(None, |accumulator, next| match accumulator {
None => Some(Arc::clone(next)),
Some(previous) => Some(if previous.line_number() < next.line_number() {
previous
} else {
Arc::clone(next)
}),
})
});
Ok(Justfile {
absent_modules,
assignment_references,
assignments,
default,
disabled_aliases,
disabled_recipes,
doc,
evaluation_order,
functions,
groups: groups.into(),
loaded: loaded.into(),
module_aliases,
module_path: ast.module_path.clone(),
modules: self.modules,
name,
private,
recipe_aliases,
recipes,
settings,
source,
unexports: self.unexports,
unstable_features,
warnings: self.warnings,
working_directory: ast.working_directory.clone(),
})
}
fn define(
definitions: &mut HashMap<&'src str, (ItemKind, Name<'src>)>,
name: Name<'src>,
second_type: ItemKind,
duplicates_allowed: bool,
) -> CompileResult<'src> {
if let Some((first_type, original)) = definitions.get(name.lexeme())
&& !(*first_type == second_type && duplicates_allowed)
{
let ((first_type, second_type), (original, redefinition)) = if name.line < original.line {
((second_type, *first_type), (name, *original))
} else {
((*first_type, second_type), (*original, name))
};
return Err(redefinition.token.error(Redefinition {
first_type,
second_type,
name: name.lexeme(),
first: original.line,
}));
}
definitions.insert(name.lexeme(), (second_type, name));
Ok(())
}
fn analyze_recipe(recipe: &UnresolvedRecipe<'src>) -> CompileResult<'src> {
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.error(DuplicateParameter {
recipe: recipe.name.lexeme(),
parameter: parameter.name.lexeme(),
}));
}
parameters.insert(parameter.name.lexeme());
if parameter.default.is_some() && !parameter.is_option() {
passed_default = true;
} else if passed_default && parameter.is_required() && !parameter.is_option() {
return Err(
parameter
.name
.token
.error(RequiredParameterFollowsDefaultParameter {
parameter: parameter.name.lexeme(),
}),
);
}
}
Ok(())
}
fn analyze_set(&self, set: &Set<'src>) -> CompileResult<'src> {
if let Some(original) = self.sets.get(set.name.lexeme()) {
return Err(set.name.error(DuplicateSet {
setting: original.name.lexeme(),
first: original.name.line,
}));
}
if let Some(second) = Keyword::from_lexeme(set.name.lexeme()) {
for &first in set.value.conflicts() {
if let Some(conflict) = self.sets.get(first.lexeme())
&& conflict.value.conflicts().contains(&second)
{
return Err(set.name.error(IncompatibleSettings {
first,
first_line: conflict.name.line,
second,
}));
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
analysis_error! {
name: duplicate_alias,
input: "alias foo := bar\nalias foo := baz",
offset: 23,
line: 1,
column: 6,
width: 3,
kind: Redefinition {
first_type: ItemKind::Alias,
second_type: ItemKind::Alias,
name: "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: Namepath::from(Name::from_identifier(
Token{
column: 13,
kind: TokenKind::Identifier,
length: 3,
line: 0,
offset: 13,
path: Path::new("justfile"),
src: "alias foo := bar\n",
}
))
},
}
analysis_error! {
name: alias_shadows_recipe_before,
input: "bar: \n echo bar\nalias foo := bar\nfoo:\n echo foo",
offset: 34,
line: 3,
column: 0,
width: 3,
kind: Redefinition {
first_type: ItemKind::Alias,
second_type: ItemKind::Recipe,
name: "foo",
first: 2,
},
}
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: Redefinition {
first_type: ItemKind::Recipe,
second_type: ItemKind::Alias,
name: "foo",
first: 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: duplicate_recipe,
input: "a:\nb:\na:",
offset: 6,
line: 2,
column: 0,
width: 1,
kind: Redefinition {
first: 0,
first_type: ItemKind::Recipe,
name: "a",
second_type: ItemKind::Recipe,
},
}
analysis_error! {
name: duplicate_variable,
input: "a := \"0\"\na := \"0\"",
offset: 9,
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,
}
analysis_error! {
name: undefined_function,
input: "a := foo()",
offset: 5,
line: 0,
column: 5,
width: 3,
kind: UndefinedFunction { function: "foo" },
}
analysis_error! {
name: undefined_function_in_interpolation,
input: "a:\n echo {{bar()}}",
offset: 11,
line: 1,
column: 8,
width: 3,
kind: UndefinedFunction { function: "bar" },
}
analysis_error! {
name: undefined_function_in_default,
input: "a f=baz():",
offset: 4,
line: 0,
column: 4,
width: 3,
kind: UndefinedFunction { function: "baz" },
}
analysis_error! {
name: function_argument_count_nullary,
input: "x := arch('foo')",
offset: 5,
line: 0,
column: 5,
width: 4,
kind: FunctionArgumentCountMismatch {
function: "arch",
arguments: 1,
expected: 0..=0,
},
}
analysis_error! {
name: function_argument_count_unary,
input: "x := env_var()",
offset: 5,
line: 0,
column: 5,
width: 7,
kind: FunctionArgumentCountMismatch {
function: "env_var",
arguments: 0,
expected: 1..=1,
},
}
analysis_error! {
name: function_argument_count_too_high_unary_opt,
input: "x := env('foo', 'foo', 'foo')",
offset: 5,
line: 0,
column: 5,
width: 3,
kind: FunctionArgumentCountMismatch {
function: "env",
arguments: 3,
expected: 1..=2,
},
}
analysis_error! {
name: function_argument_count_too_low_unary_opt,
input: "x := env()",
offset: 5,
line: 0,
column: 5,
width: 3,
kind: FunctionArgumentCountMismatch {
function: "env",
arguments: 0,
expected: 1..=2,
},
}
analysis_error! {
name: function_argument_count_binary,
input: "x := env_var_or_default('foo')",
offset: 5,
line: 0,
column: 5,
width: 18,
kind: FunctionArgumentCountMismatch {
function: "env_var_or_default",
arguments: 1,
expected: 2..=2,
},
}
analysis_error! {
name: function_argument_count_binary_plus,
input: "x := join('foo')",
offset: 5,
line: 0,
column: 5,
width: 4,
kind: FunctionArgumentCountMismatch {
function: "join",
arguments: 1,
expected: 2..=usize::MAX,
},
}
analysis_error! {
name: function_argument_count_ternary,
input: "x := replace('foo')",
offset: 5,
line: 0,
column: 5,
width: 7,
kind: FunctionArgumentCountMismatch {
function: "replace",
arguments: 1,
expected: 3..=3,
},
}
}