pub_just/
analyzer.rs

1use {super::*, CompileErrorKind::*};
2
3#[derive(Default)]
4pub struct Analyzer<'run, 'src> {
5  aliases: Table<'src, Alias<'src, Name<'src>>>,
6  assignments: Vec<&'run Binding<'src, Expression<'src>>>,
7  modules: Table<'src, Justfile<'src>>,
8  recipes: Vec<&'run Recipe<'src, UnresolvedDependency<'src>>>,
9  sets: Table<'src, Set<'src>>,
10  unexports: HashSet<String>,
11  warnings: Vec<Warning>,
12}
13
14impl<'run, 'src> Analyzer<'run, 'src> {
15  pub fn analyze(
16    asts: &'run HashMap<PathBuf, Ast<'src>>,
17    doc: Option<String>,
18    groups: &[String],
19    loaded: &[PathBuf],
20    name: Option<Name<'src>>,
21    paths: &HashMap<PathBuf, PathBuf>,
22    root: &Path,
23  ) -> CompileResult<'src, Justfile<'src>> {
24    Self::default().justfile(asts, doc, groups, loaded, name, paths, root)
25  }
26
27  fn justfile(
28    mut self,
29    asts: &'run HashMap<PathBuf, Ast<'src>>,
30    doc: Option<String>,
31    groups: &[String],
32    loaded: &[PathBuf],
33    name: Option<Name<'src>>,
34    paths: &HashMap<PathBuf, PathBuf>,
35    root: &Path,
36  ) -> CompileResult<'src, Justfile<'src>> {
37    let mut definitions = HashMap::new();
38    let mut imports = HashSet::new();
39    let mut unstable_features = BTreeSet::new();
40
41    let mut stack = Vec::new();
42    let ast = asts.get(root).unwrap();
43    stack.push(ast);
44
45    while let Some(ast) = stack.pop() {
46      unstable_features.extend(&ast.unstable_features);
47
48      for item in &ast.items {
49        match item {
50          Item::Alias(alias) => {
51            Self::define(&mut definitions, alias.name, "alias", false)?;
52            Self::analyze_alias(alias)?;
53            self.aliases.insert(alias.clone());
54          }
55          Item::Assignment(assignment) => {
56            self.assignments.push(assignment);
57          }
58          Item::Comment(_) => (),
59          Item::Import { absolute, .. } => {
60            if let Some(absolute) = absolute {
61              if imports.insert(absolute) {
62                stack.push(asts.get(absolute).unwrap());
63              }
64            }
65          }
66          Item::Module {
67            absolute,
68            name,
69            doc,
70            attributes,
71            ..
72          } => {
73            let mut doc_attr: Option<&str> = None;
74            let mut groups = Vec::new();
75            for attribute in attributes {
76              if let Attribute::Doc(ref doc) = attribute {
77                doc_attr = Some(doc.as_ref().map(|s| s.cooked.as_ref()).unwrap_or_default());
78              } else if let Attribute::Group(ref group) = attribute {
79                groups.push(group.cooked.clone());
80              } else {
81                return Err(name.token.error(InvalidAttribute {
82                  item_kind: "Module",
83                  item_name: name.lexeme(),
84                  attribute: attribute.clone(),
85                }));
86              }
87            }
88
89            if let Some(absolute) = absolute {
90              Self::define(&mut definitions, *name, "module", false)?;
91              self.modules.insert(Self::analyze(
92                asts,
93                doc_attr.or(*doc).map(ToOwned::to_owned),
94                groups.as_slice(),
95                loaded,
96                Some(*name),
97                paths,
98                absolute,
99              )?);
100            }
101          }
102          Item::Recipe(recipe) => {
103            if recipe.enabled() {
104              Self::analyze_recipe(recipe)?;
105              self.recipes.push(recipe);
106            }
107          }
108          Item::Set(set) => {
109            self.analyze_set(set)?;
110            self.sets.insert(set.clone());
111          }
112          Item::Unexport { name } => {
113            if !self.unexports.insert(name.lexeme().to_string()) {
114              return Err(name.token.error(DuplicateUnexport {
115                variable: name.lexeme(),
116              }));
117            }
118          }
119        }
120      }
121
122      self.warnings.extend(ast.warnings.iter().cloned());
123    }
124
125    let settings = Settings::from_table(self.sets);
126
127    let mut assignments: Table<'src, Assignment<'src>> = Table::default();
128    for assignment in self.assignments {
129      let variable = assignment.name.lexeme();
130
131      if !settings.allow_duplicate_variables && assignments.contains_key(variable) {
132        return Err(assignment.name.token.error(DuplicateVariable { variable }));
133      }
134
135      if assignments.get(variable).map_or(true, |original| {
136        assignment.file_depth <= original.file_depth
137      }) {
138        assignments.insert(assignment.clone());
139      }
140
141      if self.unexports.contains(variable) {
142        return Err(assignment.name.token.error(ExportUnexported { variable }));
143      }
144    }
145
146    AssignmentResolver::resolve_assignments(&assignments)?;
147
148    let mut deduplicated_recipes = Table::<'src, UnresolvedRecipe<'src>>::default();
149    for recipe in self.recipes {
150      Self::define(
151        &mut definitions,
152        recipe.name,
153        "recipe",
154        settings.allow_duplicate_recipes,
155      )?;
156
157      if deduplicated_recipes
158        .get(recipe.name.lexeme())
159        .map_or(true, |original| recipe.file_depth <= original.file_depth)
160      {
161        deduplicated_recipes.insert(recipe.clone());
162      }
163    }
164
165    let recipes = RecipeResolver::resolve_recipes(&assignments, &settings, deduplicated_recipes)?;
166
167    let mut aliases = Table::new();
168    while let Some(alias) = self.aliases.pop() {
169      aliases.insert(Self::resolve_alias(&recipes, alias)?);
170    }
171
172    for recipe in recipes.values() {
173      for attribute in &recipe.attributes {
174        if let Attribute::Script(_) = attribute {
175          unstable_features.insert(UnstableFeature::ScriptAttribute);
176          break;
177        }
178      }
179    }
180
181    if settings.script_interpreter.is_some() {
182      unstable_features.insert(UnstableFeature::ScriptInterpreterSetting);
183    }
184
185    let root = paths.get(root).unwrap();
186
187    Ok(Justfile {
188      aliases,
189      assignments,
190      default: recipes
191        .values()
192        .filter(|recipe| recipe.name.path == root)
193        .fold(None, |accumulator, next| match accumulator {
194          None => Some(Rc::clone(next)),
195          Some(previous) => Some(if previous.line_number() < next.line_number() {
196            previous
197          } else {
198            Rc::clone(next)
199          }),
200        }),
201      doc,
202      groups: groups.into(),
203      loaded: loaded.into(),
204      modules: self.modules,
205      name,
206      recipes,
207      settings,
208      source: root.into(),
209      unexports: self.unexports,
210      unstable_features,
211      warnings: self.warnings,
212      working_directory: ast.working_directory.clone(),
213    })
214  }
215
216  fn define(
217    definitions: &mut HashMap<&'src str, (&'static str, Name<'src>)>,
218    name: Name<'src>,
219    second_type: &'static str,
220    duplicates_allowed: bool,
221  ) -> CompileResult<'src> {
222    if let Some((first_type, original)) = definitions.get(name.lexeme()) {
223      if !(*first_type == second_type && duplicates_allowed) {
224        let ((first_type, second_type), (original, redefinition)) = if name.line < original.line {
225          ((second_type, *first_type), (name, *original))
226        } else {
227          ((*first_type, second_type), (*original, name))
228        };
229
230        return Err(redefinition.token.error(Redefinition {
231          first_type,
232          second_type,
233          name: name.lexeme(),
234          first: original.line,
235        }));
236      }
237    }
238
239    definitions.insert(name.lexeme(), (second_type, name));
240
241    Ok(())
242  }
243
244  fn analyze_recipe(recipe: &UnresolvedRecipe<'src>) -> CompileResult<'src> {
245    let mut parameters = BTreeSet::new();
246    let mut passed_default = false;
247
248    for parameter in &recipe.parameters {
249      if parameters.contains(parameter.name.lexeme()) {
250        return Err(parameter.name.token.error(DuplicateParameter {
251          recipe: recipe.name.lexeme(),
252          parameter: parameter.name.lexeme(),
253        }));
254      }
255      parameters.insert(parameter.name.lexeme());
256
257      if parameter.default.is_some() {
258        passed_default = true;
259      } else if passed_default {
260        return Err(
261          parameter
262            .name
263            .token
264            .error(RequiredParameterFollowsDefaultParameter {
265              parameter: parameter.name.lexeme(),
266            }),
267        );
268      }
269    }
270
271    let mut continued = false;
272    for line in &recipe.body {
273      if !recipe.is_script() && !continued {
274        if let Some(Fragment::Text { token }) = line.fragments.first() {
275          let text = token.lexeme();
276
277          if text.starts_with(' ') || text.starts_with('\t') {
278            return Err(token.error(ExtraLeadingWhitespace));
279          }
280        }
281      }
282
283      continued = line.is_continuation();
284    }
285
286    if !recipe.is_script() {
287      if let Some(attribute) = recipe
288        .attributes
289        .iter()
290        .find(|attribute| matches!(attribute, Attribute::Extension(_)))
291      {
292        return Err(recipe.name.error(InvalidAttribute {
293          item_kind: "Recipe",
294          item_name: recipe.name.lexeme(),
295          attribute: attribute.clone(),
296        }));
297      }
298    }
299
300    Ok(())
301  }
302
303  fn analyze_alias(alias: &Alias<'src, Name<'src>>) -> CompileResult<'src> {
304    for attribute in &alias.attributes {
305      if *attribute != Attribute::Private {
306        return Err(alias.name.token.error(InvalidAttribute {
307          item_kind: "Alias",
308          item_name: alias.name.lexeme(),
309          attribute: attribute.clone(),
310        }));
311      }
312    }
313
314    Ok(())
315  }
316
317  fn analyze_set(&self, set: &Set<'src>) -> CompileResult<'src> {
318    if let Some(original) = self.sets.get(set.name.lexeme()) {
319      return Err(set.name.error(DuplicateSet {
320        setting: original.name.lexeme(),
321        first: original.name.line,
322      }));
323    }
324
325    Ok(())
326  }
327
328  fn resolve_alias(
329    recipes: &Table<'src, Rc<Recipe<'src>>>,
330    alias: Alias<'src, Name<'src>>,
331  ) -> CompileResult<'src, Alias<'src>> {
332    // Make sure the target recipe exists
333    match recipes.get(alias.target.lexeme()) {
334      Some(target) => Ok(alias.resolve(Rc::clone(target))),
335      None => Err(alias.name.token.error(UnknownAliasTarget {
336        alias: alias.name.lexeme(),
337        target: alias.target.lexeme(),
338      })),
339    }
340  }
341}
342
343#[cfg(test)]
344mod tests {
345  use super::*;
346
347  analysis_error! {
348    name: duplicate_alias,
349    input: "alias foo := bar\nalias foo := baz",
350    offset: 23,
351    line: 1,
352    column: 6,
353    width: 3,
354    kind: Redefinition { first_type: "alias", second_type: "alias", name: "foo", first: 0 },
355  }
356
357  analysis_error! {
358    name: unknown_alias_target,
359    input: "alias foo := bar\n",
360    offset: 6,
361    line: 0,
362    column: 6,
363    width: 3,
364    kind: UnknownAliasTarget {alias: "foo", target: "bar"},
365  }
366
367  analysis_error! {
368    name: alias_shadows_recipe_before,
369    input: "bar: \n  echo bar\nalias foo := bar\nfoo:\n  echo foo",
370    offset: 34,
371    line: 3,
372    column: 0,
373    width: 3,
374    kind: Redefinition { first_type: "alias", second_type: "recipe", name: "foo", first: 2 },
375  }
376
377  analysis_error! {
378    name: alias_shadows_recipe_after,
379    input: "foo:\n  echo foo\nalias foo := bar\nbar:\n  echo bar",
380    offset: 22,
381    line: 2,
382    column: 6,
383    width: 3,
384    kind: Redefinition { first_type: "recipe", second_type: "alias", name: "foo", first: 0 },
385  }
386
387  analysis_error! {
388    name:   required_after_default,
389    input:  "hello arg='foo' bar:",
390    offset:  16,
391    line:   0,
392    column: 16,
393    width:  3,
394    kind:   RequiredParameterFollowsDefaultParameter{parameter: "bar"},
395  }
396
397  analysis_error! {
398    name:   duplicate_parameter,
399    input:  "a b b:",
400    offset:  4,
401    line:   0,
402    column: 4,
403    width:  1,
404    kind:   DuplicateParameter{recipe: "a", parameter: "b"},
405  }
406
407  analysis_error! {
408    name:   duplicate_variadic_parameter,
409    input:  "a b +b:",
410    offset: 5,
411    line:   0,
412    column: 5,
413    width:  1,
414    kind:   DuplicateParameter{recipe: "a", parameter: "b"},
415  }
416
417  analysis_error! {
418    name:   duplicate_recipe,
419    input:  "a:\nb:\na:",
420    offset:  6,
421    line:   2,
422    column: 0,
423    width:  1,
424    kind:   Redefinition { first_type: "recipe", second_type: "recipe", name: "a", first: 0 },
425  }
426
427  analysis_error! {
428    name:   duplicate_variable,
429    input:  "a := \"0\"\na := \"0\"",
430    offset: 9,
431    line:   1,
432    column: 0,
433    width:  1,
434    kind:   DuplicateVariable{variable: "a"},
435  }
436
437  analysis_error! {
438    name:   extra_whitespace,
439    input:  "a:\n blah\n  blarg",
440    offset:  10,
441    line:   2,
442    column: 1,
443    width:  6,
444    kind:   ExtraLeadingWhitespace,
445  }
446}