Skip to main content

just_lsp/
analyzer.rs

1use super::*;
2
3#[derive(Debug)]
4pub struct Analyzer<'a> {
5  config: Option<&'a Config>,
6  document: &'a Document,
7}
8
9impl<'a> From<&'a Document> for Analyzer<'a> {
10  fn from(document: &'a Document) -> Self {
11    Self {
12      config: None,
13      document,
14    }
15  }
16}
17
18impl<'a> Analyzer<'a> {
19  /// Run all registered rules against the document.
20  ///
21  /// Rules that return `None` from `severity()` are filtered out, so
22  /// config can suppress individual rules entirely. Diagnostics are
23  /// sorted by position then message for deterministic output.
24  #[must_use]
25  pub fn analyze(&self) -> Vec<Diagnostic> {
26    let context = RuleContext::new(self.document);
27
28    let default = Config::default();
29
30    let config = self.config.unwrap_or(&default);
31
32    let mut diagnostics = inventory::iter::<&dyn Rule>
33      .into_iter()
34      .flat_map(|rule| {
35        rule
36          .run(&context)
37          .into_iter()
38          .filter_map(move |diagnostic| {
39            let rule_config = config.rule_config(rule.id());
40
41            Some(Diagnostic {
42              id: rule.id().to_string(),
43              display: rule.message().to_string(),
44              severity: rule_config.severity(diagnostic.severity)?,
45              ..diagnostic
46            })
47          })
48      })
49      .collect::<Vec<_>>();
50
51    diagnostics.sort_by(|a, b| {
52      a.range
53        .start
54        .line
55        .cmp(&b.range.start.line)
56        .then_with(|| a.range.start.character.cmp(&b.range.start.character))
57        .then_with(|| a.message.cmp(&b.message))
58    });
59
60    diagnostics
61  }
62
63  /// Set the config for rule severity overrides.
64  ///
65  /// When no config is set, `Config::default()` is used, which leaves
66  /// all rule severities at their built-in defaults.
67  #[must_use]
68  pub fn config(self, config: &'a Config) -> Self {
69    Self {
70      config: Some(config),
71      ..self
72    }
73  }
74}
75
76#[cfg(test)]
77mod tests {
78  use {super::*, indoc::indoc, pretty_assertions::assert_eq};
79
80  #[derive(Debug)]
81  struct Test {
82    config: Config,
83    document: Document,
84    messages: Vec<(&'static str, lsp::Range, Option<lsp::DiagnosticSeverity>)>,
85  }
86
87  impl Test {
88    fn config(self, config: Config) -> Self {
89      Self { config, ..self }
90    }
91
92    fn error(self, message: &'static str, range: lsp::Range) -> Self {
93      Self {
94        messages: self
95          .messages
96          .into_iter()
97          .chain([(message, range, Some(lsp::DiagnosticSeverity::ERROR))])
98          .collect(),
99        ..self
100      }
101    }
102
103    fn new(content: &str) -> Self {
104      let uri = if cfg!(windows) {
105        "file:///C:/test.just"
106      } else {
107        "file:///test.just"
108      };
109
110      Self {
111        config: Config::default(),
112        document: Document::try_from(lsp::DidOpenTextDocumentParams {
113          text_document: lsp::TextDocumentItem {
114            uri: lsp::Url::parse(uri).unwrap(),
115            language_id: "just".to_string(),
116            version: 1,
117            text: content.to_string(),
118          },
119        })
120        .unwrap(),
121        messages: Vec::new(),
122      }
123    }
124
125    fn run(self) {
126      let Test {
127        config,
128        document,
129        messages,
130      } = self;
131
132      let analyzer = Analyzer::from(&document).config(&config);
133
134      let diagnostics = analyzer
135        .analyze()
136        .into_iter()
137        .map(lsp::Diagnostic::from)
138        .collect::<Vec<lsp::Diagnostic>>();
139
140      assert_eq!(
141        diagnostics.len(),
142        messages.len(),
143        "Expected diagnostics {:?} but got {:?}",
144        messages,
145        diagnostics,
146      );
147
148      for (diagnostic, (expected_message, expected_range, expected_severity)) in
149        diagnostics.into_iter().zip(messages)
150      {
151        assert_eq!(diagnostic.severity, expected_severity, "{diagnostic:?}");
152        assert_eq!(diagnostic.message, expected_message);
153        assert_eq!(diagnostic.range, expected_range);
154      }
155    }
156
157    fn warning(self, message: &'static str, range: lsp::Range) -> Self {
158      Self {
159        messages: self
160          .messages
161          .into_iter()
162          .chain([(message, range, Some(lsp::DiagnosticSeverity::WARNING))])
163          .collect(),
164        ..self
165      }
166    }
167  }
168
169  #[test]
170  fn accepts_logical_operators() {
171    Test::new(indoc! {
172      "
173      foo := '' || 'bar'
174      bar := 'foo' && 'bar'
175
176      baz:
177        echo {{ foo }}
178        echo {{ bar }}
179      "
180    })
181    .run();
182  }
183
184  #[test]
185  fn alias_recipe_conflict_alias_then_recipe() {
186    Test::new(indoc! {
187      "
188      alias t := other
189
190      other:
191        echo \"other\"
192
193      t:
194        echo \"recipe\"
195      "
196    })
197    .error(
198      "Alias `t` is redefined as a recipe",
199      lsp::Range::at(5, 0, 5, 1),
200    )
201    .run();
202  }
203
204  #[test]
205  fn alias_recipe_conflict_recipe_then_alias() {
206    Test::new(indoc! {
207      "
208      other:
209        echo \"other\"
210
211      t:
212        echo \"recipe\"
213
214      alias t := other
215      "
216    })
217    .error(
218      "Recipe `t` is redefined as an alias",
219      lsp::Range::at(6, 6, 6, 7),
220    )
221    .run();
222  }
223
224  #[test]
225  fn aliases_basic() {
226    Test::new(indoc! {
227      "
228      foo:
229        echo \"foo\"
230
231      alias bar := foo
232      "
233    })
234    .run();
235  }
236
237  #[test]
238  fn aliases_duplicate() {
239    Test::new(indoc! {
240      "
241      foo:
242        echo \"foo\"
243
244      alias bar := foo
245      alias bar := foo
246      alias bar := foo
247      "
248    })
249    .error("Duplicate alias `bar`", lsp::Range::at(4, 0, 4, 16))
250    .error("Duplicate alias `bar`", lsp::Range::at(5, 0, 5, 16))
251    .run();
252  }
253
254  #[test]
255  fn aliases_missing_recipe() {
256    Test::new(indoc! {
257      "
258      foo:
259        echo \"foo\"
260
261      alias bar := baz
262      "
263    })
264    .error("Recipe `baz` not found", lsp::Range::at(3, 13, 3, 16))
265    .run();
266  }
267
268  #[test]
269  fn aliases_missing_target() {
270    Test::new(indoc! {
271      "
272      alias foo :=
273      "
274    })
275    .error("Missing identifier in alias", lsp::Range::at(0, 12, 0, 12))
276    .error("Recipe `` not found", lsp::Range::at(0, 12, 0, 12))
277    .run();
278  }
279
280  #[test]
281  fn all_four_os_groups_no_conflict() {
282    Test::new(indoc! {
283      "
284      [linux]
285      build:
286        echo \"Building on Linux\"
287
288      [macos]
289      build:
290        echo \"Building on macOS\"
291
292      [windows]
293      build:
294        echo \"Building on Windows\"
295
296      [dragonfly]
297      build:
298        echo \"Building on DragonFly BSD\"
299
300      [freebsd]
301      build:
302        echo \"Building on FreeBSD\"
303
304      [netbsd]
305      build:
306        echo \"Building on NetBSD\"
307
308      [openbsd]
309      build:
310        echo \"Building on OpenBSD\"
311      "
312    })
313    .run();
314  }
315
316  #[test]
317  fn analyze_complete() {
318    Test::new(indoc! {
319      "
320      foo:
321        echo \"foo\"
322
323      bar: missing
324        echo \"bar\"
325
326      alias baz := nonexistent
327      "
328    })
329    .error("Recipe `missing` not found", lsp::Range::at(3, 5, 3, 12))
330    .error(
331      "Recipe `nonexistent` not found",
332      lsp::Range::at(6, 13, 6, 24),
333    )
334    .run();
335  }
336
337  #[test]
338  fn arg_attribute_empty_parens() {
339    Test::new(indoc! {
340      "
341      [arg()]
342      bar foo:
343        echo {{foo}}
344      "
345    })
346    .error(
347      "Attribute `arg` got 0 arguments but takes at least 1 argument",
348      lsp::Range::at(0, 0, 1, 0),
349    )
350    .error("Missing identifier in value", lsp::Range::at(0, 5, 0, 5))
351    .run();
352  }
353
354  #[test]
355  fn arg_attribute_missing_parameter_name() {
356    Test::new(indoc! {
357      "
358      [arg]
359      bar foo:
360        echo {{foo}}
361      "
362    })
363    .error(
364      "Attribute `arg` got 0 arguments but takes at least 1 argument",
365      lsp::Range::at(0, 0, 1, 0),
366    )
367    .run();
368  }
369
370  #[test]
371  fn arg_attribute_unknown_kwarg() {
372    Test::new(indoc! {
373      "
374      [arg('name', bogus='x')]
375      foo name:
376        echo {{name}}
377      "
378    })
379    .error(
380      "Unknown `[arg]` keyword `bogus`, expected one of help, long, short, value, pattern",
381    lsp::Range::at(0, 13, 0, 22))
382    .run();
383  }
384
385  #[test]
386  fn arg_attribute_unknown_parameter() {
387    Test::new(indoc! {
388      "
389      [arg('missing', help='nope')]
390      foo name:
391        echo {{name}}
392      "
393    })
394    .error(
395      "`[arg]` references unknown parameter `missing`",
396      lsp::Range::at(0, 5, 0, 14),
397    )
398    .run();
399  }
400
401  #[test]
402  fn arg_attribute_valid() {
403    Test::new(indoc! {
404      "
405      [arg('foo', help=\"Help text\")]
406      bar foo:
407        echo {{foo}}
408      "
409    })
410    .run();
411  }
412
413  #[test]
414  fn arg_attribute_value_requires_long_or_short() {
415    Test::new(indoc! {
416      "
417      [arg('name', value='hi')]
418      foo name:
419        echo {{name}}
420      "
421    })
422    .error(
423      "`[arg]` `value=` requires `long=` or `short=`",
424      lsp::Range::at(0, 13, 0, 23),
425    )
426    .run();
427  }
428
429  #[test]
430  fn arg_attribute_value_with_long_ok() {
431    Test::new(indoc! {
432      "
433      [arg('name', long='name', value='hi')]
434      foo name:
435        echo {{name}}
436      "
437    })
438    .run();
439  }
440
441  #[test]
442  fn arg_attribute_with_long_option() {
443    Test::new(indoc! {
444      "
445      [arg('foo', long=\"foo-opt\")]
446      bar foo:
447        echo {{foo}}
448      "
449    })
450    .run();
451  }
452
453  #[test]
454  fn arg_attribute_with_multiple_options() {
455    Test::new(indoc! {
456      "
457      [arg('foo', long=\"foo-opt\", short=\"f\", value=\"default\")]
458      bar foo:
459        echo {{foo}}
460      "
461    })
462    .run();
463  }
464
465  #[test]
466  fn arg_attribute_with_pattern() {
467    Test::new(indoc! {
468      "
469      [arg('version', pattern=\"[0-9]+\\\\.[0-9]+\\\\.[0-9]+\")]
470      release version:
471        echo {{version}}
472      "
473    })
474    .run();
475  }
476
477  #[test]
478  fn arg_attribute_with_short_option() {
479    Test::new(indoc! {
480      "
481      [arg('foo', short=\"f\")]
482      bar foo:
483        echo {{foo}}
484      "
485    })
486    .run();
487  }
488
489  #[test]
490  fn assert_accepts_condition_operators() {
491    Test::new(indoc! {
492      "
493      foo name:
494        {{ assert(name == \"bar\", \"msg\") }}
495        {{ assert(name != \"bar\", \"msg\") }}
496        {{ assert(name =~ \"^[a-z]+$\", \"msg\") }}
497      "
498    })
499    .run();
500  }
501
502  #[test]
503  fn attribute_expression_requires_support() {
504    Test::new(indoc! {
505      "
506      foo := 'foo'
507
508      [group(foo)]
509      [group(f'bar')]
510      foo:
511        echo \"foo\"
512      "
513    })
514    .error(
515      "Attribute `group` arguments must be string literals",
516      lsp::Range::at(2, 7, 2, 10),
517    )
518    .error(
519      "Attribute `group` arguments must be string literals",
520      lsp::Range::at(3, 7, 3, 13),
521    )
522    .run();
523  }
524
525  #[test]
526  fn attribute_string_literal_expression() {
527    Test::new(indoc! {
528      "
529      [group('foo')]
530      [metadata(x'bar')]
531      foo:
532        echo \"foo\"
533      "
534    })
535    .run();
536  }
537
538  #[test]
539  fn attributes_correct() {
540    Test::new(indoc! {
541      "
542      [no-cd]
543      [linux]
544      [macos]
545      foo:
546        echo \"foo\"
547
548      [doc('Recipe documentation')]
549      bar:
550        echo \"bar\"
551
552      [default]
553      baz:
554        echo \"baz\"
555      "
556    })
557    .run();
558  }
559
560  #[test]
561  fn attributes_duplicate_default_between_recipes() {
562    Test::new(indoc! {
563      "
564      [default]
565      check:
566        echo \"check\"
567
568      [default]
569      ci:
570        echo \"ci\"
571      "
572    })
573    .error(
574      "Recipe `ci` has duplicate `[default]` attribute, which may only appear once per module", lsp::Range::at(4, 0, 5, 0))
575    .run();
576  }
577
578  #[test]
579  fn attributes_duplicate_default_on_same_recipe() {
580    Test::new(indoc! {
581      "
582      [default]
583      [default]
584      build:
585        echo \"build\"
586      "
587    })
588    .error(
589      "Recipe `build` has duplicate `[default]` attribute, which may only appear once per module", lsp::Range::at(1, 0, 2, 0))
590    .run();
591  }
592
593  #[test]
594  fn attributes_duplicate_group_attribute() {
595    Test::new(indoc! {
596      "
597      [group('dev')]
598      [group('dev')]
599      build:
600        echo \"build\"
601      "
602    })
603    .error(
604      "Recipe attribute `group` is duplicated",
605      lsp::Range::at(1, 0, 2, 0),
606    )
607    .run();
608  }
609
610  #[test]
611  fn attributes_duplicate_recipe_attribute() {
612    Test::new(indoc! {
613      "
614      [script]
615      [script]
616      build:
617        echo \"build\"
618      "
619    })
620    .error(
621      "Recipe attribute `script` is duplicated",
622      lsp::Range::at(1, 0, 2, 0),
623    )
624    .run();
625  }
626
627  #[test]
628  fn attributes_duplicate_working_directory_attribute() {
629    Test::new(indoc! {
630      "
631      [working-directory: 'foo']
632      [working-directory: 'bar']
633      build:
634        echo \"build\"
635      "
636    })
637    .error(
638      "Recipe attribute `working-directory` is duplicated",
639      lsp::Range::at(1, 0, 2, 0),
640    )
641    .run();
642  }
643
644  #[test]
645  fn attributes_extra_arguments() {
646    Test::new(indoc! {
647      "
648      [linux('invalid')]
649      foo:
650        echo \"foo\"
651      "
652    })
653    .error(
654      "Attribute `linux` got 1 argument but takes 0 arguments",
655      lsp::Range::at(0, 0, 1, 0),
656    )
657    .run();
658
659    Test::new(indoc! {
660      "
661      [default('invalid')]
662      foo:
663        echo \"foo\"
664      "
665    })
666    .error(
667      "Attribute `default` got 1 argument but takes 0 arguments",
668      lsp::Range::at(0, 0, 1, 0),
669    )
670    .run();
671  }
672
673  #[test]
674  fn attributes_inline_parameters_focused() {
675    Test::new(indoc! {
676      "
677      [group: 'foo', no-cd]
678      foo:
679        echo \"foo\"
680      "
681    })
682    .run();
683  }
684
685  #[test]
686  fn attributes_invalid_inline() {
687    Test::new(indoc! {
688      "
689      [group: 'foo', foo]
690      foo:
691        echo \"foo\"
692      "
693    })
694    .error("Unknown attribute `foo`", lsp::Range::at(0, 15, 0, 18))
695    .run();
696  }
697
698  #[test]
699  fn attributes_metadata_multiple_arguments() {
700    Test::new(indoc! {
701      "
702      [metadata('foo', 'bar')]
703      foo:
704        echo \"foo\"
705      "
706    })
707    .run();
708  }
709
710  #[test]
711  fn attributes_missing_arguments() {
712    Test::new(indoc! {
713      "
714      [extension]
715      foo:
716        #!/usr/bin/env bash
717        echo \"foo\"
718      "
719    })
720    .error(
721      "Attribute `extension` got 0 arguments but takes 1 argument",
722      lsp::Range::at(0, 0, 1, 0),
723    )
724    .run();
725  }
726
727  #[test]
728  fn attributes_more_arguments_than_required() {
729    Test::new(indoc! {
730      "
731      [group('foo', 'bar')]
732      foo:
733        echo \"foo\"
734      "
735    })
736    .error(
737      "Attribute `group` got 2 arguments but takes 1 argument",
738      lsp::Range::at(0, 0, 1, 0),
739    )
740    .run();
741  }
742
743  #[test]
744  fn attributes_multiple_group_attributes_allowed() {
745    Test::new(indoc! {
746      "
747      [group('lint')]
748      [group('rust')]
749      build:
750        echo \"build\"
751      "
752    })
753    .run();
754  }
755
756  #[test]
757  fn attributes_multiple_metadata_allowed() {
758    Test::new(indoc! {
759      "
760      [metadata('foo', 'bar')]
761      [metadata('baz', 'qux')]
762      foo:
763        echo \"foo\"
764      "
765    })
766    .run();
767  }
768
769  #[test]
770  fn attributes_no_cd_allowed_with_global_working_directory() {
771    Test::new(indoc! {
772      "
773      set working-directory := '/tmp'
774
775      [no-cd]
776      build:
777        echo \"build\"
778      "
779    })
780    .run();
781  }
782
783  #[test]
784  fn attributes_no_parameters_needed() {
785    Test::new(indoc! {
786      "
787      [script]
788      foo:
789        echo \"foo\"
790      "
791    })
792    .run();
793
794    Test::new(indoc! {
795      "
796      [confirm]
797      foo:
798        echo \"foo\"
799      "
800    })
801    .run();
802
803    Test::new(indoc! {
804      "
805      [default]
806      foo:
807        echo \"foo\"
808      "
809    })
810    .run();
811  }
812
813  #[test]
814  fn attributes_on_assignments() {
815    Test::new(indoc! {
816      "
817      [private]
818      secret := \"secret value\"
819
820      [private]
821      _db_url := \"postgres://user:pass@host:port/db\"
822
823      public_var := \"public value\"
824
825      test:
826        echo {{ secret }}
827        echo {{ _db_url }}
828        echo {{ public_var }}
829      "
830    })
831    .run();
832  }
833
834  #[test]
835  fn attributes_on_exported_assignments() {
836    Test::new(indoc! {
837      "
838      [private]
839      export PATH := '/usr/local/bin'
840      "
841    })
842    .run();
843  }
844
845  #[test]
846  fn attributes_unknown() {
847    Test::new(indoc! {
848      "
849      [unknown_attribute]
850      foo:
851        echo \"foo\"
852      "
853    })
854    .error(
855      "Unknown attribute `unknown_attribute`",
856      lsp::Range::at(0, 1, 0, 18),
857    )
858    .run();
859  }
860
861  #[test]
862  fn attributes_working_directory_conflicts_with_no_cd() {
863    Test::new(indoc! {
864      "
865      [no-cd]
866      [working-directory: '/tmp']
867      build:
868        echo \"build\"
869      "
870    })
871    .error(
872      "Recipe `build` can't combine `[working-directory]` with `[no-cd]`",
873      lsp::Range::at(1, 0, 2, 0),
874    )
875    .run();
876  }
877
878  #[test]
879  fn attributes_exit_message_conflicts_with_no_exit_message() {
880    Test::new(indoc! {
881      "
882      [exit-message]
883      [no-exit-message]
884      build:
885        echo \"build\"
886      "
887    })
888    .error(
889      "Recipe `build` can't combine `[exit-message]` with `[no-exit-message]`",
890      lsp::Range::at(0, 0, 1, 0),
891    )
892    .run();
893  }
894
895  #[test]
896  fn attributes_wrong_target() {
897    Test::new(indoc! {
898      "
899      [group: 'foo']
900      alias f := foo
901
902      foo:
903        echo \"foo\"
904      "
905    })
906    .error(
907      "Attribute `group` cannot be applied to alias target",
908      lsp::Range::at(0, 0, 1, 0),
909    )
910    .run();
911  }
912
913  #[test]
914  fn bsd_os_specific_no_conflict() {
915    Test::new(indoc! {
916      "
917      [dragonfly]
918      build:
919        echo \"Building on DragonFly BSD\"
920
921      [freebsd]
922      build:
923        echo \"Building on FreeBSD\"
924
925      [netbsd]
926      build:
927        echo \"Building on NetBSD\"
928
929      [openbsd]
930      build:
931        echo \"Building on OpenBSD\"
932      "
933    })
934    .run();
935  }
936
937  #[test]
938  fn circular_dependencies_long_chain() {
939    Test::new(indoc! {
940      "
941      foo: bar
942        echo \"foo\"
943
944      bar: baz
945        echo \"bar\"
946
947      baz: foo
948        echo \"baz\"
949      "
950    })
951    .error(
952      "Recipe `foo` has circular dependency `foo -> bar -> baz -> foo`",
953      lsp::Range::at(0, 0, 3, 0),
954    )
955    .error(
956      "Recipe `bar` has circular dependency `bar -> baz -> foo -> bar`",
957      lsp::Range::at(3, 0, 6, 0),
958    )
959    .error(
960      "Recipe `baz` has circular dependency `baz -> foo -> bar -> baz`",
961      lsp::Range::at(6, 0, 8, 0),
962    )
963    .run();
964  }
965
966  #[test]
967  fn circular_dependencies_multiple_cycles() {
968    Test::new(indoc! {
969      "
970      a: b
971        echo \"a\"
972
973      b: a
974        echo \"b\"
975
976      x: y
977        echo \"x\"
978
979      y: z
980        echo \"y\"
981
982      z: x
983        echo \"z\"
984      "
985    })
986    .error(
987      "Recipe `a` has circular dependency `a -> b -> a`",
988      lsp::Range::at(0, 0, 3, 0),
989    )
990    .error(
991      "Recipe `b` has circular dependency `b -> a -> b`",
992      lsp::Range::at(3, 0, 6, 0),
993    )
994    .error(
995      "Recipe `x` has circular dependency `x -> y -> z -> x`",
996      lsp::Range::at(6, 0, 9, 0),
997    )
998    .error(
999      "Recipe `y` has circular dependency `y -> z -> x -> y`",
1000      lsp::Range::at(9, 0, 12, 0),
1001    )
1002    .error(
1003      "Recipe `z` has circular dependency `z -> x -> y -> z`",
1004      lsp::Range::at(12, 0, 14, 0),
1005    )
1006    .run();
1007  }
1008
1009  #[test]
1010  fn circular_dependencies_only_flags_cycle_members() {
1011    Test::new(indoc! {
1012      "
1013      foo: bar
1014        echo \"foo\"
1015
1016      bar: baz
1017        echo \"bar\"
1018
1019      baz: bar
1020        echo \"baz\"
1021      "
1022    })
1023    .error(
1024      "Recipe `bar` has circular dependency `bar -> baz -> bar`",
1025      lsp::Range::at(3, 0, 6, 0),
1026    )
1027    .error(
1028      "Recipe `baz` has circular dependency `baz -> bar -> baz`",
1029      lsp::Range::at(6, 0, 8, 0),
1030    )
1031    .run();
1032  }
1033
1034  #[test]
1035  fn circular_dependencies_self() {
1036    Test::new(indoc! {
1037      "
1038      foo: foo
1039        echo \"foo\"
1040      "
1041    })
1042    .error("Recipe `foo` depends on itself", lsp::Range::at(0, 0, 2, 0))
1043    .run();
1044  }
1045
1046  #[test]
1047  fn circular_dependencies_simple() {
1048    Test::new(indoc! {
1049      "
1050      foo: bar
1051        echo \"foo\"
1052
1053      bar: foo
1054        echo \"bar\"
1055      "
1056    })
1057    .error(
1058      "Recipe `foo` has circular dependency `foo -> bar -> foo`",
1059      lsp::Range::at(0, 0, 3, 0),
1060    )
1061    .error(
1062      "Recipe `bar` has circular dependency `bar -> foo -> bar`",
1063      lsp::Range::at(3, 0, 5, 0),
1064    )
1065    .run();
1066  }
1067
1068  #[test]
1069  fn circular_dependencies_with_multiple_dependencies() {
1070    Test::new(indoc! {
1071      "
1072      foo: bar baz
1073        echo \"foo\"
1074
1075      bar:
1076        echo \"bar\"
1077
1078      baz: qux
1079        echo \"baz\"
1080
1081      qux: foo
1082        echo \"qux\"
1083      "
1084    })
1085    .error(
1086      "Recipe `foo` has circular dependency `foo -> baz -> qux -> foo`",
1087      lsp::Range::at(0, 0, 3, 0),
1088    )
1089    .error(
1090      "Recipe `baz` has circular dependency `baz -> qux -> foo -> baz`",
1091      lsp::Range::at(6, 0, 9, 0),
1092    )
1093    .error(
1094      "Recipe `qux` has circular dependency `qux -> foo -> baz -> qux`",
1095      lsp::Range::at(9, 0, 11, 0),
1096    )
1097    .run();
1098  }
1099
1100  #[test]
1101  fn comma_separated_os_attributes_no_conflict() {
1102    Test::new(indoc! {
1103      "
1104      [private, unix]
1105      hello:
1106        @echo 'hello'
1107
1108      [private, windows]
1109      hello:
1110        @echo hello
1111      "
1112    })
1113    .run();
1114  }
1115
1116  #[test]
1117  fn comma_separated_os_attributes_with_conflict() {
1118    Test::new(indoc! {
1119      "
1120      [private, linux]
1121      hello:
1122        @echo 'hello on linux'
1123
1124      [private, linux]
1125      hello:
1126        @echo 'hello on linux again'
1127      "
1128    })
1129    .error("Duplicate recipe name `hello`", lsp::Range::at(4, 0, 7, 0))
1130    .run();
1131  }
1132
1133  #[test]
1134  fn comma_separated_unix_windows_no_conflict() {
1135    Test::new(indoc! {
1136      "
1137      [unix]
1138      [private]
1139      build:
1140        @echo 'building on unix'
1141
1142      [private, windows]
1143      build:
1144        @echo 'building on windows'
1145      "
1146    })
1147    .run();
1148  }
1149
1150  #[test]
1151  fn confirm_attribute_accepts_expression() {
1152    Test::new(indoc! {
1153      r#"
1154      [confirm("Deploy to " + env + "?")]
1155      deploy env:
1156        echo {{env}}
1157      "#
1158    })
1159    .run();
1160  }
1161
1162  #[test]
1163  fn cross_parameter_default_references_preceding_parameter() {
1164    Test::new(indoc! {
1165      "
1166      a := 'foo'
1167
1168      foo a b=a:
1169        echo {{ a }} {{ b }}
1170      "
1171    })
1172    .warning("Variable `a` appears unused", lsp::Range::at(0, 0, 0, 1))
1173    .run();
1174  }
1175
1176  #[test]
1177  fn default_parameter_expression_functions() {
1178    Test::new(indoc! {
1179      "
1180      build version=uppercase(\"1.0.0\"):
1181        echo {{ version }}
1182      "
1183    })
1184    .run();
1185  }
1186
1187  #[test]
1188  fn default_parameter_expression_with_env_call() {
1189    Test::new(indoc! {
1190      "
1191      build target=(env('TARGET', 'debug')):
1192        echo {{ target }}
1193      "
1194    })
1195    .run();
1196  }
1197
1198  #[test]
1199  fn dir_aliases_recognized() {
1200    Test::new(indoc! {
1201      "
1202      foo:
1203        echo {{ home_dir() }}
1204        echo {{ cache_dir() }}
1205        echo {{ config_dir() }}
1206        echo {{ config_local_dir() }}
1207        echo {{ data_dir() }}
1208        echo {{ data_local_dir() }}
1209        echo {{ executable_dir() }}
1210        echo {{ invocation_dir() }}
1211        echo {{ invocation_dir_native() }}
1212        echo {{ justfile_dir() }}
1213        echo {{ source_dir() }}
1214        echo {{ parent_dir('~/.config') }}
1215      "
1216    })
1217    .run();
1218  }
1219
1220  #[test]
1221  fn duplicate_dragonfly_attribute() {
1222    Test::new(indoc! {
1223      "
1224      [dragonfly]
1225      [dragonfly]
1226      build:
1227        echo \"foo\"
1228      "
1229    })
1230    .error(
1231      "Recipe attribute `dragonfly` is duplicated",
1232      lsp::Range::at(1, 0, 2, 0),
1233    )
1234    .run();
1235  }
1236
1237  #[test]
1238  fn duplicate_freebsd_attribute() {
1239    Test::new(indoc! {
1240      "
1241      [freebsd]
1242      [freebsd]
1243      build:
1244        echo \"foo\"
1245      "
1246    })
1247    .error(
1248      "Recipe attribute `freebsd` is duplicated",
1249      lsp::Range::at(1, 0, 2, 0),
1250    )
1251    .run();
1252  }
1253
1254  #[test]
1255  fn duplicate_netbsd_attribute() {
1256    Test::new(indoc! {
1257      "
1258      [netbsd]
1259      [netbsd]
1260      build:
1261        echo \"foo\"
1262      "
1263    })
1264    .error(
1265      "Recipe attribute `netbsd` is duplicated",
1266      lsp::Range::at(1, 0, 2, 0),
1267    )
1268    .run();
1269  }
1270
1271  #[test]
1272  fn duplicate_recipe_names() {
1273    Test::new(indoc! {
1274      "
1275      foo:
1276        echo foo
1277
1278      foo:
1279        echo foo
1280
1281      foo:
1282        echo foo
1283      "
1284    })
1285    .error("Duplicate recipe name `foo`", lsp::Range::at(3, 0, 6, 0))
1286    .error("Duplicate recipe name `foo`", lsp::Range::at(6, 0, 8, 0))
1287    .run();
1288  }
1289
1290  #[test]
1291  fn duplicate_recipe_names_allowed_via_setting() {
1292    Test::new(indoc! {
1293      "
1294      set allow-duplicate-recipes := true
1295
1296      foo:
1297        echo foo
1298
1299      [linux]
1300      foo:
1301        echo foo on linux
1302      "
1303    })
1304    .run();
1305  }
1306
1307  #[test]
1308  fn duplicate_recipes_with_same_os_attribute() {
1309    Test::new(indoc! {
1310      "
1311      [linux]
1312      build:
1313        echo \"Building on Linux version 1\"
1314
1315      [linux]
1316      build:
1317        echo \"Building on Linux version 2\"
1318      "
1319    })
1320    .error("Duplicate recipe name `build`", lsp::Range::at(4, 0, 7, 0))
1321    .run();
1322  }
1323
1324  #[test]
1325  fn duplicate_variable_assignments() {
1326    Test::new(indoc! {
1327      "
1328      foo := \"one\"
1329      foo := \"two\"
1330
1331      recipe:
1332        echo {{ foo }}
1333      "
1334    })
1335    .error("Duplicate variable `foo`", lsp::Range::at(1, 0, 2, 0))
1336    .run();
1337  }
1338
1339  #[test]
1340  fn duplicate_variable_assignments_allowed_via_setting() {
1341    Test::new(indoc! {
1342      "
1343      set allow-duplicate-variables := true
1344
1345      foo := \"one\"
1346      foo := \"two\"
1347
1348      recipe:
1349        echo {{ foo }}
1350      "
1351    })
1352    .run();
1353  }
1354
1355  #[test]
1356  fn env_attribute_duplicate_var_name() {
1357    Test::new(indoc! {
1358      "
1359      [env('FOO', 'bar')]
1360      [env('FOO', 'baz')]
1361      foo:
1362        echo \"$FOO\"
1363      "
1364    })
1365    .error(
1366      "Recipe attribute `env` is duplicated",
1367      lsp::Range::at(1, 0, 2, 0),
1368    )
1369    .run();
1370  }
1371
1372  #[test]
1373  fn env_attribute_missing_value() {
1374    Test::new(indoc! {
1375      "
1376      [env('FOO')]
1377      foo:
1378        echo \"$FOO\"
1379      "
1380    })
1381    .error(
1382      "Attribute `env` got 1 argument but takes 2 arguments",
1383      lsp::Range::at(0, 0, 1, 0),
1384    )
1385    .run();
1386  }
1387
1388  #[test]
1389  fn env_attribute_multiple_vars_allowed() {
1390    Test::new(indoc! {
1391      "
1392      [env('FOO', 'bar')]
1393      [env('BAZ', 'qux')]
1394      foo:
1395        echo \"$FOO $BAZ\"
1396      "
1397    })
1398    .run();
1399  }
1400
1401  #[test]
1402  fn env_attribute_too_many_arguments() {
1403    Test::new(indoc! {
1404      "
1405      [env('FOO', 'bar', 'extra')]
1406      foo:
1407        echo \"$FOO\"
1408      "
1409    })
1410    .error(
1411      "Attribute `env` got 3 arguments but takes 2 arguments",
1412      lsp::Range::at(0, 0, 1, 0),
1413    )
1414    .run();
1415  }
1416
1417  #[test]
1418  fn env_attribute_valid() {
1419    Test::new(indoc! {
1420      "
1421      [env('FOO', 'bar')]
1422      foo:
1423        echo \"$FOO\"
1424      "
1425    })
1426    .run();
1427  }
1428
1429  #[test]
1430  fn env_attribute_wrong_target() {
1431    Test::new(indoc! {
1432      "
1433      [env('FOO', 'bar')]
1434      alias baz := foo
1435
1436      foo:
1437        echo \"foo\"
1438      "
1439    })
1440    .error(
1441      "Attribute `env` cannot be applied to alias target",
1442      lsp::Range::at(0, 0, 1, 0),
1443    )
1444    .run();
1445  }
1446
1447  #[test]
1448  fn escaped_braces_are_treated_as_literal_text() {
1449    Test::new(indoc! {
1450      "
1451      test:
1452        echo \"{{{{hello}}\"
1453      "
1454    })
1455    .run();
1456  }
1457
1458  #[test]
1459  fn exported_variables_not_warned() {
1460    Test::new(indoc! {
1461      "
1462      foo := \"unused value\"
1463      export bar := \"exported but unused\"
1464      baz := \"used value\"
1465
1466      recipe:
1467        echo {{ baz }}
1468      "
1469    })
1470    .warning("Variable `foo` appears unused", lsp::Range::at(0, 0, 0, 3))
1471    .run();
1472  }
1473
1474  #[test]
1475  fn expression_attribute_arguments() {
1476    Test::new(indoc! {
1477      "
1478      bar := 'bar'
1479
1480      [confirm('foo' / bar)]
1481      [env('FOO', bar)]
1482      [working-directory('foo' / bar)]
1483      foo:
1484        echo \"foo\"
1485      "
1486    })
1487    .run();
1488  }
1489
1490  #[test]
1491  fn extension_with_script_is_allowed() {
1492    Test::new(indoc! {
1493      "
1494      [script]
1495      [extension: '.sh']
1496      foo:
1497        echo \"foo\"
1498      "
1499    })
1500    .run();
1501  }
1502
1503  #[test]
1504  fn extension_with_shebang_is_allowed() {
1505    Test::new(indoc! {
1506      "
1507      [extension: '.sh']
1508      foo:
1509        #!/usr/bin/env bash
1510        echo \"foo\"
1511      "
1512    })
1513    .run();
1514  }
1515
1516  #[test]
1517  fn extension_without_script_or_shebang() {
1518    Test::new(indoc! {
1519      "
1520      [extension: '.sh']
1521      foo:
1522        echo \"foo\"
1523      "
1524    })
1525    .error(
1526      "Recipe `foo` uses `[extension]` without `[script]` or a shebang",
1527      lsp::Range::at(0, 0, 1, 0),
1528    )
1529    .run();
1530  }
1531
1532  #[test]
1533  fn format_strings_mark_variables_as_used() {
1534    Test::new(indoc! {
1535      r#"
1536      name := "world"
1537      greeting := f'Hello, {{name}}!'
1538      foo:
1539        echo {{greeting}}
1540      "#
1541    })
1542    .run();
1543  }
1544
1545  #[test]
1546  fn format_strings_with_function_calls() {
1547    Test::new(indoc! {
1548      r"
1549      info := f'arch: {{arch()}}'
1550      foo:
1551        echo {{info}}
1552      "
1553    })
1554    .run();
1555  }
1556
1557  #[test]
1558  fn format_strings_with_undefined_variables() {
1559    Test::new(indoc! {
1560      r"
1561      greeting := f'Hello, {{undefined_var}}!'
1562      foo:
1563        echo {{greeting}}
1564      "
1565    })
1566    .error(
1567      "Variable `undefined_var` not found",
1568      lsp::Range::at(0, 23, 0, 36),
1569    )
1570    .run();
1571  }
1572
1573  #[test]
1574  fn format_strings_with_valid_variables() {
1575    Test::new(indoc! {
1576      r#"
1577      name := "world"
1578      greeting := f'Hello, {{name}}!'
1579      foo:
1580        echo {{greeting}}
1581      "#
1582    })
1583    .run();
1584  }
1585
1586  #[test]
1587  fn function_calls_correct() {
1588    Test::new(indoc! {
1589      "
1590      foo:
1591        echo {{ arch() }}
1592        echo {{ join(\"a\", \"b\", \"c\") }}
1593      "
1594    })
1595    .run();
1596  }
1597
1598  #[test]
1599  fn function_calls_nested() {
1600    Test::new(indoc! {
1601      "
1602      foo:
1603        echo {{ replace(parent_directory('~/.config/nvim/init.lua'), '.', 'dot-') }}
1604      "
1605    })
1606    .run();
1607  }
1608
1609  #[test]
1610  fn function_calls_too_few_args() {
1611    Test::new(indoc! {
1612      "
1613      foo:
1614        echo {{ replace() }}
1615      "
1616    })
1617    .error(
1618      "Function `replace` requires at least 3 arguments, but 0 provided",
1619      lsp::Range::at(1, 10, 1, 19),
1620    )
1621    .run();
1622  }
1623
1624  #[test]
1625  fn function_calls_too_many_args() {
1626    Test::new(indoc! {
1627      "
1628      foo:
1629        echo {{ uppercase(\"hello\", \"extra\") }}
1630      "
1631    })
1632    .error(
1633      "Function `uppercase` accepts 1 argument, but 2 provided",
1634      lsp::Range::at(1, 10, 1, 37),
1635    )
1636    .run();
1637  }
1638
1639  #[test]
1640  fn function_calls_unknown() {
1641    Test::new(indoc! {
1642      "
1643      foo:
1644        echo {{ unknown_function() }}
1645      "
1646    })
1647    .error(
1648      "Unknown function `unknown_function`",
1649      lsp::Range::at(1, 10, 1, 26),
1650    )
1651    .run();
1652  }
1653
1654  #[test]
1655  fn import_format_string_skipped() {
1656    Test::new(indoc! {
1657      r#"
1658      import f"{'nonexistent.just'}"
1659      "#
1660    })
1661    .run();
1662  }
1663
1664  #[test]
1665  fn import_invalid_path() {
1666    let expected = if cfg!(windows) {
1667      "Import path does not exist: `C:\\nonexistent.just`"
1668    } else {
1669      "Import path does not exist: `/nonexistent.just`"
1670    };
1671
1672    Test::new(indoc! {
1673      "
1674      import 'nonexistent.just'
1675      "
1676    })
1677    .error(expected, lsp::Range::at(0, 7, 0, 25))
1678    .run();
1679  }
1680
1681  #[test]
1682  fn import_optional_invalid_path() {
1683    Test::new(indoc! {
1684      "
1685      import? 'nonexistent.just'
1686      "
1687    })
1688    .run();
1689  }
1690
1691  #[test]
1692  fn import_shell_expanded_string_skipped() {
1693    Test::new(indoc! {
1694      "
1695      import x'nonexistent.just'
1696      "
1697    })
1698    .run();
1699  }
1700
1701  #[test]
1702  fn linux_openbsd_no_conflict() {
1703    Test::new(indoc! {
1704      "
1705      [linux]
1706      build:
1707        echo \"Building on Linux\"
1708
1709      [openbsd]
1710      build:
1711        echo \"Building on OpenBSD\"
1712      "
1713    })
1714    .run();
1715  }
1716
1717  #[test]
1718  fn linux_unix_conflict() {
1719    Test::new(indoc! {
1720      "
1721      [linux]
1722      build:
1723        echo \"Building on Linux\"
1724
1725      [unix]
1726      build:
1727        echo \"Building on Unix systems\"
1728      "
1729    })
1730    .error("Duplicate recipe name `build`", lsp::Range::at(4, 0, 7, 0))
1731    .run();
1732  }
1733
1734  #[test]
1735  fn mixed_os_specific_and_regular_recipe() {
1736    Test::new(indoc! {
1737      "
1738      [linux]
1739      build:
1740        echo \"Building on Linux\"
1741
1742      build:
1743        echo \"Building on any OS\"
1744      "
1745    })
1746    .error("Duplicate recipe name `build`", lsp::Range::at(4, 0, 6, 0))
1747    .run();
1748  }
1749
1750  #[test]
1751  fn module_attributes_group() {
1752    Test::new(indoc! {
1753      "
1754      [group: 'tools']
1755      mod foo
1756      "
1757    })
1758    .run();
1759  }
1760
1761  #[test]
1762  fn openbsd_macos_no_conflict() {
1763    Test::new(indoc! {
1764      "
1765      [openbsd]
1766      build:
1767        echo \"Building on OpenBSD\"
1768
1769      [macos]
1770      build:
1771        echo \"Building on macOS\"
1772      "
1773    })
1774    .run();
1775  }
1776
1777  #[test]
1778  fn os_specific_duplicate_recipes() {
1779    Test::new(indoc! {
1780      "
1781      [linux]
1782      build:
1783        echo \"Building on Linux\"
1784
1785      [windows]
1786      build:
1787        echo \"Building on Windows\"
1788
1789      [macos]
1790      build:
1791        echo \"Building on macOS\"
1792      "
1793    })
1794    .run();
1795  }
1796
1797  #[test]
1798  fn parallel_with_single_dependency_warns() {
1799    Test::new(indoc! {
1800      "
1801      [parallel]
1802      foo: bar
1803        echo \"foo\"
1804
1805      bar:
1806        echo \"bar\"
1807      "
1808    })
1809    .warning(
1810      "Recipe `foo` has only one dependency, so `[parallel]` has no effect",
1811      lsp::Range::at(0, 0, 1, 0),
1812    )
1813    .run();
1814  }
1815
1816  #[test]
1817  fn parallel_without_dependencies_warns() {
1818    Test::new(indoc! {
1819      "
1820      [parallel]
1821      foo:
1822        echo \"foo\"
1823      "
1824    })
1825    .warning(
1826      "Recipe `foo` has no dependencies, so `[parallel]` has no effect",
1827      lsp::Range::at(0, 0, 1, 0),
1828    )
1829    .run();
1830  }
1831
1832  #[test]
1833  fn parameter_default_references_preceding_parameter() {
1834    Test::new(indoc! {
1835      "
1836      @binstall crate bin=crate:
1837        which {{bin}} 2>&1 >/dev/null || cargo binstall -y {{crate}}
1838      "
1839    })
1840    .run();
1841  }
1842
1843  #[test]
1844  fn parenthesized_expression_default_uses_global_variable() {
1845    Test::new(indoc! {
1846      "
1847      foo := 'foo'
1848      bar := 'bar'
1849
1850      recipe x=(foo + bar):
1851        echo {{ x }}
1852      "
1853    })
1854    .run();
1855  }
1856
1857  #[test]
1858  fn parser_errors_invalid() {
1859    Test::new(indoc! {
1860      "
1861      foo
1862        echo \"foo\"
1863      "
1864    })
1865    .error(
1866      "Syntax error near `foo echo \"foo\"`",
1867      lsp::Range::at(0, 0, 2, 0),
1868    )
1869    .run();
1870  }
1871
1872  #[test]
1873  fn parser_errors_valid() {
1874    Test::new(indoc! {
1875      "
1876      foo:
1877        echo \"foo\"
1878      "
1879    })
1880    .run();
1881  }
1882
1883  #[test]
1884  fn parser_errors_valid_with_recipe_line_containing_only_open_brace() {
1885    Test::new(indoc! {
1886      r#"
1887      foo bar="baz" qux="quux":
1888        #!/usr/bin/env bash
1889        cat <<'JSON'
1890        {
1891          "foo": "bar",
1892          "{{ bar }}": "{{ qux }}"
1893        }
1894        JSON
1895      "#
1896    })
1897    .run();
1898  }
1899
1900  #[test]
1901  fn parser_errors_valid_with_shell_expanded_strings() {
1902    Test::new(indoc! {
1903      r#"
1904      import x'~/.config/just/common.just'
1905
1906      greeting := x"~/$USER/${GREETING:-hello}"
1907
1908      foo:
1909        echo {{greeting}}
1910      "#
1911    })
1912    .run();
1913  }
1914
1915  #[test]
1916  fn positional_arguments_attribute_marks_parameters_as_used() {
1917    Test::new(indoc! {
1918      "
1919      [positional-arguments]
1920      graph log:
1921        ./bin/graph $1
1922      "
1923    })
1924    .run();
1925  }
1926
1927  #[test]
1928  fn positional_arguments_attribute_scope_is_limited() {
1929    Test::new(indoc! {
1930      "
1931      [positional-arguments]
1932      graph log:
1933        ./bin/graph $1
1934
1935      other data:
1936        ./bin/graph $1
1937      "
1938    })
1939    .warning(
1940      "Parameter `data` appears unused",
1941      lsp::Range::at(4, 6, 4, 10),
1942    )
1943    .run();
1944  }
1945
1946  #[test]
1947  fn positional_arguments_disabled_still_warns() {
1948    Test::new(indoc! {
1949      "
1950      graph log:
1951        ./bin/graph $1
1952      "
1953    })
1954    .warning("Parameter `log` appears unused", lsp::Range::at(0, 6, 0, 9))
1955    .run();
1956  }
1957
1958  #[test]
1959  fn positional_arguments_dollar_at_marks_all_as_used() {
1960    Test::new(indoc! {
1961      r#"
1962      [positional-arguments]
1963      run *args:
1964        #!/usr/bin/env bash
1965        exec "$@"
1966      "#
1967    })
1968    .run();
1969  }
1970
1971  #[test]
1972  fn positional_arguments_only_mark_used_indices() {
1973    Test::new(indoc! {
1974      "
1975      set positional-arguments := true
1976
1977      graph first second:
1978        ./bin/graph $2
1979      "
1980    })
1981    .warning(
1982      "Parameter `first` appears unused",
1983      lsp::Range::at(2, 6, 2, 11),
1984    )
1985    .run();
1986  }
1987
1988  #[test]
1989  fn positional_arguments_setting_handles_multiple_parameters() {
1990    Test::new(indoc! {
1991      "
1992      set positional-arguments := true
1993
1994      graph first second third:
1995        ./bin/graph $1 ${2} $3
1996      "
1997    })
1998    .run();
1999  }
2000
2001  #[test]
2002  fn positional_arguments_setting_handles_multiple_parameters_unused() {
2003    Test::new(indoc! {
2004      "
2005      set positional-arguments := true
2006
2007      graph first second third fourth:
2008        ./bin/graph $1 ${2} $3
2009      "
2010    })
2011    .warning(
2012      "Parameter `fourth` appears unused",
2013      lsp::Range::at(2, 25, 2, 31),
2014    )
2015    .run();
2016  }
2017
2018  #[test]
2019  fn positional_arguments_setting_marks_parameters_as_used() {
2020    Test::new(indoc! {
2021      "
2022      set positional-arguments := true
2023
2024      graph log:
2025        ./bin/graph $1
2026      "
2027    })
2028    .run();
2029  }
2030
2031  #[test]
2032  fn positional_arguments_shebang_marks_all_as_used() {
2033    Test::new(indoc! {
2034      r"
2035      [positional-arguments]
2036      run *args:
2037        #!/usr/bin/env -S deno run
2038        console.log(Deno.args)
2039      "
2040    })
2041    .run();
2042  }
2043
2044  #[test]
2045  fn recipe_consistent_indentation() {
2046    Test::new("foo:\n  echo \"foo\"\n  echo \"bar\"\n").run();
2047  }
2048
2049  #[test]
2050  fn recipe_dependencies_correct() {
2051    Test::new(indoc! {
2052      "
2053      foo:
2054        echo \"foo\"
2055
2056      bar: foo
2057        echo \"bar\"
2058      "
2059    })
2060    .run();
2061  }
2062
2063  #[test]
2064  fn recipe_dependencies_duplicate_warns() {
2065    Test::new(indoc! {
2066      "
2067      foo:
2068        echo \"foo\"
2069
2070      bar: foo foo
2071        echo \"bar\"
2072      "
2073    })
2074    .warning(
2075      "Recipe `bar` lists dependency `foo` more than once; just only runs it once, so it's redundant",
2076    lsp::Range::at(3, 9, 3, 12))
2077    .run();
2078  }
2079
2080  #[test]
2081  fn recipe_dependencies_duplicate_with_arguments_warns() {
2082    Test::new(indoc! {
2083      "
2084      foo arg1:
2085        echo \"{{arg1}}\"
2086
2087      bar: (foo `a`) (foo `a`)
2088        echo \"bar\"
2089      "
2090    })
2091    .warning(
2092      "Recipe `bar` lists dependency `foo` with the same arguments more than once; just only runs it once, so it's redundant",
2093    lsp::Range::at(3, 15, 3, 24))
2094    .run();
2095  }
2096
2097  #[test]
2098  fn recipe_dependencies_missing() {
2099    Test::new(indoc! {
2100      "
2101      foo:
2102        echo \"foo\"
2103
2104      bar: baz
2105        echo \"bar\"
2106      "
2107    })
2108    .error("Recipe `baz` not found", lsp::Range::at(3, 5, 3, 8))
2109    .run();
2110  }
2111
2112  #[test]
2113  fn recipe_dependencies_multiple_missing() {
2114    Test::new(indoc! {
2115      "
2116      foo:
2117        echo \"foo\"
2118
2119      bar: missing1 missing2
2120        echo \"bar\"
2121      "
2122    })
2123    .error("Recipe `missing1` not found", lsp::Range::at(3, 5, 3, 13))
2124    .error("Recipe `missing2` not found", lsp::Range::at(3, 14, 3, 22))
2125    .run();
2126  }
2127
2128  #[test]
2129  fn recipe_dependencies_with_different_arguments_no_warning() {
2130    Test::new(indoc! {
2131      "
2132      foo arg1:
2133        echo \"{{arg1}}\"
2134
2135      bar: (foo `a`) (foo `b`)
2136        echo \"bar\"
2137      "
2138    })
2139    .run();
2140  }
2141
2142  #[test]
2143  fn recipe_dependencies_with_expressions() {
2144    Test::new(indoc! {
2145      "
2146      recipe-a param:
2147        echo {{param}}
2148
2149      recipe-b param: (recipe-a (\"##\" + param + \"##\"))
2150        echo \"recipe-b called with {{param}}\"
2151      "
2152    })
2153    .run();
2154  }
2155
2156  #[test]
2157  fn recipe_dependencies_with_multiple_expression_arguments() {
2158    Test::new(indoc! {
2159      "
2160      recipe-a a b:
2161        echo {{a}} {{b}}
2162
2163      recipe-b param: (recipe-a (\"1\") (\"2\"))
2164        echo \"recipe-b called with {{param}}\"
2165      "
2166    })
2167    .run();
2168  }
2169
2170  #[test]
2171  fn recipe_inconsistent_indentation_between_lines() {
2172    Test::new("foo:\n        echo \"foo\"\n  echo \"bar\"\n")
2173    .error(
2174      "Recipe line has inconsistent leading whitespace. Recipe started with `␠␠␠␠␠␠␠␠` but found line with `␠␠`", lsp::Range::at(3, 0, 3, 2))
2175    .run();
2176  }
2177
2178  #[test]
2179  fn recipe_invocation_argument_count_correct() {
2180    Test::new(indoc! {
2181      "
2182      foo arg1 arg2=\"default\":
2183        echo \"{{arg1}} {{arg2}}\"
2184
2185      bar: (foo `value1`)
2186        echo \"bar\"
2187      "
2188    })
2189    .run();
2190  }
2191
2192  #[test]
2193  fn recipe_invocation_missing_args() {
2194    Test::new(indoc! {
2195      "
2196      foo arg1 arg2:
2197        echo \"{{arg1}} {{arg2}}\"
2198
2199      bar: (foo)
2200        echo \"bar\"
2201      "
2202    })
2203    .error(
2204      "Dependency `foo` requires 2 arguments, but 0 provided",
2205      lsp::Range::at(3, 5, 3, 10),
2206    )
2207    .run();
2208  }
2209
2210  #[test]
2211  fn recipe_invocation_one_or_more_variadic_requires_argument() {
2212    Test::new(indoc! {
2213      "
2214      foo +args:
2215        echo \"{{args}}\"
2216
2217      bar: (foo)
2218        echo \"bar\"
2219      "
2220    })
2221    .error(
2222      "Dependency `foo` requires 1 argument, but 0 provided",
2223      lsp::Range::at(3, 5, 3, 10),
2224    )
2225    .run();
2226  }
2227
2228  #[test]
2229  fn recipe_invocation_too_few_args() {
2230    Test::new(indoc! {
2231      "
2232      foo arg1 arg2:
2233        echo \"{{arg1}} {{arg2}}\"
2234
2235      bar: (foo `value1`)
2236        echo \"bar\"
2237      "
2238    })
2239    .error(
2240      "Dependency `foo` requires 2 arguments, but 1 provided",
2241      lsp::Range::at(3, 5, 3, 19),
2242    )
2243    .run();
2244  }
2245
2246  #[test]
2247  fn recipe_invocation_too_many_args() {
2248    Test::new(indoc! {
2249      "
2250      foo arg1:
2251        echo \"{{arg1}}\"
2252
2253      bar: (foo `value1` `value2` `value3`)
2254        echo \"bar\"
2255      "
2256    })
2257    .error(
2258      "Dependency `foo` accepts 1 argument, but 3 provided",
2259      lsp::Range::at(3, 5, 3, 37),
2260    )
2261    .run();
2262  }
2263
2264  #[test]
2265  fn recipe_invocation_unknown_variable() {
2266    Test::new(indoc! {
2267      "
2268      foo arg1:
2269        echo {{ arg1 }}
2270
2271      bar: (foo wow)
2272        echo \"bar\"
2273      "
2274    })
2275    .error("Variable `wow` not found", lsp::Range::at(3, 10, 3, 13))
2276    .run();
2277  }
2278
2279  #[test]
2280  fn recipe_invocation_valid_variable() {
2281    Test::new(indoc! {
2282      "
2283      wow := `foo`
2284
2285      foo arg1:
2286        echo \"{{arg1}}\"
2287
2288      bar: (foo wow)
2289        echo \"bar\"
2290      "
2291    })
2292    .run();
2293  }
2294
2295  #[test]
2296  fn recipe_invocation_variadic_params() {
2297    Test::new(indoc! {
2298      "
2299      foo arg1 +args:
2300        echo \"{{arg1}} {{args}}\"
2301
2302      bar: (foo 'value1' 'value2' 'value3')
2303        echo \"bar\"
2304      "
2305    })
2306    .run();
2307  }
2308
2309  #[test]
2310  fn recipe_invocation_zero_or_more_variadic_accepts_no_arguments() {
2311    Test::new(indoc! {
2312      "
2313      foo *args:
2314        echo \"{{args}}\"
2315
2316      bar: (foo)
2317        echo \"bar\"
2318      "
2319    })
2320    .run();
2321  }
2322
2323  #[test]
2324  fn recipe_line_continuations_allow_extra_indentation() {
2325    Test::new(indoc! {
2326      "
2327      update-mdbook-theme:
2328        curl \\
2329          https://example.com/resource \\
2330          > docs/theme/index.hbs
2331      "
2332    })
2333    .run();
2334  }
2335
2336  #[test]
2337  fn recipe_mixed_indentation_between_lines() {
2338    Test::new(indoc! {
2339      "
2340      foo:
2341      \techo \"foo\"
2342        echo \"bar\"
2343      "
2344    })
2345    .error(
2346      "Recipe `foo` mixes tabs and spaces for indentation",
2347      lsp::Range::at(3, 0, 3, 2),
2348    )
2349    .run();
2350  }
2351
2352  #[test]
2353  fn recipe_mixed_indentation_single_line_mix() {
2354    Test::new(indoc! {
2355      "
2356      foo:
2357   \t  echo \"foo\"
2358      "
2359    })
2360    .error(
2361      "Recipe `foo` mixes tabs and spaces for indentation",
2362      lsp::Range::at(2, 0, 2, 3),
2363    )
2364    .run();
2365  }
2366
2367  #[test]
2368  fn recipe_named_import() {
2369    Test::new(indoc! {
2370      r"
2371      run: import
2372
2373      import:
2374        body
2375      "
2376    })
2377    .run();
2378  }
2379
2380  #[test]
2381  fn recipe_parameters_defaults_all() {
2382    Test::new(indoc! {
2383      "
2384      recipe_with_defaults arg1=\"first\" arg2=\"second\":
2385        echo \"{{arg1}} {{arg2}}\"
2386      "
2387    })
2388    .run();
2389  }
2390
2391  #[test]
2392  fn recipe_parameters_duplicate() {
2393    Test::new(indoc! {
2394      "
2395      recipe_with_duplicate_param arg1 arg1:
2396        echo \"{{arg1}}\"
2397      "
2398    })
2399    .error("Duplicate parameter `arg1`", lsp::Range::at(0, 33, 0, 37))
2400    .run();
2401  }
2402
2403  #[test]
2404  fn recipe_parameters_order() {
2405    Test::new(indoc! {
2406      "
2407      recipe_with_param_order arg1=\"default\" arg2:
2408        echo \"{{arg1}} {{arg2}}\"
2409      "
2410    })
2411    .error(
2412      "Required parameter `arg2` follows a parameter with a default value",
2413      lsp::Range::at(0, 39, 0, 43),
2414    )
2415    .run();
2416  }
2417
2418  #[test]
2419  fn recipe_parameters_valid() {
2420    Test::new(indoc! {
2421      "
2422      valid_recipe arg1 arg2=\"default\":
2423        echo \"{{arg1}} {{arg2}}\"
2424      "
2425    })
2426    .run();
2427  }
2428
2429  #[test]
2430  fn recipe_parameters_variadic() {
2431    Test::new(indoc! {
2432      "
2433      recipe_with_variadic arg1=\"default\" +args:
2434        echo \"{{arg1}} {{args}}\"
2435      "
2436    })
2437    .run();
2438  }
2439
2440  #[test]
2441  fn recipe_with_all_os_attributes() {
2442    Test::new(indoc! {
2443      "
2444      [linux]
2445      [windows]
2446      [unix]
2447      [macos]
2448      [dragonfly]
2449      [freebsd]
2450      [netbsd]
2451      [openbsd]
2452      build:
2453        echo \"Building everywhere\"
2454
2455      test:
2456        echo \"Testing\"
2457      "
2458    })
2459    .run();
2460  }
2461
2462  #[test]
2463  fn recipe_with_conflicting_multiple_os_attributes() {
2464    Test::new(indoc! {
2465      "
2466      [linux]
2467      [openbsd]
2468      build:
2469        echo \"Building on Linux and OpenBSD\"
2470
2471      [linux]
2472      build:
2473        echo \"Building on Linux again\"
2474      "
2475    })
2476    .error("Duplicate recipe name `build`", lsp::Range::at(5, 0, 8, 0))
2477    .run();
2478  }
2479
2480  #[test]
2481  fn recipe_with_multiple_os_attributes() {
2482    Test::new(indoc! {
2483      "
2484      [windows]
2485      [linux]
2486      build:
2487        echo \"Building on Linux or Windows\"
2488
2489      [linux]
2490      build:
2491        echo \"Building on macOS\"
2492
2493      [macos]
2494      build:
2495        echo \"Building on macOS\"
2496      "
2497    })
2498    .error("Duplicate recipe name `build`", lsp::Range::at(5, 0, 9, 0))
2499    .run();
2500  }
2501
2502  #[test]
2503  fn rule_config_off_suppresses_diagnostic() {
2504    let config = serde_json::from_value::<Config>(serde_json::json!({
2505      "rules": {
2506        "unused-variables": "off"
2507      }
2508    }))
2509    .unwrap();
2510
2511    Test::new(indoc! {
2512      "
2513      foo := \"unused value\"
2514
2515      recipe:
2516        echo foo
2517      "
2518    })
2519    .config(config)
2520    .run();
2521  }
2522
2523  #[test]
2524  fn rule_config_overrides_severity_to_error() {
2525    let config = serde_json::from_value::<Config>(serde_json::json!({
2526      "rules": {
2527        "unused-variables": "error"
2528      }
2529    }))
2530    .unwrap();
2531
2532    Test::new(indoc! {
2533      "
2534      foo := \"unused value\"
2535
2536      recipe:
2537        echo foo
2538      "
2539    })
2540    .config(config)
2541    .error("Variable `foo` appears unused", lsp::Range::at(0, 0, 0, 3))
2542    .run();
2543  }
2544
2545  #[test]
2546  fn rule_config_overrides_severity_to_warning() {
2547    let config = serde_json::from_value::<Config>(serde_json::json!({
2548      "rules": {
2549        "missing-dependencies": "warning"
2550      }
2551    }))
2552    .unwrap();
2553
2554    Test::new(indoc! {
2555      "
2556      foo:
2557        echo \"foo\"
2558
2559      bar: baz
2560        echo \"bar\"
2561      "
2562    })
2563    .config(config)
2564    .warning("Recipe `baz` not found", lsp::Range::at(3, 5, 3, 8))
2565    .run();
2566  }
2567
2568  #[test]
2569  fn script_attribute_with_shebang_conflict() {
2570    Test::new(indoc! {
2571      "
2572      [script]
2573      publish:
2574        #!/usr/bin/env bash
2575        echo \"publish\"
2576      "
2577    })
2578    .error(
2579      "Recipe `publish` has both shebang line and `[script]` attribute",
2580      lsp::Range::at(0, 0, 1, 0),
2581    )
2582    .run();
2583  }
2584
2585  #[test]
2586  fn script_attribute_without_shebang_is_allowed() {
2587    Test::new(indoc! {
2588      "
2589      [script]
2590      publish:
2591        echo \"publish\"
2592      "
2593    })
2594    .run();
2595  }
2596
2597  #[test]
2598  fn set_export_suppresses_unused_variable_warnings() {
2599    Test::new(indoc! {
2600      "
2601      set export
2602
2603      foo := 'bar'
2604      baz := 'qux'
2605
2606      recipe:
2607        echo $foo
2608      "
2609    })
2610    .run();
2611  }
2612
2613  #[test]
2614  fn settings_boolean_shorthand() {
2615    Test::new(indoc! {
2616      "
2617      set export
2618
2619      foo:
2620        echo \"foo\"
2621      "
2622    })
2623    .run();
2624  }
2625
2626  #[test]
2627  fn settings_boolean_type_correct() {
2628    Test::new(indoc! {
2629      "
2630      set export := true
2631      set dotenv-load := false
2632
2633      foo:
2634        echo \"foo\"
2635      "
2636    })
2637    .run();
2638  }
2639
2640  #[test]
2641  fn settings_boolean_type_error() {
2642    Test::new(indoc! {
2643      "
2644      set export := 'foo'
2645
2646      foo:
2647        echo \"foo\"
2648      "
2649    })
2650    .error(
2651      "Setting `export` expects a boolean value",
2652      lsp::Range::at(0, 0, 1, 0),
2653    )
2654    .run();
2655  }
2656
2657  #[test]
2658  fn settings_boolean_type_error_with_expression() {
2659    Test::new(indoc! {
2660      "
2661      env := 'true'
2662      set export := env
2663
2664      foo:
2665        echo \"foo\"
2666      "
2667    })
2668    .error(
2669      "Setting `export` expects a boolean value",
2670      lsp::Range::at(1, 0, 2, 0),
2671    )
2672    .run();
2673  }
2674
2675  #[test]
2676  fn settings_duplicate() {
2677    Test::new(indoc! {
2678      "
2679      set export := true
2680      set shell := [\"bash\", \"-c\"]
2681      set export := false
2682
2683      foo:
2684        echo \"foo\"
2685      "
2686    })
2687    .error("Duplicate setting `export`", lsp::Range::at(2, 0, 3, 0))
2688    .run();
2689  }
2690
2691  #[test]
2692  fn settings_guards_recognized() {
2693    Test::new(indoc! {
2694      "
2695      set guards
2696
2697      foo:
2698        echo \"foo\"
2699      "
2700    })
2701    .run();
2702  }
2703
2704  #[test]
2705  fn settings_lazy_recognized() {
2706    Test::new(indoc! {
2707      "
2708      set lazy
2709
2710      foo:
2711        echo \"foo\"
2712      "
2713    })
2714    .run();
2715  }
2716
2717  #[test]
2718  fn settings_multiple_errors() {
2719    Test::new(indoc! {
2720      "
2721      set unknown-setting := true
2722      set export := false
2723      set shell := ['bash']
2724      set export := false
2725
2726      foo:
2727        echo \"foo\"
2728      "
2729    })
2730    .error(
2731      "Unknown setting `unknown-setting`",
2732      lsp::Range::at(0, 0, 1, 0),
2733    )
2734    .error("Duplicate setting `export`", lsp::Range::at(3, 0, 4, 0))
2735    .run();
2736  }
2737
2738  #[test]
2739  fn settings_shell_array_accepts_shell_expanded_strings() {
2740    Test::new(indoc! {
2741      r#"
2742      set shell := [x"${SHELL_BIN:-bash}", x"-c"]
2743
2744      foo:
2745        echo "foo"
2746      "#
2747    })
2748    .run();
2749  }
2750
2751  #[test]
2752  fn settings_string_type_correct() {
2753    Test::new(indoc! {
2754      "
2755      set dotenv-path := \".env.development\"
2756
2757      foo:
2758        echo \"foo\"
2759      "
2760    })
2761    .run();
2762  }
2763
2764  #[test]
2765  fn settings_string_type_correct_with_expression() {
2766    Test::new(indoc! {
2767      "
2768      env := 'development'
2769      set dotenv-path := '.env.' + env
2770
2771      foo:
2772        echo \"foo\"
2773      "
2774    })
2775    .run();
2776  }
2777
2778  #[test]
2779  fn settings_string_type_correct_with_shell_expanded_string() {
2780    Test::new(indoc! {
2781      r#"
2782      set dotenv-path := x"~/.env.${JUST_ENV:-development}"
2783
2784      foo:
2785        echo "foo"
2786      "#
2787    })
2788    .run();
2789  }
2790
2791  #[test]
2792  fn settings_string_type_error() {
2793    Test::new(indoc! {
2794      "
2795      set dotenv-path := true
2796
2797      foo:
2798        echo \"foo\"
2799      "
2800    })
2801    .error(
2802      "Setting `dotenv-path` expects a string value",
2803      lsp::Range::at(0, 0, 1, 0),
2804    )
2805    .run();
2806  }
2807
2808  #[test]
2809  fn settings_unknown() {
2810    Test::new(indoc! {
2811      "
2812      set unknown-setting := true
2813
2814      foo:
2815        echo \"foo\"
2816      "
2817    })
2818    .error(
2819      "Unknown setting `unknown-setting`",
2820      lsp::Range::at(0, 0, 1, 0),
2821    )
2822    .run();
2823  }
2824
2825  #[test]
2826  fn settings_unknown_with_expression() {
2827    Test::new(indoc! {
2828      "
2829      value := 'bar'
2830      set unknown-setting := value
2831
2832      foo:
2833        echo \"foo\"
2834      "
2835    })
2836    .error(
2837      "Unknown setting `unknown-setting`",
2838      lsp::Range::at(1, 0, 2, 0),
2839    )
2840    .run();
2841  }
2842
2843  #[test]
2844  fn shadowed_parameter_default_uses_global_variable() {
2845    Test::new(indoc! {
2846      "
2847      a := 'default a'
2848
2849      b a=a:
2850        echo {{ a }}
2851      "
2852    })
2853    .run();
2854  }
2855
2856  #[test]
2857  fn shebang_recipe_is_exempt_from_inconsistent_indentation() {
2858    Test::new(indoc! {
2859      "
2860      build-docs:
2861        #!/usr/bin/env bash
2862        mdbook build docs -d build
2863        for language in ar de; do
2864          echo $language
2865        done
2866      "
2867    })
2868    .run();
2869  }
2870
2871  #[test]
2872  fn should_recognize_recipe_parameters_in_dependency_arguments() {
2873    Test::new(indoc! {
2874      "
2875      other-recipe var=\"else\":
2876        echo {{ var }}
2877
2878      test var=\"something\": (other-recipe var)
2879      "
2880    })
2881    .run();
2882  }
2883
2884  #[test]
2885  fn unexported_variables_warned() {
2886    Test::new(indoc! {
2887      "
2888      foo := \"unused value\"
2889      unexport BAR := \"unexported but unused\"
2890      baz := \"used value\"
2891
2892      recipe:
2893        echo {{ baz }}
2894      "
2895    })
2896    .warning("Variable `foo` appears unused", lsp::Range::at(0, 0, 0, 3))
2897    .warning("Variable `BAR` appears unused", lsp::Range::at(1, 9, 1, 12))
2898    .run();
2899  }
2900
2901  #[test]
2902  fn unix_dragonfly_conflict() {
2903    Test::new(indoc! {
2904      "
2905      [unix]
2906      build:
2907        echo \"Building on Unix systems\"
2908
2909      [dragonfly]
2910      build:
2911        echo \"Building on DragonFly BSD\"
2912      "
2913    })
2914    .error("Duplicate recipe name `build`", lsp::Range::at(4, 0, 7, 0))
2915    .run();
2916  }
2917
2918  #[test]
2919  fn unix_freebsd_conflict() {
2920    Test::new(indoc! {
2921      "
2922      [unix]
2923      build:
2924        echo \"Building on Unix systems\"
2925
2926      [freebsd]
2927      build:
2928        echo \"Building on FreeBSD\"
2929      "
2930    })
2931    .error("Duplicate recipe name `build`", lsp::Range::at(4, 0, 7, 0))
2932    .run();
2933  }
2934
2935  #[test]
2936  fn unix_macos_conflicts() {
2937    Test::new(indoc! {
2938      "
2939      [unix]
2940      build:
2941        echo \"Building on Unix systems\"
2942
2943      [macos]
2944      build:
2945        echo \"Building on macOS specifically\"
2946      "
2947    })
2948    .error("Duplicate recipe name `build`", lsp::Range::at(4, 0, 7, 0))
2949    .run();
2950  }
2951
2952  #[test]
2953  fn unix_netbsd_conflict() {
2954    Test::new(indoc! {
2955      "
2956      [unix]
2957      build:
2958        echo \"Building on Unix systems\"
2959
2960      [netbsd]
2961      build:
2962        echo \"Building on NetBSD\"
2963      "
2964    })
2965    .error("Duplicate recipe name `build`", lsp::Range::at(4, 0, 7, 0))
2966    .run();
2967  }
2968
2969  #[test]
2970  fn unknown_default_recipe_parameter_reference() {
2971    Test::new(indoc! {
2972      "
2973      recipe arg=foo:
2974        echo {{ arg }}
2975      "
2976    })
2977    .error("Variable `foo` not found", lsp::Range::at(0, 11, 0, 14))
2978    .run();
2979  }
2980
2981  #[test]
2982  fn unreferenced_variable_in_expression() {
2983    Test::new(indoc! {
2984      "
2985      foo:
2986        echo {{ var }}
2987      "
2988    })
2989    .error("Variable `var` not found", lsp::Range::at(1, 10, 1, 13))
2990    .run();
2991  }
2992
2993  #[test]
2994  fn used_variables_no_warnings() {
2995    Test::new(indoc! {
2996      "
2997      foo := \"used in recipe\"
2998      bar := \"used as dependency arg\"
2999
3000      another arg:
3001        echo {{ arg }}
3002
3003      recipe: (another bar)
3004        echo {{ foo }}
3005      "
3006    })
3007    .run();
3008  }
3009
3010  #[test]
3011  fn user_defined_function_body_references_variable() {
3012    Test::new(indoc! {
3013      "
3014      base := \"hello\"
3015
3016      foo(x) := base + x
3017      "
3018    })
3019    .run();
3020  }
3021
3022  #[test]
3023  fn user_defined_function_body_unknown_identifier() {
3024    Test::new(indoc! {
3025      "
3026      foo(x) := x + unknown
3027      "
3028    })
3029    .error("Variable `unknown` not found", lsp::Range::at(0, 14, 0, 21))
3030    .run();
3031  }
3032
3033  #[test]
3034  fn user_defined_function_duplicate_parameters() {
3035    Test::new(indoc! {
3036      "
3037      foo(bar, bar) := bar
3038      "
3039    })
3040    .error("Duplicate parameter `bar`", lsp::Range::at(0, 9, 0, 12))
3041    .run();
3042  }
3043
3044  #[test]
3045  fn user_defined_function_duplicates() {
3046    Test::new(indoc! {
3047      "
3048      foo() := \"bar\"
3049      foo() := \"baz\"
3050      foo() := \"bat\"
3051      "
3052    })
3053    .error("Duplicate function `foo`", lsp::Range::at(1, 0, 2, 0))
3054    .error("Duplicate function `foo`", lsp::Range::at(2, 0, 3, 0))
3055    .run();
3056  }
3057
3058  #[test]
3059  fn user_defined_function_no_params() {
3060    Test::new(indoc! {
3061      "
3062      foo() := \"bar\"
3063
3064      baz:
3065        echo {{ foo() }}
3066      "
3067    })
3068    .run();
3069  }
3070
3071  #[test]
3072  fn user_defined_function_not_flagged_as_unknown() {
3073    Test::new(indoc! {
3074      "
3075      foo(x) := x + \"!\"
3076
3077      bar:
3078        echo {{ foo(\"baz\") }}
3079      "
3080    })
3081    .run();
3082  }
3083
3084  #[test]
3085  fn user_defined_function_parameters_not_unresolved() {
3086    Test::new(indoc! {
3087      "
3088      foo(x) := x + \"!\"
3089      "
3090    })
3091    .run();
3092  }
3093
3094  #[test]
3095  fn user_defined_function_too_few_args() {
3096    Test::new(indoc! {
3097      "
3098      foo(a, b) := a + b
3099
3100      bar:
3101        echo {{ foo(\"a\") }}
3102      "
3103    })
3104    .error(
3105      "Function `foo` accepts 2 arguments, but 1 provided",
3106      lsp::Range::at(3, 10, 3, 18),
3107    )
3108    .run();
3109  }
3110
3111  #[test]
3112  fn user_defined_function_wrong_arity() {
3113    Test::new(indoc! {
3114      "
3115      foo(x) := x + \"!\"
3116
3117      bar:
3118        echo {{ foo(\"a\", \"b\") }}
3119      "
3120    })
3121    .error(
3122      "Function `foo` accepts 1 argument, but 2 provided",
3123      lsp::Range::at(3, 10, 3, 23),
3124    )
3125    .run();
3126  }
3127
3128  #[test]
3129  fn variables_and_parameters_same_name() {
3130    Test::new(indoc! {
3131      "
3132      param := \"variable value\"
3133      other := \"other value\"
3134
3135      recipe param:
3136        # This should reference the parameter, not the variable
3137        echo {{ param }}
3138        echo {{ other }}
3139      "
3140    })
3141    .warning(
3142      "Variable `param` appears unused",
3143      lsp::Range::at(0, 0, 0, 5),
3144    )
3145    .run();
3146  }
3147
3148  #[test]
3149  fn variables_used_after_hash_in_command() {
3150    Test::new(indoc! {
3151      "
3152      flake := \"testflake\"
3153      output := \"testoutput\"
3154
3155      test:
3156        darwin-rebuild switch --flake {{ flake }}#{{ output }}
3157      "
3158    })
3159    .run();
3160  }
3161
3162  #[test]
3163  fn variables_used_in_dependency_args() {
3164    Test::new(indoc! {
3165      "
3166      used_arg := \"value\"
3167      unused_var := \"not used\"
3168
3169      recipe: (another used_arg)
3170        echo \"something\"
3171
3172      another arg:
3173        echo {{ arg }}
3174      "
3175    })
3176    .warning(
3177      "Variable `unused_var` appears unused",
3178      lsp::Range::at(1, 0, 1, 10),
3179    )
3180    .run();
3181  }
3182
3183  #[test]
3184  fn variables_used_in_multiple_recipes() {
3185    Test::new(indoc! {
3186      "
3187      shared := \"shared value\"
3188      only_in_first := \"first value\"
3189      only_in_second := \"second value\"
3190      never_used := \"unused\"
3191
3192      first:
3193        echo {{ shared }}
3194        echo {{ only_in_first }}
3195
3196      second:
3197        echo {{ shared }}
3198        echo {{ only_in_second }}
3199      "
3200    })
3201    .warning(
3202      "Variable `never_used` appears unused",
3203      lsp::Range::at(3, 0, 3, 10),
3204    )
3205    .run();
3206  }
3207
3208  #[test]
3209  fn variables_used_in_recipe_default_parameters() {
3210    Test::new(indoc! {
3211      "
3212      param_value := \"value\"
3213
3214      recipe arg=param_value:
3215        echo {{ arg }}
3216      "
3217    })
3218    .run();
3219  }
3220
3221  #[test]
3222  fn variables_used_in_recipe_dependencies() {
3223    Test::new(indoc! {
3224      "
3225      param_value := \"value\"
3226      unused := \"unused\"
3227
3228      recipe arg=\"default\": (another param_value)
3229        echo {{ arg }}
3230
3231      another arg:
3232        echo {{ arg }}
3233      "
3234    })
3235    .warning(
3236      "Variable `unused` appears unused",
3237      lsp::Range::at(1, 0, 1, 6),
3238    )
3239    .run();
3240  }
3241
3242  #[test]
3243  fn warn_for_unused_non_exported_recipe_parameters() {
3244    Test::new(indoc! {
3245      "
3246      foo bar:
3247        echo foo
3248      "
3249    })
3250    .warning("Parameter `bar` appears unused", lsp::Range::at(0, 4, 0, 7))
3251    .run();
3252
3253    Test::new(indoc! {
3254      "
3255      foo $bar:
3256        echo foo
3257      "
3258    })
3259    .run();
3260
3261    Test::new(indoc! {
3262      "
3263      set export := false
3264
3265      foo bar:
3266        echo foo
3267      "
3268    })
3269    .warning("Parameter `bar` appears unused", lsp::Range::at(2, 4, 2, 7))
3270    .run();
3271
3272    Test::new(indoc! {
3273      "
3274      set export
3275
3276      foo bar:
3277        echo foo
3278      "
3279    })
3280    .run();
3281  }
3282
3283  #[test]
3284  fn warn_for_unused_variables() {
3285    Test::new(indoc! {
3286      "
3287      foo := \"unused value\"
3288      bar := \"used value\"
3289
3290      recipe:
3291        echo {{ bar }}
3292      "
3293    })
3294    .warning("Variable `foo` appears unused", lsp::Range::at(0, 0, 0, 3))
3295    .run();
3296  }
3297
3298  #[test]
3299  fn windows_recipe_conflicts_with_default() {
3300    Test::new(indoc! {
3301      "
3302      [windows]
3303      build:
3304        echo \"Building on Windows\"
3305
3306      build:
3307        echo \"Building on every OS\"
3308      "
3309    })
3310    .error("Duplicate recipe name `build`", lsp::Range::at(4, 0, 6, 0))
3311    .run();
3312  }
3313}