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