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 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}