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