1use std::fmt;
4
5use wdl_ast::AstToken;
6use wdl_ast::Diagnostic;
7use wdl_ast::Ident;
8use wdl_ast::Span;
9use wdl_ast::SupportedVersion;
10use wdl_ast::TreeNode;
11use wdl_ast::TreeToken;
12use wdl_ast::Version;
13use wdl_ast::v1::PlaceholderOption;
14use wdl_grammar::Severity;
15
16use crate::MeaninglessLintDirective;
17use crate::MisleadingDeclarationOrderRule;
18use crate::UnnecessaryFunctionCall;
19use crate::UnusedCallRule;
20use crate::UnusedDeclarationRule;
21use crate::UnusedImportRule;
22use crate::UnusedInputRule;
23use crate::types::CallKind;
24use crate::types::CallType;
25use crate::types::Type;
26use crate::types::display_types;
27use crate::types::v1::ComparisonOperator;
28use crate::types::v1::NumericOperator;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum Io {
33 Input,
35 Output,
37}
38
39impl fmt::Display for Io {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 Self::Input => write!(f, "input"),
43 Self::Output => write!(f, "output"),
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum Context {
51 Namespace(Span),
53 Workflow(Span),
55 Task(Span),
57 Struct(Span),
59 StructMember(Span),
61 Enum(Span),
63 EnumChoice(Span),
65 Name(NameContext),
67}
68
69impl Context {
70 fn span(&self) -> Span {
72 match self {
73 Self::Namespace(s) => *s,
74 Self::Workflow(s) => *s,
75 Self::Task(s) => *s,
76 Self::Struct(s) => *s,
77 Self::StructMember(s) => *s,
78 Self::Enum(s) => *s,
79 Self::EnumChoice(s) => *s,
80 Self::Name(n) => n.span(),
81 }
82 }
83}
84
85impl fmt::Display for Context {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 match self {
88 Self::Namespace(_) => write!(f, "namespace"),
89 Self::Workflow(_) => write!(f, "workflow"),
90 Self::Task(_) => write!(f, "task"),
91 Self::Struct(_) => write!(f, "struct"),
92 Self::StructMember(_) => write!(f, "struct member"),
93 Self::Enum(_) => write!(f, "enum"),
94 Self::EnumChoice(_) => write!(f, "enum choice"),
95 Self::Name(n) => n.fmt(f),
96 }
97 }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum NameContext {
103 Input(Span),
105 Output(Span),
107 Decl(Span),
109 Call(Span),
111 ScatterVariable(Span),
113}
114
115impl NameContext {
116 pub fn span(&self) -> Span {
118 match self {
119 Self::Input(s) => *s,
120 Self::Output(s) => *s,
121 Self::Decl(s) => *s,
122 Self::Call(s) => *s,
123 Self::ScatterVariable(s) => *s,
124 }
125 }
126}
127
128impl fmt::Display for NameContext {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 match self {
131 Self::Input(_) => write!(f, "input"),
132 Self::Output(_) => write!(f, "output"),
133 Self::Decl(_) => write!(f, "declaration"),
134 Self::Call(_) => write!(f, "call"),
135 Self::ScatterVariable(_) => write!(f, "scatter variable"),
136 }
137 }
138}
139
140impl From<NameContext> for Context {
141 fn from(context: NameContext) -> Self {
142 Self::Name(context)
143 }
144}
145
146pub fn name_conflict(name: &str, conflicting: Context, first: Context) -> Diagnostic {
148 Diagnostic::error(format!("conflicting {conflicting} name `{name}`"))
149 .with_label(
150 format!("this {conflicting} conflicts with a previously used name"),
151 conflicting.span(),
152 )
153 .with_label(
154 format!("the {first} with the conflicting name is here"),
155 first.span(),
156 )
157}
158
159pub fn cannot_index(actual: &Type, span: Span) -> Diagnostic {
161 Diagnostic::error("indexing is only allowed on `Array` and `Map` types")
162 .with_label(format!("this is {actual:#}"), span)
163}
164
165pub fn unknown_name(name: &str, span: Span) -> Diagnostic {
167 let message = match name {
169 "task" => "the `task` variable may only be used within a task command section or task \
170 output section using WDL 1.2 or later, or within a task requirements, task \
171 hints, or task runtime section using WDL 1.3 or later"
172 .to_string(),
173 _ => format!("unknown name `{name}`"),
174 };
175
176 Diagnostic::error(message).with_highlight(span)
177}
178
179pub fn self_referential(name: &str, span: Span, reference: Span) -> Diagnostic {
181 Diagnostic::error(format!("declaration of `{name}` is self-referential"))
182 .with_label("self-reference is here", reference)
183 .with_highlight(span)
184}
185
186pub fn task_reference_cycle(
188 from: &impl fmt::Display,
189 from_span: Span,
190 to: &str,
191 to_span: Span,
192) -> Diagnostic {
193 Diagnostic::error("a name reference cycle was detected")
194 .with_label(
195 format!("ensure this expression does not directly or indirectly refer to {from}"),
196 to_span,
197 )
198 .with_label(format!("a reference back to `{to}` is here"), from_span)
199}
200
201pub fn workflow_reference_cycle(
203 from: &impl fmt::Display,
204 from_span: Span,
205 to: &str,
206 to_span: Span,
207) -> Diagnostic {
208 Diagnostic::error("a name reference cycle was detected")
209 .with_label(format!("this name depends on {from}"), to_span)
210 .with_label(format!("a reference back to `{to}` is here"), from_span)
211}
212
213pub fn call_conflict<T: TreeToken>(
215 name: &Ident<T>,
216 first: NameContext,
217 suggest_fix: bool,
218) -> Diagnostic {
219 let diagnostic = Diagnostic::error(format!(
220 "conflicting call name `{name}`",
221 name = name.text()
222 ))
223 .with_label(
224 "this call name conflicts with a previously used name",
225 name.span(),
226 )
227 .with_label(
228 format!("the {first} with the conflicting name is here"),
229 first.span(),
230 );
231
232 if suggest_fix {
233 diagnostic.with_fix("add an `as` clause to the call to specify a different name")
234 } else {
235 diagnostic
236 }
237}
238
239pub fn namespace_conflict(
241 name: &str,
242 conflicting: Span,
243 first: Span,
244 suggest_fix: bool,
245) -> Diagnostic {
246 let diagnostic = Diagnostic::error(format!("conflicting import namespace `{name}`"))
247 .with_label("this conflicts with another import namespace", conflicting)
248 .with_label(
249 "the conflicting import namespace was introduced here",
250 first,
251 );
252
253 if suggest_fix {
254 diagnostic.with_fix("add an `as` clause to the import to specify a namespace")
255 } else {
256 diagnostic
257 }
258}
259
260pub fn unknown_namespace<T: TreeToken>(ns: &Ident<T>) -> Diagnostic {
262 Diagnostic::error(format!("unknown namespace `{ns}`", ns = ns.text())).with_highlight(ns.span())
263}
264
265pub fn only_one_namespace(span: Span) -> Diagnostic {
267 Diagnostic::error("only one namespace may be specified in a call statement")
268 .with_highlight(span)
269}
270
271pub fn import_cycle(span: Span) -> Diagnostic {
273 Diagnostic::error("import introduces a dependency cycle")
274 .with_label("this import has been skipped to break the cycle", span)
275}
276
277pub fn import_failure(uri: &str, error: &anyhow::Error, span: Span) -> Diagnostic {
279 Diagnostic::error(format!("failed to import `{uri}`: {error:#}")).with_highlight(span)
280}
281
282pub fn incompatible_import(
284 import_version: &str,
285 import_span: Span,
286 importer_version: &Version,
287) -> Diagnostic {
288 Diagnostic::error("imported document has incompatible version")
289 .with_label(
290 format!("the imported document is version `{import_version}`"),
291 import_span,
292 )
293 .with_label(
294 format!(
295 "the importing document is version `{version}`",
296 version = importer_version.text()
297 ),
298 importer_version.span(),
299 )
300}
301
302pub fn import_missing_version(span: Span) -> Diagnostic {
304 Diagnostic::error("imported document is missing a version statement").with_highlight(span)
305}
306
307pub fn invalid_relative_import(error: &url::ParseError, span: Span) -> Diagnostic {
309 Diagnostic::error(format!("{error:#}")).with_highlight(span)
310}
311
312pub fn wildcard_import_conflict(name: &str, import_span: Span, prev_span: Span) -> Diagnostic {
314 Diagnostic::error(format!(
315 "wildcard import introduces `{name}` which conflicts with an existing definition"
316 ))
317 .with_label("imported here", import_span)
318 .with_label("previous definition", prev_span)
319}
320
321pub fn workflow_conflict(
324 rejected_name: &str,
325 rejected_span: Span,
326 retained_name: &str,
327 retained_span: Span,
328) -> Diagnostic {
329 Diagnostic::error(format!(
330 "cannot add workflow `{rejected_name}` because only one workflow may be in scope"
331 ))
332 .with_label("this workflow is rejected", rejected_span)
333 .with_label(
334 format!("workflow `{retained_name}` is retained here"),
335 retained_span,
336 )
337}
338
339pub fn selected_member_not_found(name: &str, span: Span) -> Diagnostic {
341 Diagnostic::error(format!("`{name}` does not exist in the imported module"))
342 .with_highlight(span)
343}
344
345pub fn selected_import_conflict(name: &str, import_span: Span, prev_span: Span) -> Diagnostic {
347 Diagnostic::error(format!(
348 "import of `{name}` conflicts with an existing definition"
349 ))
350 .with_label("imported here", import_span)
351 .with_label("previous definition", prev_span)
352}
353
354pub fn type_not_in_document<T: TreeToken>(name: &Ident<T>) -> Diagnostic {
356 Diagnostic::error(format!(
357 "a struct or enum named `{name}` does not exist in the imported document",
358 name = name.text()
359 ))
360 .with_label("this type name does not exist", name.span())
361}
362
363pub fn imported_struct_conflict(
365 name: &str,
366 conflicting: Span,
367 first: Span,
368 suggest_fix: bool,
369) -> Diagnostic {
370 let diagnostic = Diagnostic::error(format!("conflicting struct name `{name}`"))
371 .with_label(
372 "this import introduces a conflicting definition",
373 conflicting,
374 )
375 .with_label("the first definition was introduced by this import", first);
376
377 if suggest_fix {
378 diagnostic.with_fix("add an `alias` clause to the import to specify a different name")
379 } else {
380 diagnostic
381 }
382}
383
384pub fn struct_conflicts_with_import(name: &str, conflicting: Span, import: Span) -> Diagnostic {
386 Diagnostic::error(format!("conflicting struct name `{name}`"))
387 .with_label("this name conflicts with an imported struct", conflicting)
388 .with_label("the import that introduced the struct is here", import)
389 .with_fix(
390 "either rename the struct or use an `alias` clause on the import with a different name",
391 )
392}
393
394pub fn imported_enum_conflict(
396 name: &str,
397 conflicting: Span,
398 first: Span,
399 suggest_fix: bool,
400) -> Diagnostic {
401 let diagnostic = Diagnostic::error(format!("conflicting enum name `{name}`"))
402 .with_label(
403 "this import introduces a conflicting definition",
404 conflicting,
405 )
406 .with_label("the first definition was introduced by this import", first);
407
408 if suggest_fix {
409 diagnostic.with_fix("add an `alias` clause to the import to specify a different name")
410 } else {
411 diagnostic
412 }
413}
414
415pub fn enum_conflicts_with_import(name: &str, conflicting: Span, import: Span) -> Diagnostic {
417 Diagnostic::error(format!("conflicting enum name `{name}`"))
418 .with_label("this name conflicts with an imported enum", conflicting)
419 .with_label("the import that introduced the enum is here", import)
420 .with_fix(
421 "either rename the enum or use an `alias` clause on the import with a different name",
422 )
423}
424
425pub fn duplicate_workflow<T: TreeToken>(name: &Ident<T>, first: Span) -> Diagnostic {
427 Diagnostic::error(format!(
428 "cannot define workflow `{name}` as only one workflow is allowed per source file",
429 name = name.text(),
430 ))
431 .with_label("consider moving this workflow to a new file", name.span())
432 .with_label("first workflow is defined here", first)
433}
434
435pub fn recursive_struct(name: &str, span: Span, member: Span) -> Diagnostic {
437 Diagnostic::error(format!("struct `{name}` has a recursive definition"))
438 .with_highlight(span)
439 .with_label("this struct member participates in the recursion", member)
440}
441
442pub fn recursive_enum(name: &str, span: Span, ty: &str) -> Diagnostic {
444 Diagnostic::error(format!("enum `{name}` has a recursive definition"))
448 .with_highlight(span)
449 .with_help(format!("the type `{ty}` participates in the recursion"))
450}
451
452pub fn unknown_type(name: &str, span: Span) -> Diagnostic {
454 Diagnostic::error(format!("unknown type name `{name}`")).with_highlight(span)
455}
456
457pub fn type_mismatch(
459 expected: &Type,
460 expected_span: Span,
461 actual: &Type,
462 actual_span: Span,
463) -> Diagnostic {
464 Diagnostic::error(format!(
465 "type mismatch: expected {expected:#}, but found {actual:#}"
466 ))
467 .with_label(format!("this is {actual:#}"), actual_span)
468 .with_label(format!("this expects {expected:#}"), expected_span)
469}
470
471pub fn non_empty_array_assignment(expected_span: Span, actual_span: Span) -> Diagnostic {
473 Diagnostic::error("cannot assign an empty array to a non-empty array type")
474 .with_label("this is an empty array", actual_span)
475 .with_label("this expects a non-empty array", expected_span)
476}
477
478pub fn call_input_type_mismatch<T: TreeToken>(
480 name: &Ident<T>,
481 expected: &Type,
482 actual: &Type,
483) -> Diagnostic {
484 Diagnostic::error(format!(
485 "type mismatch: expected {expected:#}, but found {actual:#}",
486 ))
487 .with_label(
488 format!(
489 "input `{name}` is {expected:#}, but name `{name}` is {actual:#}",
490 name = name.text(),
491 ),
492 name.span(),
493 )
494}
495
496pub fn no_common_type(
501 expected: &Type,
502 expected_span: Span,
503 actual: &Type,
504 actual_span: Span,
505) -> Diagnostic {
506 Diagnostic::error(format!(
507 "type mismatch: a type common to both {expected:#} and {actual:#} does not exist"
508 ))
509 .with_label(format!("this is {actual:#}"), actual_span)
510 .with_label(
511 format!("this and all prior elements had a common {expected:#}"),
512 expected_span,
513 )
514}
515
516pub fn multiple_type_mismatch(
518 expected: &[Type],
519 expected_span: Span,
520 actual: &Type,
521 actual_span: Span,
522) -> Diagnostic {
523 Diagnostic::error(format!(
524 "type mismatch: expected {expected:#}, but found {actual:#}",
525 expected = display_types(expected),
526 ))
527 .with_label(format!("this is {actual:#}"), actual_span)
528 .with_label(
529 format!(
530 "this expects {expected:#}",
531 expected = display_types(expected)
532 ),
533 expected_span,
534 )
535}
536
537pub fn not_a_task_member<T: TreeToken>(member: &Ident<T>) -> Diagnostic {
539 Diagnostic::error(format!(
540 "the `task` variable does not have a member named `{member}`",
541 member = member.text()
542 ))
543 .with_highlight(member.span())
544}
545
546pub fn not_a_previous_task_data_member<T: TreeToken>(member: &Ident<T>) -> Diagnostic {
548 Diagnostic::error(format!(
549 "`task.previous` does not have a member named `{member}`",
550 member = member.text()
551 ))
552 .with_highlight(member.span())
553}
554
555pub fn not_a_struct<T: TreeToken>(member: &Ident<T>, input: bool) -> Diagnostic {
557 Diagnostic::error(format!(
558 "{kind} `{member}` is not a struct",
559 kind = if input { "input" } else { "struct member" },
560 member = member.text()
561 ))
562 .with_highlight(member.span())
563}
564
565pub fn not_a_struct_member<T: TreeToken>(name: &str, member: &Ident<T>) -> Diagnostic {
567 Diagnostic::error(format!(
568 "struct `{name}` does not have a member named `{member}`",
569 member = member.text()
570 ))
571 .with_highlight(member.span())
572}
573
574pub fn not_an_enum_choice<T: TreeToken>(name: &str, choice: &Ident<T>) -> Diagnostic {
576 Diagnostic::error(format!(
577 "enum `{name}` does not have a choice named `{choice}`",
578 choice = choice.text()
579 ))
580 .with_highlight(choice.span())
581}
582
583pub fn non_literal_enum_value(span: Span) -> Diagnostic {
585 Diagnostic::error("enum choice value must be a literal expression")
586 .with_highlight(span)
587 .with_fix(
588 "enum values must be literal expressions only (string literals, numeric literals, \
589 collection literals, or struct literals); string interpolation, variable references, \
590 and computed expressions are not allowed",
591 )
592}
593
594pub fn not_a_pair_accessor<T: TreeToken>(name: &Ident<T>) -> Diagnostic {
596 Diagnostic::error(format!(
597 "cannot access a pair with name `{name}`",
598 name = name.text()
599 ))
600 .with_highlight(name.span())
601 .with_fix("use `left` or `right` to access a pair")
602}
603
604pub fn missing_struct_members<T: TreeToken>(
606 name: &Ident<T>,
607 count: usize,
608 members: &str,
609) -> Diagnostic {
610 Diagnostic::error(format!(
611 "struct `{name}` requires a value for member{s} {members}",
612 name = name.text(),
613 s = if count > 1 { "s" } else { "" },
614 ))
615 .with_highlight(name.span())
616}
617
618pub fn map_key_not_primitive(span: Span, actual: &Type) -> Diagnostic {
620 Diagnostic::error("expected map key to be a non-optional primitive type")
621 .with_highlight(span)
622 .with_label(format!("this is {actual:#}"), span)
623}
624
625pub fn if_conditional_mismatch(actual: &Type, actual_span: Span) -> Diagnostic {
627 Diagnostic::error(format!(
628 "type mismatch: expected `if` conditional expression to be type `Boolean`, but found \
629 {actual:#}"
630 ))
631 .with_label(format!("this is {actual:#}"), actual_span)
632}
633
634pub fn else_if_not_supported(version: SupportedVersion, span: Span) -> Diagnostic {
636 Diagnostic::error(format!(
637 "`else if` conditional clauses are not supported in WDL v{version}"
638 ))
639 .with_label("this `else if` is not supported", span)
640 .with_fix("use WDL v1.3 or higher to use `else if` conditional clauses")
641}
642
643pub fn else_not_supported(version: SupportedVersion, span: Span) -> Diagnostic {
645 Diagnostic::error(format!(
646 "`else` conditional clauses are not supported in WDL v{version}"
647 ))
648 .with_label("this `else` is not supported", span)
649 .with_fix("use WDL v1.3 or higher to use `else` conditional clauses")
650}
651
652pub fn enum_not_supported(version: SupportedVersion, span: Span) -> Diagnostic {
654 Diagnostic::error(format!("enums are not supported in WDL v{version}"))
655 .with_label("this enum is not supported", span)
656 .with_fix("use WDL v1.3 or higher to use enums")
657}
658
659pub fn logical_not_mismatch(actual: &Type, actual_span: Span) -> Diagnostic {
661 Diagnostic::error(format!(
662 "type mismatch: expected `logical not` operand to be type `Boolean`, but found {actual:#}"
663 ))
664 .with_label(format!("this is {actual:#}"), actual_span)
665}
666
667pub fn negation_mismatch(actual: &Type, actual_span: Span) -> Diagnostic {
669 Diagnostic::error(format!(
670 "type mismatch: expected negation operand to be type `Int` or `Float`, but found \
671 {actual:#}"
672 ))
673 .with_label(format!("this is {actual:#}"), actual_span)
674}
675
676pub fn logical_or_mismatch(actual: &Type, actual_span: Span) -> Diagnostic {
678 Diagnostic::error(format!(
679 "type mismatch: expected `logical or` operand to be type `Boolean`, but found {actual:#}"
680 ))
681 .with_label(format!("this is {actual:#}"), actual_span)
682}
683
684pub fn logical_and_mismatch(actual: &Type, actual_span: Span) -> Diagnostic {
686 Diagnostic::error(format!(
687 "type mismatch: expected `logical and` operand to be type `Boolean`, but found {actual:#}"
688 ))
689 .with_label(format!("this is {actual:#}"), actual_span)
690}
691
692pub fn comparison_mismatch(
694 op: ComparisonOperator,
695 span: Span,
696 lhs: &Type,
697 lhs_span: Span,
698 rhs: &Type,
699 rhs_span: Span,
700) -> Diagnostic {
701 Diagnostic::error(format!(
702 "type mismatch: operator `{op}` cannot compare {lhs:#} to {rhs:#}"
703 ))
704 .with_highlight(span)
705 .with_label(format!("this is {lhs:#}"), lhs_span)
706 .with_label(format!("this is {rhs:#}"), rhs_span)
707}
708
709pub fn numeric_mismatch(
711 op: NumericOperator,
712 span: Span,
713 lhs: &Type,
714 lhs_span: Span,
715 rhs: &Type,
716 rhs_span: Span,
717) -> Diagnostic {
718 Diagnostic::error(format!(
719 "type mismatch: {op} operator is not supported for {lhs:#} and {rhs:#}"
720 ))
721 .with_highlight(span)
722 .with_label(format!("this is {lhs:#}"), lhs_span)
723 .with_label(format!("this is {rhs:#}"), rhs_span)
724}
725
726pub fn string_concat_mismatch(actual: &Type, actual_span: Span) -> Diagnostic {
728 Diagnostic::error(format!(
729 "type mismatch: string concatenation is not supported for {actual:#}"
730 ))
731 .with_label(format!("this is {actual:#}"), actual_span)
732}
733
734pub fn unknown_function(name: &str, span: Span) -> Diagnostic {
736 Diagnostic::error(format!("unknown function `{name}`")).with_label(
737 "the WDL standard library does not have a function with this name",
738 span,
739 )
740}
741
742pub fn unsupported_function(minimum: SupportedVersion, name: &str, span: Span) -> Diagnostic {
744 Diagnostic::error(format!(
745 "this use of function `{name}` requires a minimum WDL version of {minimum}"
746 ))
747 .with_highlight(span)
748}
749
750pub fn too_few_arguments(name: &str, span: Span, minimum: usize, count: usize) -> Diagnostic {
752 Diagnostic::error(format!(
753 "function `{name}` requires at least {minimum} argument{s} but {count} {v} supplied",
754 s = if minimum == 1 { "" } else { "s" },
755 v = if count == 1 { "was" } else { "were" },
756 ))
757 .with_highlight(span)
758}
759
760pub fn too_many_arguments(
762 name: &str,
763 span: Span,
764 maximum: usize,
765 count: usize,
766 excessive: impl Iterator<Item = Span>,
767) -> Diagnostic {
768 let mut diagnostic = Diagnostic::error(format!(
769 "function `{name}` requires no more than {maximum} argument{s} but {count} {v} supplied",
770 s = if maximum == 1 { "" } else { "s" },
771 v = if count == 1 { "was" } else { "were" },
772 ))
773 .with_highlight(span);
774
775 for span in excessive {
776 diagnostic = diagnostic.with_label("this argument is unexpected", span);
777 }
778
779 diagnostic
780}
781
782pub fn argument_type_mismatch(name: &str, expected: &str, actual: &Type, span: Span) -> Diagnostic {
784 Diagnostic::error(format!(
785 "type mismatch: argument to function `{name}` expects {expected}, but found {actual:#}"
786 ))
787 .with_label(format!("this is {actual:#}"), span)
788}
789
790pub fn ambiguous_argument(name: &str, span: Span, first: &str, second: &str) -> Diagnostic {
792 Diagnostic::error(format!(
793 "ambiguous call to function `{name}` with conflicting signatures `{first}` and `{second}`",
794 ))
795 .with_highlight(span)
796}
797
798pub fn index_type_mismatch(expected: &Type, actual: &Type, span: Span) -> Diagnostic {
800 Diagnostic::error(format!(
801 "type mismatch: expected index to be {expected:#}, but found {actual:#}"
802 ))
803 .with_label(format!("this is {actual:#}"), span)
804}
805
806pub fn type_is_not_array(actual: &Type, span: Span) -> Diagnostic {
808 Diagnostic::error(format!(
809 "type mismatch: expected an array type, but found {actual:#}"
810 ))
811 .with_label(format!("this is {actual:#}"), span)
812}
813
814pub fn cannot_access(actual: &Type, actual_span: Span) -> Diagnostic {
816 Diagnostic::error(format!("cannot access {actual:#}"))
817 .with_label(format!("this is {actual:#}"), actual_span)
818}
819
820pub fn cannot_coerce_to_string(actual: &Type, span: Span) -> Diagnostic {
822 Diagnostic::error(format!("cannot coerce {actual:#} to type `String`"))
823 .with_label(format!("this is {actual:#}"), span)
824}
825
826pub fn unknown_task_or_workflow(namespace: Option<Span>, name: &str, span: Span) -> Diagnostic {
828 let mut diagnostic =
829 Diagnostic::error(format!("unknown task or workflow `{name}`")).with_highlight(span);
830
831 if let Some(namespace) = namespace {
832 diagnostic = diagnostic.with_label(
833 format!("this namespace does not have a task or workflow named `{name}`"),
834 namespace,
835 );
836 }
837
838 diagnostic
839}
840
841pub fn unknown_call_io<T: TreeToken>(call: &CallType, name: &Ident<T>, io: Io) -> Diagnostic {
843 Diagnostic::error(format!(
844 "{kind} `{call}` does not have an {io} named `{name}`",
845 kind = call.kind(),
846 call = call.name(),
847 name = name.text(),
848 ))
849 .with_highlight(name.span())
850}
851
852pub fn unknown_task_io<T: TreeToken>(task_name: &str, name: &Ident<T>, io: Io) -> Diagnostic {
854 Diagnostic::error(format!(
855 "task `{task_name}` does not have an {io} named `{name}`",
856 name = name.text(),
857 ))
858 .with_highlight(name.span())
859}
860
861pub fn recursive_workflow_call(name: &str, span: Span) -> Diagnostic {
863 Diagnostic::error(format!("cannot recursively call workflow `{name}`")).with_highlight(span)
864}
865
866pub fn missing_call_input<T: TreeToken>(
868 kind: CallKind,
869 target: &Ident<T>,
870 input: &str,
871 nested_inputs_allowed: bool,
872) -> Diagnostic {
873 let message = format!(
874 "missing required call input `{input}` for {kind} `{target}`",
875 target = target.text(),
876 );
877
878 if nested_inputs_allowed {
879 Diagnostic::warning(message).with_highlight(target.span())
880 } else {
881 Diagnostic::error(message).with_highlight(target.span())
882 }
883}
884
885pub fn unused_import(name: &str, span: Span) -> Diagnostic {
887 Diagnostic::warning(format!("unused import namespace `{name}`"))
888 .with_rule(UnusedImportRule::ID)
889 .with_highlight(span)
890}
891
892pub fn unused_input(name: &str, span: Span) -> Diagnostic {
894 Diagnostic::warning(format!("unused input `{name}`"))
895 .with_rule(UnusedInputRule::ID)
896 .with_highlight(span)
897}
898
899pub fn unused_declaration(name: &str, span: Span) -> Diagnostic {
901 Diagnostic::warning(format!("unused declaration `{name}`"))
902 .with_rule(UnusedDeclarationRule::ID)
903 .with_highlight(span)
904}
905
906pub fn misleading_declaration_order(name: &str, span: Span) -> Diagnostic {
908 Diagnostic::warning("variable declaration appears after the `command` section")
909 .with_rule(MisleadingDeclarationOrderRule::ID)
910 .with_highlight(span)
911 .with_help(
912 "this is visually misleading; tasks are evaluated in dependency order, not \
913 top-to-bottom",
914 )
915 .with_fix(format!(
916 "move the declaration of `{name}` above the `command` section"
917 ))
918}
919
920pub fn unused_call(name: &str, span: Span) -> Diagnostic {
922 Diagnostic::warning(format!("unused call `{name}`"))
923 .with_rule(UnusedCallRule::ID)
924 .with_highlight(span)
925}
926
927pub fn unnecessary_function_call(
929 name: &str,
930 span: Span,
931 label: &str,
932 label_span: Span,
933) -> Diagnostic {
934 Diagnostic::warning(format!("unnecessary call to function `{name}`"))
935 .with_rule(UnnecessaryFunctionCall::ID)
936 .with_highlight(span)
937 .with_label(label.to_string(), label_span)
938}
939
940pub fn meaningless_lint_directive(rule: &str, span: Span, severity: Severity) -> Diagnostic {
942 Diagnostic::note(format!(
943 "unnecessary `except` directive for lint rule `{rule}`"
944 ))
945 .with_rule(MeaninglessLintDirective::ID)
946 .with_highlight(span)
947 .with_severity(severity)
948}
949
950pub fn invalid_placeholder_option<N: TreeNode>(
953 ty: &Type,
954 span: Span,
955 option: &PlaceholderOption<N>,
956) -> Diagnostic {
957 let message = match option {
958 PlaceholderOption::Sep(_) => format!(
959 "type mismatch for placeholder option `sep`: expected type `Array[P]` where P: any \
960 primitive type, but found {ty:#}"
961 ),
962 PlaceholderOption::Default(_) => format!(
963 "type mismatch for placeholder option `default`: expected any primitive type, but \
964 found {ty:#}"
965 ),
966 PlaceholderOption::TrueFalse(_) => format!(
967 "type mismatch for placeholder option `true/false`: expected type `Boolean`, but \
968 found {ty:#}"
969 ),
970 };
971
972 Diagnostic::error(message).with_label(format!("this is {ty:#}"), span)
973}
974
975pub fn invalid_regex_pattern(
977 function: &str,
978 pattern: &str,
979 error: ®ex::Error,
980 span: Span,
981) -> Diagnostic {
982 Diagnostic::error(format!(
983 "invalid regular expression `{pattern}` used in function `{function}`: {error}"
984 ))
985 .with_label("invalid regular expression", span)
986}
987
988pub fn not_a_custom_type<T: TreeToken>(name: &Ident<T>) -> Diagnostic {
990 Diagnostic::error(format!("`{}` is not a custom type", name.text())).with_label(
991 "only struct and enum types can be referenced as values",
992 name.span(),
993 )
994}
995
996pub fn no_common_inferred_type_for_enum(
1001 enum_name: &str,
1002 common_type: &Type,
1003 common_span: Span,
1004 discordant_type: &Type,
1005 discordant_span: Span,
1006) -> Diagnostic {
1007 Diagnostic::error(format!("cannot infer a common type for enum `{enum_name}`"))
1008 .with_label(
1009 format!(
1010 "this is the first choice with {discordant_type:#} that has no common type with \
1011 {common_type:#}"
1012 ),
1013 discordant_span,
1014 )
1015 .with_label(
1016 format!("this is the last choice with a common {common_type:#}"),
1017 common_span,
1018 )
1019}
1020
1021pub fn enum_choice_does_not_coerce_to_type(
1023 enum_name: &str,
1024 enum_span: Span,
1025 choice_name: &str,
1026 choice_span: Span,
1027 expected: &Type,
1028 actual: &Type,
1029) -> Diagnostic {
1030 Diagnostic::error(format!(
1031 "cannot coerce choice `{choice_name}` in enum `{enum_name}` from {actual:#} to \
1032 {expected:#}"
1033 ))
1034 .with_label(format!("this is the `{enum_name}` enum"), enum_span)
1035 .with_label(format!("this is the `{choice_name}` choice"), choice_span)
1036 .with_fix(format!(
1037 "change the value to something that coerces to {expected:#} or explicitly set the enum's \
1038 inner type"
1039 ))
1040}