Skip to main content

wdl_analysis/
diagnostics.rs

1//! Module for all diagnostic creation functions.
2
3use 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/// Utility type to represent an input or an output.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum Io {
31    /// The I/O is an input.
32    Input,
33    /// The I/O is an output.
34    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/// Represents the context for diagnostic reporting.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum Context {
49    /// The name is a namespace introduced by an imported document.
50    Namespace(Span),
51    /// The name is a workflow name.
52    Workflow(Span),
53    /// The name is a task name.
54    Task(Span),
55    /// The name is a struct name.
56    Struct(Span),
57    /// The name is a struct member name.
58    StructMember(Span),
59    /// The name is an enum name.
60    Enum(Span),
61    /// The name is an enum choice name.
62    EnumChoice(Span),
63    /// A name from a scope.
64    Name(NameContext),
65}
66
67impl Context {
68    /// Gets the span of the name.
69    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/// Represents the context of a name in a scope.
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum NameContext {
101    /// The name was introduced by an task or workflow input.
102    Input(Span),
103    /// The name was introduced by an task or workflow output.
104    Output(Span),
105    /// The name was introduced by a private declaration.
106    Decl(Span),
107    /// The name was introduced by a workflow call statement.
108    Call(Span),
109    /// The name was introduced by a variable in workflow scatter statement.
110    ScatterVariable(Span),
111}
112
113impl NameContext {
114    /// Gets the span of the name.
115    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
144/// Creates a "name conflict" diagnostic.
145pub 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
157/// Constructs a "cannot index" diagnostic.
158pub 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
163/// Creates an "unknown name" diagnostic.
164pub fn unknown_name(name: &str, span: Span) -> Diagnostic {
165    // Handle special case names here
166    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
177/// Creates a "self-referential" diagnostic.
178pub 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
184/// Creates a "task reference cycle" diagnostic.
185pub 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
199/// Creates a "workflow reference cycle" diagnostic.
200pub 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
211/// Creates a "call conflict" diagnostic.
212pub 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
237/// Creates a "namespace conflict" diagnostic.
238pub 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
258/// Creates an "unknown namespace" diagnostic.
259pub 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
263/// Creates an "only one namespace" diagnostic.
264pub 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
269/// Creates an "import cycle" diagnostic.
270pub 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
275/// Creates an "import failure" diagnostic.
276pub 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
280/// Creates an "incompatible import" diagnostic.
281pub 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
300/// Creates an "import missing version" diagnostic.
301pub fn import_missing_version(span: Span) -> Diagnostic {
302    Diagnostic::error("imported document is missing a version statement").with_highlight(span)
303}
304
305/// Creates an "invalid relative import" diagnostic.
306pub fn invalid_relative_import(error: &url::ParseError, span: Span) -> Diagnostic {
307    Diagnostic::error(format!("{error:#}")).with_highlight(span)
308}
309
310/// Creates a "struct not in document" diagnostic.
311pub 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
319/// Creates an "imported struct conflict" diagnostic.
320pub 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
340/// Creates a "struct conflicts with import" diagnostic.
341pub 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
350/// Creates an "imported enum conflict" diagnostic.
351pub 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
371/// Creates an "enum conflicts with import" diagnostic.
372pub 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
381/// Creates a "duplicate workflow" diagnostic.
382pub 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
391/// Creates a "recursive struct" diagnostic.
392pub 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
398/// Creates a "recursive enum" diagnostic.
399pub fn recursive_enum(name: &str, span: Span, ty: &str) -> Diagnostic {
400    // Unlike `recursive_struct`, which labels individual members, an `enum` has a
401    // single type for all of its choices. Just highlight the `enum` name, as
402    // its type as a *whole* is recursive.
403    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
408/// Creates an "unknown type" diagnostic.
409pub fn unknown_type(name: &str, span: Span) -> Diagnostic {
410    Diagnostic::error(format!("unknown type name `{name}`")).with_highlight(span)
411}
412
413/// Creates a "type mismatch" diagnostic.
414pub 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
427/// Creates a "non-empty array assignment" diagnostic.
428pub 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
434/// Creates a "call input type mismatch" diagnostic.
435pub 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
452/// Creates a "no common type" diagnostic for arrays, maps, and scope unions.
453///
454/// This is called if the elements of a map or an array do not have a common
455/// type.
456pub 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
472/// Creates a "multiple type mismatch" diagnostic.
473pub 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
493/// Creates a "not a task member" diagnostic.
494pub 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
502/// Creates a "not a task.previous member" diagnostic.
503pub 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
511/// Creates a "not a struct" diagnostic.
512pub 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
521/// Creates a "not a struct member" diagnostic.
522pub 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
530/// Creates a "not an enum choice" diagnostic.
531pub 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
539/// Creates a "non-literal enum value" diagnostic.
540pub 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
550/// Creates a "not a pair accessor" diagnostic.
551pub 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
560/// Creates a "missing struct members" diagnostic.
561pub 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
574/// Creates a "map key not primitive" diagnostic.
575pub 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
581/// Creates a "if conditional mismatch" diagnostic.
582pub 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
590/// Creates an "else if not supported" diagnostic.
591pub 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
599/// Creates an "else not supported" diagnostic.
600pub 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
608/// Creates an "enum not supported" diagnostic.
609pub 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
615/// Creates a "logical not mismatch" diagnostic.
616pub 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
623/// Creates a "negation mismatch" diagnostic.
624pub 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
632/// Creates a "logical or mismatch" diagnostic.
633pub 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
640/// Creates a "logical and mismatch" diagnostic.
641pub 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
648/// Creates a "comparison mismatch" diagnostic.
649pub 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
665/// Creates a "numeric mismatch" diagnostic.
666pub 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
682/// Creates a "string concat mismatch" diagnostic.
683pub 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
690/// Creates an "unknown function" diagnostic.
691pub 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
698/// Creates an "unsupported function" diagnostic.
699pub 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
706/// Creates a "too few arguments" diagnostic.
707pub 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
716/// Creates a "too many arguments" diagnostic.
717pub 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
738/// Constructs an "argument type mismatch" diagnostic.
739pub 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
746/// Constructs an "ambiguous argument" diagnostic.
747pub 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
754/// Constructs an "index type mismatch" diagnostic.
755pub 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
762/// Constructs an "type is not array" diagnostic.
763pub 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
770/// Constructs a "cannot access" diagnostic.
771pub 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
776/// Constructs a "cannot coerce to string" diagnostic.
777pub 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
782/// Creates an "unknown task or workflow" diagnostic.
783pub 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
797/// Creates an "unknown call input/output" diagnostic.
798pub 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
808/// Creates an "unknown task input/output name" diagnostic.
809pub 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
817/// Creates a "recursive workflow call" diagnostic.
818pub fn recursive_workflow_call(name: &str, span: Span) -> Diagnostic {
819    Diagnostic::error(format!("cannot recursively call workflow `{name}`")).with_highlight(span)
820}
821
822/// Creates a "missing call input" diagnostic.
823pub 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
841/// Creates an "unused import" diagnostic.
842pub 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
848/// Creates an "unused input" diagnostic.
849pub 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
855/// Creates an "unused declaration" diagnostic.
856pub 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
862/// Creates a "misleading declaration order" diagnostic.
863pub 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
876/// Creates an "unused call" diagnostic.
877pub 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
883/// Creates an "unnecessary function call" diagnostic.
884pub 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
896/// Generates a diagnostic error message when a placeholder option has a type
897/// mismatch.
898pub 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
921/// Creates an invalid regex pattern diagnostic.
922pub fn invalid_regex_pattern(
923    function: &str,
924    pattern: &str,
925    error: &regex::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
934/// Creates a "not a custom type" diagnostic.
935pub 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
942/// Creates a "no common inferred type for enum" diagnostic.
943///
944/// This diagnostic occurs during enum type calculation when no common type can
945/// be inferred from the choice types.
946pub 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
967/// Creates an "enum choice does not coerce to type" diagnostic.
968pub 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}