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 selected_member_not_found(name: &str, span: Span) -> Diagnostic {
323 Diagnostic::error(format!("`{name}` does not exist in the imported module"))
324 .with_highlight(span)
325}
326
327pub fn selected_import_conflict(name: &str, import_span: Span, prev_span: Span) -> Diagnostic {
329 Diagnostic::error(format!(
330 "import of `{name}` conflicts with an existing definition"
331 ))
332 .with_label("imported here", import_span)
333 .with_label("previous definition", prev_span)
334}
335
336pub fn struct_not_in_document<T: TreeToken>(name: &Ident<T>) -> Diagnostic {
338 Diagnostic::error(format!(
339 "a struct named `{name}` does not exist in the imported document",
340 name = name.text()
341 ))
342 .with_label("this struct does not exist", name.span())
343}
344
345pub fn imported_struct_conflict(
347 name: &str,
348 conflicting: Span,
349 first: Span,
350 suggest_fix: bool,
351) -> Diagnostic {
352 let diagnostic = Diagnostic::error(format!("conflicting struct name `{name}`"))
353 .with_label(
354 "this import introduces a conflicting definition",
355 conflicting,
356 )
357 .with_label("the first definition was introduced by this import", first);
358
359 if suggest_fix {
360 diagnostic.with_fix("add an `alias` clause to the import to specify a different name")
361 } else {
362 diagnostic
363 }
364}
365
366pub fn struct_conflicts_with_import(name: &str, conflicting: Span, import: Span) -> Diagnostic {
368 Diagnostic::error(format!("conflicting struct name `{name}`"))
369 .with_label("this name conflicts with an imported struct", conflicting)
370 .with_label("the import that introduced the struct is here", import)
371 .with_fix(
372 "either rename the struct or use an `alias` clause on the import with a different name",
373 )
374}
375
376pub fn imported_enum_conflict(
378 name: &str,
379 conflicting: Span,
380 first: Span,
381 suggest_fix: bool,
382) -> Diagnostic {
383 let diagnostic = Diagnostic::error(format!("conflicting enum name `{name}`"))
384 .with_label(
385 "this import introduces a conflicting definition",
386 conflicting,
387 )
388 .with_label("the first definition was introduced by this import", first);
389
390 if suggest_fix {
391 diagnostic.with_fix("add an `alias` clause to the import to specify a different name")
392 } else {
393 diagnostic
394 }
395}
396
397pub fn enum_conflicts_with_import(name: &str, conflicting: Span, import: Span) -> Diagnostic {
399 Diagnostic::error(format!("conflicting enum name `{name}`"))
400 .with_label("this name conflicts with an imported enum", conflicting)
401 .with_label("the import that introduced the enum is here", import)
402 .with_fix(
403 "either rename the enum or use an `alias` clause on the import with a different name",
404 )
405}
406
407pub fn duplicate_workflow<T: TreeToken>(name: &Ident<T>, first: Span) -> Diagnostic {
409 Diagnostic::error(format!(
410 "cannot define workflow `{name}` as only one workflow is allowed per source file",
411 name = name.text(),
412 ))
413 .with_label("consider moving this workflow to a new file", name.span())
414 .with_label("first workflow is defined here", first)
415}
416
417pub fn recursive_struct(name: &str, span: Span, member: Span) -> Diagnostic {
419 Diagnostic::error(format!("struct `{name}` has a recursive definition"))
420 .with_highlight(span)
421 .with_label("this struct member participates in the recursion", member)
422}
423
424pub fn recursive_enum(name: &str, span: Span, ty: &str) -> Diagnostic {
426 Diagnostic::error(format!("enum `{name}` has a recursive definition"))
430 .with_highlight(span)
431 .with_help(format!("the type `{ty}` participates in the recursion"))
432}
433
434pub fn unknown_type(name: &str, span: Span) -> Diagnostic {
436 Diagnostic::error(format!("unknown type name `{name}`")).with_highlight(span)
437}
438
439pub fn type_mismatch(
441 expected: &Type,
442 expected_span: Span,
443 actual: &Type,
444 actual_span: Span,
445) -> Diagnostic {
446 Diagnostic::error(format!(
447 "type mismatch: expected {expected:#}, but found {actual:#}"
448 ))
449 .with_label(format!("this is {actual:#}"), actual_span)
450 .with_label(format!("this expects {expected:#}"), expected_span)
451}
452
453pub fn non_empty_array_assignment(expected_span: Span, actual_span: Span) -> Diagnostic {
455 Diagnostic::error("cannot assign an empty array to a non-empty array type")
456 .with_label("this is an empty array", actual_span)
457 .with_label("this expects a non-empty array", expected_span)
458}
459
460pub fn call_input_type_mismatch<T: TreeToken>(
462 name: &Ident<T>,
463 expected: &Type,
464 actual: &Type,
465) -> Diagnostic {
466 Diagnostic::error(format!(
467 "type mismatch: expected {expected:#}, but found {actual:#}",
468 ))
469 .with_label(
470 format!(
471 "input `{name}` is {expected:#}, but name `{name}` is {actual:#}",
472 name = name.text(),
473 ),
474 name.span(),
475 )
476}
477
478pub fn no_common_type(
483 expected: &Type,
484 expected_span: Span,
485 actual: &Type,
486 actual_span: Span,
487) -> Diagnostic {
488 Diagnostic::error(format!(
489 "type mismatch: a type common to both {expected:#} and {actual:#} does not exist"
490 ))
491 .with_label(format!("this is {actual:#}"), actual_span)
492 .with_label(
493 format!("this and all prior elements had a common {expected:#}"),
494 expected_span,
495 )
496}
497
498pub fn multiple_type_mismatch(
500 expected: &[Type],
501 expected_span: Span,
502 actual: &Type,
503 actual_span: Span,
504) -> Diagnostic {
505 Diagnostic::error(format!(
506 "type mismatch: expected {expected:#}, but found {actual:#}",
507 expected = display_types(expected),
508 ))
509 .with_label(format!("this is {actual:#}"), actual_span)
510 .with_label(
511 format!(
512 "this expects {expected:#}",
513 expected = display_types(expected)
514 ),
515 expected_span,
516 )
517}
518
519pub fn not_a_task_member<T: TreeToken>(member: &Ident<T>) -> Diagnostic {
521 Diagnostic::error(format!(
522 "the `task` variable does not have a member named `{member}`",
523 member = member.text()
524 ))
525 .with_highlight(member.span())
526}
527
528pub fn not_a_previous_task_data_member<T: TreeToken>(member: &Ident<T>) -> Diagnostic {
530 Diagnostic::error(format!(
531 "`task.previous` does not have a member named `{member}`",
532 member = member.text()
533 ))
534 .with_highlight(member.span())
535}
536
537pub fn not_a_struct<T: TreeToken>(member: &Ident<T>, input: bool) -> Diagnostic {
539 Diagnostic::error(format!(
540 "{kind} `{member}` is not a struct",
541 kind = if input { "input" } else { "struct member" },
542 member = member.text()
543 ))
544 .with_highlight(member.span())
545}
546
547pub fn not_a_struct_member<T: TreeToken>(name: &str, member: &Ident<T>) -> Diagnostic {
549 Diagnostic::error(format!(
550 "struct `{name}` does not have a member named `{member}`",
551 member = member.text()
552 ))
553 .with_highlight(member.span())
554}
555
556pub fn not_an_enum_choice<T: TreeToken>(name: &str, choice: &Ident<T>) -> Diagnostic {
558 Diagnostic::error(format!(
559 "enum `{name}` does not have a choice named `{choice}`",
560 choice = choice.text()
561 ))
562 .with_highlight(choice.span())
563}
564
565pub fn non_literal_enum_value(span: Span) -> Diagnostic {
567 Diagnostic::error("enum choice value must be a literal expression")
568 .with_highlight(span)
569 .with_fix(
570 "enum values must be literal expressions only (string literals, numeric literals, \
571 collection literals, or struct literals); string interpolation, variable references, \
572 and computed expressions are not allowed",
573 )
574}
575
576pub fn not_a_pair_accessor<T: TreeToken>(name: &Ident<T>) -> Diagnostic {
578 Diagnostic::error(format!(
579 "cannot access a pair with name `{name}`",
580 name = name.text()
581 ))
582 .with_highlight(name.span())
583 .with_fix("use `left` or `right` to access a pair")
584}
585
586pub fn missing_struct_members<T: TreeToken>(
588 name: &Ident<T>,
589 count: usize,
590 members: &str,
591) -> Diagnostic {
592 Diagnostic::error(format!(
593 "struct `{name}` requires a value for member{s} {members}",
594 name = name.text(),
595 s = if count > 1 { "s" } else { "" },
596 ))
597 .with_highlight(name.span())
598}
599
600pub fn map_key_not_primitive(span: Span, actual: &Type) -> Diagnostic {
602 Diagnostic::error("expected map key to be a non-optional primitive type")
603 .with_highlight(span)
604 .with_label(format!("this is {actual:#}"), span)
605}
606
607pub fn if_conditional_mismatch(actual: &Type, actual_span: Span) -> Diagnostic {
609 Diagnostic::error(format!(
610 "type mismatch: expected `if` conditional expression to be type `Boolean`, but found \
611 {actual:#}"
612 ))
613 .with_label(format!("this is {actual:#}"), actual_span)
614}
615
616pub fn else_if_not_supported(version: SupportedVersion, span: Span) -> Diagnostic {
618 Diagnostic::error(format!(
619 "`else if` conditional clauses are not supported in WDL v{version}"
620 ))
621 .with_label("this `else if` is not supported", span)
622 .with_fix("use WDL v1.3 or higher to use `else if` conditional clauses")
623}
624
625pub fn else_not_supported(version: SupportedVersion, span: Span) -> Diagnostic {
627 Diagnostic::error(format!(
628 "`else` conditional clauses are not supported in WDL v{version}"
629 ))
630 .with_label("this `else` is not supported", span)
631 .with_fix("use WDL v1.3 or higher to use `else` conditional clauses")
632}
633
634pub fn enum_not_supported(version: SupportedVersion, span: Span) -> Diagnostic {
636 Diagnostic::error(format!("enums are not supported in WDL v{version}"))
637 .with_label("this enum is not supported", span)
638 .with_fix("use WDL v1.3 or higher to use enums")
639}
640
641pub fn logical_not_mismatch(actual: &Type, actual_span: Span) -> Diagnostic {
643 Diagnostic::error(format!(
644 "type mismatch: expected `logical not` operand to be type `Boolean`, but found {actual:#}"
645 ))
646 .with_label(format!("this is {actual:#}"), actual_span)
647}
648
649pub fn negation_mismatch(actual: &Type, actual_span: Span) -> Diagnostic {
651 Diagnostic::error(format!(
652 "type mismatch: expected negation operand to be type `Int` or `Float`, but found \
653 {actual:#}"
654 ))
655 .with_label(format!("this is {actual:#}"), actual_span)
656}
657
658pub fn logical_or_mismatch(actual: &Type, actual_span: Span) -> Diagnostic {
660 Diagnostic::error(format!(
661 "type mismatch: expected `logical or` operand to be type `Boolean`, but found {actual:#}"
662 ))
663 .with_label(format!("this is {actual:#}"), actual_span)
664}
665
666pub fn logical_and_mismatch(actual: &Type, actual_span: Span) -> Diagnostic {
668 Diagnostic::error(format!(
669 "type mismatch: expected `logical and` operand to be type `Boolean`, but found {actual:#}"
670 ))
671 .with_label(format!("this is {actual:#}"), actual_span)
672}
673
674pub fn comparison_mismatch(
676 op: ComparisonOperator,
677 span: Span,
678 lhs: &Type,
679 lhs_span: Span,
680 rhs: &Type,
681 rhs_span: Span,
682) -> Diagnostic {
683 Diagnostic::error(format!(
684 "type mismatch: operator `{op}` cannot compare {lhs:#} to {rhs:#}"
685 ))
686 .with_highlight(span)
687 .with_label(format!("this is {lhs:#}"), lhs_span)
688 .with_label(format!("this is {rhs:#}"), rhs_span)
689}
690
691pub fn numeric_mismatch(
693 op: NumericOperator,
694 span: Span,
695 lhs: &Type,
696 lhs_span: Span,
697 rhs: &Type,
698 rhs_span: Span,
699) -> Diagnostic {
700 Diagnostic::error(format!(
701 "type mismatch: {op} operator is not supported for {lhs:#} and {rhs:#}"
702 ))
703 .with_highlight(span)
704 .with_label(format!("this is {lhs:#}"), lhs_span)
705 .with_label(format!("this is {rhs:#}"), rhs_span)
706}
707
708pub fn string_concat_mismatch(actual: &Type, actual_span: Span) -> Diagnostic {
710 Diagnostic::error(format!(
711 "type mismatch: string concatenation is not supported for {actual:#}"
712 ))
713 .with_label(format!("this is {actual:#}"), actual_span)
714}
715
716pub fn unknown_function(name: &str, span: Span) -> Diagnostic {
718 Diagnostic::error(format!("unknown function `{name}`")).with_label(
719 "the WDL standard library does not have a function with this name",
720 span,
721 )
722}
723
724pub fn unsupported_function(minimum: SupportedVersion, name: &str, span: Span) -> Diagnostic {
726 Diagnostic::error(format!(
727 "this use of function `{name}` requires a minimum WDL version of {minimum}"
728 ))
729 .with_highlight(span)
730}
731
732pub fn too_few_arguments(name: &str, span: Span, minimum: usize, count: usize) -> Diagnostic {
734 Diagnostic::error(format!(
735 "function `{name}` requires at least {minimum} argument{s} but {count} {v} supplied",
736 s = if minimum == 1 { "" } else { "s" },
737 v = if count == 1 { "was" } else { "were" },
738 ))
739 .with_highlight(span)
740}
741
742pub fn too_many_arguments(
744 name: &str,
745 span: Span,
746 maximum: usize,
747 count: usize,
748 excessive: impl Iterator<Item = Span>,
749) -> Diagnostic {
750 let mut diagnostic = Diagnostic::error(format!(
751 "function `{name}` requires no more than {maximum} argument{s} but {count} {v} supplied",
752 s = if maximum == 1 { "" } else { "s" },
753 v = if count == 1 { "was" } else { "were" },
754 ))
755 .with_highlight(span);
756
757 for span in excessive {
758 diagnostic = diagnostic.with_label("this argument is unexpected", span);
759 }
760
761 diagnostic
762}
763
764pub fn argument_type_mismatch(name: &str, expected: &str, actual: &Type, span: Span) -> Diagnostic {
766 Diagnostic::error(format!(
767 "type mismatch: argument to function `{name}` expects {expected}, but found {actual:#}"
768 ))
769 .with_label(format!("this is {actual:#}"), span)
770}
771
772pub fn ambiguous_argument(name: &str, span: Span, first: &str, second: &str) -> Diagnostic {
774 Diagnostic::error(format!(
775 "ambiguous call to function `{name}` with conflicting signatures `{first}` and `{second}`",
776 ))
777 .with_highlight(span)
778}
779
780pub fn index_type_mismatch(expected: &Type, actual: &Type, span: Span) -> Diagnostic {
782 Diagnostic::error(format!(
783 "type mismatch: expected index to be {expected:#}, but found {actual:#}"
784 ))
785 .with_label(format!("this is {actual:#}"), span)
786}
787
788pub fn type_is_not_array(actual: &Type, span: Span) -> Diagnostic {
790 Diagnostic::error(format!(
791 "type mismatch: expected an array type, but found {actual:#}"
792 ))
793 .with_label(format!("this is {actual:#}"), span)
794}
795
796pub fn cannot_access(actual: &Type, actual_span: Span) -> Diagnostic {
798 Diagnostic::error(format!("cannot access {actual:#}"))
799 .with_label(format!("this is {actual:#}"), actual_span)
800}
801
802pub fn cannot_coerce_to_string(actual: &Type, span: Span) -> Diagnostic {
804 Diagnostic::error(format!("cannot coerce {actual:#} to type `String`"))
805 .with_label(format!("this is {actual:#}"), span)
806}
807
808pub fn unknown_task_or_workflow(namespace: Option<Span>, name: &str, span: Span) -> Diagnostic {
810 let mut diagnostic =
811 Diagnostic::error(format!("unknown task or workflow `{name}`")).with_highlight(span);
812
813 if let Some(namespace) = namespace {
814 diagnostic = diagnostic.with_label(
815 format!("this namespace does not have a task or workflow named `{name}`"),
816 namespace,
817 );
818 }
819
820 diagnostic
821}
822
823pub fn unknown_call_io<T: TreeToken>(call: &CallType, name: &Ident<T>, io: Io) -> Diagnostic {
825 Diagnostic::error(format!(
826 "{kind} `{call}` does not have an {io} named `{name}`",
827 kind = call.kind(),
828 call = call.name(),
829 name = name.text(),
830 ))
831 .with_highlight(name.span())
832}
833
834pub fn unknown_task_io<T: TreeToken>(task_name: &str, name: &Ident<T>, io: Io) -> Diagnostic {
836 Diagnostic::error(format!(
837 "task `{task_name}` does not have an {io} named `{name}`",
838 name = name.text(),
839 ))
840 .with_highlight(name.span())
841}
842
843pub fn recursive_workflow_call(name: &str, span: Span) -> Diagnostic {
845 Diagnostic::error(format!("cannot recursively call workflow `{name}`")).with_highlight(span)
846}
847
848pub fn missing_call_input<T: TreeToken>(
850 kind: CallKind,
851 target: &Ident<T>,
852 input: &str,
853 nested_inputs_allowed: bool,
854) -> Diagnostic {
855 let message = format!(
856 "missing required call input `{input}` for {kind} `{target}`",
857 target = target.text(),
858 );
859
860 if nested_inputs_allowed {
861 Diagnostic::warning(message).with_highlight(target.span())
862 } else {
863 Diagnostic::error(message).with_highlight(target.span())
864 }
865}
866
867pub fn unused_import(name: &str, span: Span) -> Diagnostic {
869 Diagnostic::warning(format!("unused import namespace `{name}`"))
870 .with_rule(UnusedImportRule::ID)
871 .with_highlight(span)
872}
873
874pub fn unused_input(name: &str, span: Span) -> Diagnostic {
876 Diagnostic::warning(format!("unused input `{name}`"))
877 .with_rule(UnusedInputRule::ID)
878 .with_highlight(span)
879}
880
881pub fn unused_declaration(name: &str, span: Span) -> Diagnostic {
883 Diagnostic::warning(format!("unused declaration `{name}`"))
884 .with_rule(UnusedDeclarationRule::ID)
885 .with_highlight(span)
886}
887
888pub fn misleading_declaration_order(name: &str, span: Span) -> Diagnostic {
890 Diagnostic::warning("variable declaration appears after the `command` section")
891 .with_rule(MisleadingDeclarationOrderRule::ID)
892 .with_highlight(span)
893 .with_help(
894 "this is visually misleading; tasks are evaluated in dependency order, not \
895 top-to-bottom",
896 )
897 .with_fix(format!(
898 "move the declaration of `{name}` above the `command` section"
899 ))
900}
901
902pub fn unused_call(name: &str, span: Span) -> Diagnostic {
904 Diagnostic::warning(format!("unused call `{name}`"))
905 .with_rule(UnusedCallRule::ID)
906 .with_highlight(span)
907}
908
909pub fn unnecessary_function_call(
911 name: &str,
912 span: Span,
913 label: &str,
914 label_span: Span,
915) -> Diagnostic {
916 Diagnostic::warning(format!("unnecessary call to function `{name}`"))
917 .with_rule(UnnecessaryFunctionCall::ID)
918 .with_highlight(span)
919 .with_label(label.to_string(), label_span)
920}
921
922pub fn meaningless_lint_directive(rule: &str, span: Span, severity: Severity) -> Diagnostic {
924 Diagnostic::note(format!(
925 "unnecessary `except` directive for lint rule `{rule}`"
926 ))
927 .with_rule(MeaninglessLintDirective::ID)
928 .with_highlight(span)
929 .with_severity(severity)
930}
931
932pub fn invalid_placeholder_option<N: TreeNode>(
935 ty: &Type,
936 span: Span,
937 option: &PlaceholderOption<N>,
938) -> Diagnostic {
939 let message = match option {
940 PlaceholderOption::Sep(_) => format!(
941 "type mismatch for placeholder option `sep`: expected type `Array[P]` where P: any \
942 primitive type, but found {ty:#}"
943 ),
944 PlaceholderOption::Default(_) => format!(
945 "type mismatch for placeholder option `default`: expected any primitive type, but \
946 found {ty:#}"
947 ),
948 PlaceholderOption::TrueFalse(_) => format!(
949 "type mismatch for placeholder option `true/false`: expected type `Boolean`, but \
950 found {ty:#}"
951 ),
952 };
953
954 Diagnostic::error(message).with_label(format!("this is {ty:#}"), span)
955}
956
957pub fn invalid_regex_pattern(
959 function: &str,
960 pattern: &str,
961 error: ®ex::Error,
962 span: Span,
963) -> Diagnostic {
964 Diagnostic::error(format!(
965 "invalid regular expression `{pattern}` used in function `{function}`: {error}"
966 ))
967 .with_label("invalid regular expression", span)
968}
969
970pub fn not_a_custom_type<T: TreeToken>(name: &Ident<T>) -> Diagnostic {
972 Diagnostic::error(format!("`{}` is not a custom type", name.text())).with_label(
973 "only struct and enum types can be referenced as values",
974 name.span(),
975 )
976}
977
978pub fn no_common_inferred_type_for_enum(
983 enum_name: &str,
984 common_type: &Type,
985 common_span: Span,
986 discordant_type: &Type,
987 discordant_span: Span,
988) -> Diagnostic {
989 Diagnostic::error(format!("cannot infer a common type for enum `{enum_name}`"))
990 .with_label(
991 format!(
992 "this is the first choice with {discordant_type:#} that has no common type with \
993 {common_type:#}"
994 ),
995 discordant_span,
996 )
997 .with_label(
998 format!("this is the last choice with a common {common_type:#}"),
999 common_span,
1000 )
1001}
1002
1003pub fn enum_choice_does_not_coerce_to_type(
1005 enum_name: &str,
1006 enum_span: Span,
1007 choice_name: &str,
1008 choice_span: Span,
1009 expected: &Type,
1010 actual: &Type,
1011) -> Diagnostic {
1012 Diagnostic::error(format!(
1013 "cannot coerce choice `{choice_name}` in enum `{enum_name}` from {actual:#} to \
1014 {expected:#}"
1015 ))
1016 .with_label(format!("this is the `{enum_name}` enum"), enum_span)
1017 .with_label(format!("this is the `{choice_name}` choice"), choice_span)
1018 .with_fix(format!(
1019 "change the value to something that coerces to {expected:#} or explicitly set the enum's \
1020 inner type"
1021 ))
1022}