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;
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/// Utility type to represent an input or an output.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum Io {
33    /// The I/O is an input.
34    Input,
35    /// The I/O is an output.
36    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/// Represents the context for diagnostic reporting.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum Context {
51    /// The name is a namespace introduced by an imported document.
52    Namespace(Span),
53    /// The name is a workflow name.
54    Workflow(Span),
55    /// The name is a task name.
56    Task(Span),
57    /// The name is a struct name.
58    Struct(Span),
59    /// The name is a struct member name.
60    StructMember(Span),
61    /// The name is an enum name.
62    Enum(Span),
63    /// The name is an enum choice name.
64    EnumChoice(Span),
65    /// A name from a scope.
66    Name(NameContext),
67}
68
69impl Context {
70    /// Gets the span of the name.
71    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/// Represents the context of a name in a scope.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum NameContext {
103    /// The name was introduced by an task or workflow input.
104    Input(Span),
105    /// The name was introduced by an task or workflow output.
106    Output(Span),
107    /// The name was introduced by a private declaration.
108    Decl(Span),
109    /// The name was introduced by a workflow call statement.
110    Call(Span),
111    /// The name was introduced by a variable in workflow scatter statement.
112    ScatterVariable(Span),
113}
114
115impl NameContext {
116    /// Gets the span of the name.
117    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
146/// Creates a "name conflict" diagnostic.
147pub 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
159/// Constructs a "cannot index" diagnostic.
160pub 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
165/// Creates an "unknown name" diagnostic.
166pub fn unknown_name(name: &str, span: Span) -> Diagnostic {
167    // Handle special case names here
168    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
179/// Creates a "self-referential" diagnostic.
180pub 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
186/// Creates a "task reference cycle" diagnostic.
187pub 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
201/// Creates a "workflow reference cycle" diagnostic.
202pub 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
213/// Creates a "call conflict" diagnostic.
214pub 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
239/// Creates a "namespace conflict" diagnostic.
240pub 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
260/// Creates an "unknown namespace" diagnostic.
261pub 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
265/// Creates an "only one namespace" diagnostic.
266pub 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
271/// Creates an "import cycle" diagnostic.
272pub 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
277/// Creates an "import failure" diagnostic.
278pub 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
282/// Creates an "incompatible import" diagnostic.
283pub 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
302/// Creates an "import missing version" diagnostic.
303pub fn import_missing_version(span: Span) -> Diagnostic {
304    Diagnostic::error("imported document is missing a version statement").with_highlight(span)
305}
306
307/// Creates an "invalid relative import" diagnostic.
308pub fn invalid_relative_import(error: &url::ParseError, span: Span) -> Diagnostic {
309    Diagnostic::error(format!("{error:#}")).with_highlight(span)
310}
311
312/// Creates a diagnostic for a wildcard import conflict.
313pub 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
321/// Creates a diagnostic for attempting to add a workflow when one already
322/// occupies local scope.
323pub fn workflow_conflict(
324    rejected_name: &str,
325    rejected_span: Span,
326    retained_name: &str,
327    retained_span: Span,
328) -> Diagnostic {
329    Diagnostic::error(format!(
330        "cannot add workflow `{rejected_name}` because only one workflow may be in scope"
331    ))
332    .with_label("this workflow is rejected", rejected_span)
333    .with_label(
334        format!("workflow `{retained_name}` is retained here"),
335        retained_span,
336    )
337}
338
339/// Creates a diagnostic for a member not found in a selected import.
340pub fn selected_member_not_found(name: &str, span: Span) -> Diagnostic {
341    Diagnostic::error(format!("`{name}` does not exist in the imported module"))
342        .with_highlight(span)
343}
344
345/// Creates a diagnostic for a selected import conflict.
346pub fn selected_import_conflict(name: &str, import_span: Span, prev_span: Span) -> Diagnostic {
347    Diagnostic::error(format!(
348        "import of `{name}` conflicts with an existing definition"
349    ))
350    .with_label("imported here", import_span)
351    .with_label("previous definition", prev_span)
352}
353
354/// Creates a "type not in document" diagnostic.
355pub fn type_not_in_document<T: TreeToken>(name: &Ident<T>) -> Diagnostic {
356    Diagnostic::error(format!(
357        "a struct or enum named `{name}` does not exist in the imported document",
358        name = name.text()
359    ))
360    .with_label("this type name does not exist", name.span())
361}
362
363/// Creates an "imported struct conflict" diagnostic.
364pub fn imported_struct_conflict(
365    name: &str,
366    conflicting: Span,
367    first: Span,
368    suggest_fix: bool,
369) -> Diagnostic {
370    let diagnostic = Diagnostic::error(format!("conflicting struct name `{name}`"))
371        .with_label(
372            "this import introduces a conflicting definition",
373            conflicting,
374        )
375        .with_label("the first definition was introduced by this import", first);
376
377    if suggest_fix {
378        diagnostic.with_fix("add an `alias` clause to the import to specify a different name")
379    } else {
380        diagnostic
381    }
382}
383
384/// Creates a "struct conflicts with import" diagnostic.
385pub fn struct_conflicts_with_import(name: &str, conflicting: Span, import: Span) -> Diagnostic {
386    Diagnostic::error(format!("conflicting struct name `{name}`"))
387        .with_label("this name conflicts with an imported struct", conflicting)
388        .with_label("the import that introduced the struct is here", import)
389        .with_fix(
390            "either rename the struct or use an `alias` clause on the import with a different name",
391        )
392}
393
394/// Creates an "imported enum conflict" diagnostic.
395pub fn imported_enum_conflict(
396    name: &str,
397    conflicting: Span,
398    first: Span,
399    suggest_fix: bool,
400) -> Diagnostic {
401    let diagnostic = Diagnostic::error(format!("conflicting enum name `{name}`"))
402        .with_label(
403            "this import introduces a conflicting definition",
404            conflicting,
405        )
406        .with_label("the first definition was introduced by this import", first);
407
408    if suggest_fix {
409        diagnostic.with_fix("add an `alias` clause to the import to specify a different name")
410    } else {
411        diagnostic
412    }
413}
414
415/// Creates an "enum conflicts with import" diagnostic.
416pub fn enum_conflicts_with_import(name: &str, conflicting: Span, import: Span) -> Diagnostic {
417    Diagnostic::error(format!("conflicting enum name `{name}`"))
418        .with_label("this name conflicts with an imported enum", conflicting)
419        .with_label("the import that introduced the enum is here", import)
420        .with_fix(
421            "either rename the enum or use an `alias` clause on the import with a different name",
422        )
423}
424
425/// Creates a "duplicate workflow" diagnostic.
426pub fn duplicate_workflow<T: TreeToken>(name: &Ident<T>, first: Span) -> Diagnostic {
427    Diagnostic::error(format!(
428        "cannot define workflow `{name}` as only one workflow is allowed per source file",
429        name = name.text(),
430    ))
431    .with_label("consider moving this workflow to a new file", name.span())
432    .with_label("first workflow is defined here", first)
433}
434
435/// Creates a "recursive struct" diagnostic.
436pub fn recursive_struct(name: &str, span: Span, member: Span) -> Diagnostic {
437    Diagnostic::error(format!("struct `{name}` has a recursive definition"))
438        .with_highlight(span)
439        .with_label("this struct member participates in the recursion", member)
440}
441
442/// Creates a "recursive enum" diagnostic.
443pub fn recursive_enum(name: &str, span: Span, ty: &str) -> Diagnostic {
444    // Unlike `recursive_struct`, which labels individual members, an `enum` has a
445    // single type for all of its choices. Just highlight the `enum` name, as
446    // its type as a *whole* is recursive.
447    Diagnostic::error(format!("enum `{name}` has a recursive definition"))
448        .with_highlight(span)
449        .with_help(format!("the type `{ty}` participates in the recursion"))
450}
451
452/// Creates an "unknown type" diagnostic.
453pub fn unknown_type(name: &str, span: Span) -> Diagnostic {
454    Diagnostic::error(format!("unknown type name `{name}`")).with_highlight(span)
455}
456
457/// Creates a "type mismatch" diagnostic.
458pub fn type_mismatch(
459    expected: &Type,
460    expected_span: Span,
461    actual: &Type,
462    actual_span: Span,
463) -> Diagnostic {
464    Diagnostic::error(format!(
465        "type mismatch: expected {expected:#}, but found {actual:#}"
466    ))
467    .with_label(format!("this is {actual:#}"), actual_span)
468    .with_label(format!("this expects {expected:#}"), expected_span)
469}
470
471/// Creates a "non-empty array assignment" diagnostic.
472pub fn non_empty_array_assignment(expected_span: Span, actual_span: Span) -> Diagnostic {
473    Diagnostic::error("cannot assign an empty array to a non-empty array type")
474        .with_label("this is an empty array", actual_span)
475        .with_label("this expects a non-empty array", expected_span)
476}
477
478/// Creates a "call input type mismatch" diagnostic.
479pub fn call_input_type_mismatch<T: TreeToken>(
480    name: &Ident<T>,
481    expected: &Type,
482    actual: &Type,
483) -> Diagnostic {
484    Diagnostic::error(format!(
485        "type mismatch: expected {expected:#}, but found {actual:#}",
486    ))
487    .with_label(
488        format!(
489            "input `{name}` is {expected:#}, but name `{name}` is {actual:#}",
490            name = name.text(),
491        ),
492        name.span(),
493    )
494}
495
496/// Creates a "no common type" diagnostic for arrays, maps, and scope unions.
497///
498/// This is called if the elements of a map or an array do not have a common
499/// type.
500pub fn no_common_type(
501    expected: &Type,
502    expected_span: Span,
503    actual: &Type,
504    actual_span: Span,
505) -> Diagnostic {
506    Diagnostic::error(format!(
507        "type mismatch: a type common to both {expected:#} and {actual:#} does not exist"
508    ))
509    .with_label(format!("this is {actual:#}"), actual_span)
510    .with_label(
511        format!("this and all prior elements had a common {expected:#}"),
512        expected_span,
513    )
514}
515
516/// Creates a "multiple type mismatch" diagnostic.
517pub fn multiple_type_mismatch(
518    expected: &[Type],
519    expected_span: Span,
520    actual: &Type,
521    actual_span: Span,
522) -> Diagnostic {
523    Diagnostic::error(format!(
524        "type mismatch: expected {expected:#}, but found {actual:#}",
525        expected = display_types(expected),
526    ))
527    .with_label(format!("this is {actual:#}"), actual_span)
528    .with_label(
529        format!(
530            "this expects {expected:#}",
531            expected = display_types(expected)
532        ),
533        expected_span,
534    )
535}
536
537/// Creates a "not a task member" diagnostic.
538pub fn not_a_task_member<T: TreeToken>(member: &Ident<T>) -> Diagnostic {
539    Diagnostic::error(format!(
540        "the `task` variable does not have a member named `{member}`",
541        member = member.text()
542    ))
543    .with_highlight(member.span())
544}
545
546/// Creates a "not a task.previous member" diagnostic.
547pub fn not_a_previous_task_data_member<T: TreeToken>(member: &Ident<T>) -> Diagnostic {
548    Diagnostic::error(format!(
549        "`task.previous` does not have a member named `{member}`",
550        member = member.text()
551    ))
552    .with_highlight(member.span())
553}
554
555/// Creates a "not a struct" diagnostic.
556pub fn not_a_struct<T: TreeToken>(member: &Ident<T>, input: bool) -> Diagnostic {
557    Diagnostic::error(format!(
558        "{kind} `{member}` is not a struct",
559        kind = if input { "input" } else { "struct member" },
560        member = member.text()
561    ))
562    .with_highlight(member.span())
563}
564
565/// Creates a "not a struct member" diagnostic.
566pub fn not_a_struct_member<T: TreeToken>(name: &str, member: &Ident<T>) -> Diagnostic {
567    Diagnostic::error(format!(
568        "struct `{name}` does not have a member named `{member}`",
569        member = member.text()
570    ))
571    .with_highlight(member.span())
572}
573
574/// Creates a "not an enum choice" diagnostic.
575pub fn not_an_enum_choice<T: TreeToken>(name: &str, choice: &Ident<T>) -> Diagnostic {
576    Diagnostic::error(format!(
577        "enum `{name}` does not have a choice named `{choice}`",
578        choice = choice.text()
579    ))
580    .with_highlight(choice.span())
581}
582
583/// Creates a "non-literal enum value" diagnostic.
584pub fn non_literal_enum_value(span: Span) -> Diagnostic {
585    Diagnostic::error("enum choice value must be a literal expression")
586        .with_highlight(span)
587        .with_fix(
588            "enum values must be literal expressions only (string literals, numeric literals, \
589             collection literals, or struct literals); string interpolation, variable references, \
590             and computed expressions are not allowed",
591        )
592}
593
594/// Creates a "not a pair accessor" diagnostic.
595pub fn not_a_pair_accessor<T: TreeToken>(name: &Ident<T>) -> Diagnostic {
596    Diagnostic::error(format!(
597        "cannot access a pair with name `{name}`",
598        name = name.text()
599    ))
600    .with_highlight(name.span())
601    .with_fix("use `left` or `right` to access a pair")
602}
603
604/// Creates a "missing struct members" diagnostic.
605pub fn missing_struct_members<T: TreeToken>(
606    name: &Ident<T>,
607    count: usize,
608    members: &str,
609) -> Diagnostic {
610    Diagnostic::error(format!(
611        "struct `{name}` requires a value for member{s} {members}",
612        name = name.text(),
613        s = if count > 1 { "s" } else { "" },
614    ))
615    .with_highlight(name.span())
616}
617
618/// Creates a "map key not primitive" diagnostic.
619pub fn map_key_not_primitive(span: Span, actual: &Type) -> Diagnostic {
620    Diagnostic::error("expected map key to be a non-optional primitive type")
621        .with_highlight(span)
622        .with_label(format!("this is {actual:#}"), span)
623}
624
625/// Creates a "if conditional mismatch" diagnostic.
626pub fn if_conditional_mismatch(actual: &Type, actual_span: Span) -> Diagnostic {
627    Diagnostic::error(format!(
628        "type mismatch: expected `if` conditional expression to be type `Boolean`, but found \
629         {actual:#}"
630    ))
631    .with_label(format!("this is {actual:#}"), actual_span)
632}
633
634/// Creates an "else if not supported" diagnostic.
635pub fn else_if_not_supported(version: SupportedVersion, span: Span) -> Diagnostic {
636    Diagnostic::error(format!(
637        "`else if` conditional clauses are not supported in WDL v{version}"
638    ))
639    .with_label("this `else if` is not supported", span)
640    .with_fix("use WDL v1.3 or higher to use `else if` conditional clauses")
641}
642
643/// Creates an "else not supported" diagnostic.
644pub fn else_not_supported(version: SupportedVersion, span: Span) -> Diagnostic {
645    Diagnostic::error(format!(
646        "`else` conditional clauses are not supported in WDL v{version}"
647    ))
648    .with_label("this `else` is not supported", span)
649    .with_fix("use WDL v1.3 or higher to use `else` conditional clauses")
650}
651
652/// Creates an "enum not supported" diagnostic.
653pub fn enum_not_supported(version: SupportedVersion, span: Span) -> Diagnostic {
654    Diagnostic::error(format!("enums are not supported in WDL v{version}"))
655        .with_label("this enum is not supported", span)
656        .with_fix("use WDL v1.3 or higher to use enums")
657}
658
659/// Creates a "logical not mismatch" diagnostic.
660pub fn logical_not_mismatch(actual: &Type, actual_span: Span) -> Diagnostic {
661    Diagnostic::error(format!(
662        "type mismatch: expected `logical not` operand to be type `Boolean`, but found {actual:#}"
663    ))
664    .with_label(format!("this is {actual:#}"), actual_span)
665}
666
667/// Creates a "negation mismatch" diagnostic.
668pub fn negation_mismatch(actual: &Type, actual_span: Span) -> Diagnostic {
669    Diagnostic::error(format!(
670        "type mismatch: expected negation operand to be type `Int` or `Float`, but found \
671         {actual:#}"
672    ))
673    .with_label(format!("this is {actual:#}"), actual_span)
674}
675
676/// Creates a "logical or mismatch" diagnostic.
677pub fn logical_or_mismatch(actual: &Type, actual_span: Span) -> Diagnostic {
678    Diagnostic::error(format!(
679        "type mismatch: expected `logical or` operand to be type `Boolean`, but found {actual:#}"
680    ))
681    .with_label(format!("this is {actual:#}"), actual_span)
682}
683
684/// Creates a "logical and mismatch" diagnostic.
685pub fn logical_and_mismatch(actual: &Type, actual_span: Span) -> Diagnostic {
686    Diagnostic::error(format!(
687        "type mismatch: expected `logical and` operand to be type `Boolean`, but found {actual:#}"
688    ))
689    .with_label(format!("this is {actual:#}"), actual_span)
690}
691
692/// Creates a "comparison mismatch" diagnostic.
693pub fn comparison_mismatch(
694    op: ComparisonOperator,
695    span: Span,
696    lhs: &Type,
697    lhs_span: Span,
698    rhs: &Type,
699    rhs_span: Span,
700) -> Diagnostic {
701    Diagnostic::error(format!(
702        "type mismatch: operator `{op}` cannot compare {lhs:#} to {rhs:#}"
703    ))
704    .with_highlight(span)
705    .with_label(format!("this is {lhs:#}"), lhs_span)
706    .with_label(format!("this is {rhs:#}"), rhs_span)
707}
708
709/// Creates a "numeric mismatch" diagnostic.
710pub fn numeric_mismatch(
711    op: NumericOperator,
712    span: Span,
713    lhs: &Type,
714    lhs_span: Span,
715    rhs: &Type,
716    rhs_span: Span,
717) -> Diagnostic {
718    Diagnostic::error(format!(
719        "type mismatch: {op} operator is not supported for {lhs:#} and {rhs:#}"
720    ))
721    .with_highlight(span)
722    .with_label(format!("this is {lhs:#}"), lhs_span)
723    .with_label(format!("this is {rhs:#}"), rhs_span)
724}
725
726/// Creates a "string concat mismatch" diagnostic.
727pub fn string_concat_mismatch(actual: &Type, actual_span: Span) -> Diagnostic {
728    Diagnostic::error(format!(
729        "type mismatch: string concatenation is not supported for {actual:#}"
730    ))
731    .with_label(format!("this is {actual:#}"), actual_span)
732}
733
734/// Creates an "unknown function" diagnostic.
735pub fn unknown_function(name: &str, span: Span) -> Diagnostic {
736    Diagnostic::error(format!("unknown function `{name}`")).with_label(
737        "the WDL standard library does not have a function with this name",
738        span,
739    )
740}
741
742/// Creates an "unsupported function" diagnostic.
743pub fn unsupported_function(minimum: SupportedVersion, name: &str, span: Span) -> Diagnostic {
744    Diagnostic::error(format!(
745        "this use of function `{name}` requires a minimum WDL version of {minimum}"
746    ))
747    .with_highlight(span)
748}
749
750/// Creates a "too few arguments" diagnostic.
751pub fn too_few_arguments(name: &str, span: Span, minimum: usize, count: usize) -> Diagnostic {
752    Diagnostic::error(format!(
753        "function `{name}` requires at least {minimum} argument{s} but {count} {v} supplied",
754        s = if minimum == 1 { "" } else { "s" },
755        v = if count == 1 { "was" } else { "were" },
756    ))
757    .with_highlight(span)
758}
759
760/// Creates a "too many arguments" diagnostic.
761pub fn too_many_arguments(
762    name: &str,
763    span: Span,
764    maximum: usize,
765    count: usize,
766    excessive: impl Iterator<Item = Span>,
767) -> Diagnostic {
768    let mut diagnostic = Diagnostic::error(format!(
769        "function `{name}` requires no more than {maximum} argument{s} but {count} {v} supplied",
770        s = if maximum == 1 { "" } else { "s" },
771        v = if count == 1 { "was" } else { "were" },
772    ))
773    .with_highlight(span);
774
775    for span in excessive {
776        diagnostic = diagnostic.with_label("this argument is unexpected", span);
777    }
778
779    diagnostic
780}
781
782/// Constructs an "argument type mismatch" diagnostic.
783pub fn argument_type_mismatch(name: &str, expected: &str, actual: &Type, span: Span) -> Diagnostic {
784    Diagnostic::error(format!(
785        "type mismatch: argument to function `{name}` expects {expected}, but found {actual:#}"
786    ))
787    .with_label(format!("this is {actual:#}"), span)
788}
789
790/// Constructs an "ambiguous argument" diagnostic.
791pub fn ambiguous_argument(name: &str, span: Span, first: &str, second: &str) -> Diagnostic {
792    Diagnostic::error(format!(
793        "ambiguous call to function `{name}` with conflicting signatures `{first}` and `{second}`",
794    ))
795    .with_highlight(span)
796}
797
798/// Constructs an "index type mismatch" diagnostic.
799pub fn index_type_mismatch(expected: &Type, actual: &Type, span: Span) -> Diagnostic {
800    Diagnostic::error(format!(
801        "type mismatch: expected index to be {expected:#}, but found {actual:#}"
802    ))
803    .with_label(format!("this is {actual:#}"), span)
804}
805
806/// Constructs an "type is not array" diagnostic.
807pub fn type_is_not_array(actual: &Type, span: Span) -> Diagnostic {
808    Diagnostic::error(format!(
809        "type mismatch: expected an array type, but found {actual:#}"
810    ))
811    .with_label(format!("this is {actual:#}"), span)
812}
813
814/// Constructs a "cannot access" diagnostic.
815pub fn cannot_access(actual: &Type, actual_span: Span) -> Diagnostic {
816    Diagnostic::error(format!("cannot access {actual:#}"))
817        .with_label(format!("this is {actual:#}"), actual_span)
818}
819
820/// Constructs a "cannot coerce to string" diagnostic.
821pub fn cannot_coerce_to_string(actual: &Type, span: Span) -> Diagnostic {
822    Diagnostic::error(format!("cannot coerce {actual:#} to type `String`"))
823        .with_label(format!("this is {actual:#}"), span)
824}
825
826/// Creates an "unknown task or workflow" diagnostic.
827pub fn unknown_task_or_workflow(namespace: Option<Span>, name: &str, span: Span) -> Diagnostic {
828    let mut diagnostic =
829        Diagnostic::error(format!("unknown task or workflow `{name}`")).with_highlight(span);
830
831    if let Some(namespace) = namespace {
832        diagnostic = diagnostic.with_label(
833            format!("this namespace does not have a task or workflow named `{name}`"),
834            namespace,
835        );
836    }
837
838    diagnostic
839}
840
841/// Creates an "unknown call input/output" diagnostic.
842pub fn unknown_call_io<T: TreeToken>(call: &CallType, name: &Ident<T>, io: Io) -> Diagnostic {
843    Diagnostic::error(format!(
844        "{kind} `{call}` does not have an {io} named `{name}`",
845        kind = call.kind(),
846        call = call.name(),
847        name = name.text(),
848    ))
849    .with_highlight(name.span())
850}
851
852/// Creates an "unknown task input/output name" diagnostic.
853pub fn unknown_task_io<T: TreeToken>(task_name: &str, name: &Ident<T>, io: Io) -> Diagnostic {
854    Diagnostic::error(format!(
855        "task `{task_name}` does not have an {io} named `{name}`",
856        name = name.text(),
857    ))
858    .with_highlight(name.span())
859}
860
861/// Creates a "recursive workflow call" diagnostic.
862pub fn recursive_workflow_call(name: &str, span: Span) -> Diagnostic {
863    Diagnostic::error(format!("cannot recursively call workflow `{name}`")).with_highlight(span)
864}
865
866/// Creates a "missing call input" diagnostic.
867pub fn missing_call_input<T: TreeToken>(
868    kind: CallKind,
869    target: &Ident<T>,
870    input: &str,
871    nested_inputs_allowed: bool,
872) -> Diagnostic {
873    let message = format!(
874        "missing required call input `{input}` for {kind} `{target}`",
875        target = target.text(),
876    );
877
878    if nested_inputs_allowed {
879        Diagnostic::warning(message).with_highlight(target.span())
880    } else {
881        Diagnostic::error(message).with_highlight(target.span())
882    }
883}
884
885/// Creates an "unused import" diagnostic.
886pub fn unused_import(name: &str, span: Span) -> Diagnostic {
887    Diagnostic::warning(format!("unused import namespace `{name}`"))
888        .with_rule(UnusedImportRule::ID)
889        .with_highlight(span)
890}
891
892/// Creates an "unused input" diagnostic.
893pub fn unused_input(name: &str, span: Span) -> Diagnostic {
894    Diagnostic::warning(format!("unused input `{name}`"))
895        .with_rule(UnusedInputRule::ID)
896        .with_highlight(span)
897}
898
899/// Creates an "unused declaration" diagnostic.
900pub fn unused_declaration(name: &str, span: Span) -> Diagnostic {
901    Diagnostic::warning(format!("unused declaration `{name}`"))
902        .with_rule(UnusedDeclarationRule::ID)
903        .with_highlight(span)
904}
905
906/// Creates a "misleading declaration order" diagnostic.
907pub fn misleading_declaration_order(name: &str, span: Span) -> Diagnostic {
908    Diagnostic::warning("variable declaration appears after the `command` section")
909        .with_rule(MisleadingDeclarationOrderRule::ID)
910        .with_highlight(span)
911        .with_help(
912            "this is visually misleading; tasks are evaluated in dependency order, not \
913             top-to-bottom",
914        )
915        .with_fix(format!(
916            "move the declaration of `{name}` above the `command` section"
917        ))
918}
919
920/// Creates an "unused call" diagnostic.
921pub fn unused_call(name: &str, span: Span) -> Diagnostic {
922    Diagnostic::warning(format!("unused call `{name}`"))
923        .with_rule(UnusedCallRule::ID)
924        .with_highlight(span)
925}
926
927/// Creates an "unnecessary function call" diagnostic.
928pub fn unnecessary_function_call(
929    name: &str,
930    span: Span,
931    label: &str,
932    label_span: Span,
933) -> Diagnostic {
934    Diagnostic::warning(format!("unnecessary call to function `{name}`"))
935        .with_rule(UnnecessaryFunctionCall::ID)
936        .with_highlight(span)
937        .with_label(label.to_string(), label_span)
938}
939
940/// Creates a "meaningless lint directive" diagnostic.
941pub fn meaningless_lint_directive(rule: &str, span: Span, severity: Severity) -> Diagnostic {
942    Diagnostic::note(format!(
943        "unnecessary `except` directive for lint rule `{rule}`"
944    ))
945    .with_rule(MeaninglessLintDirective::ID)
946    .with_highlight(span)
947    .with_severity(severity)
948}
949
950/// Generates a diagnostic error message when a placeholder option has a type
951/// mismatch.
952pub fn invalid_placeholder_option<N: TreeNode>(
953    ty: &Type,
954    span: Span,
955    option: &PlaceholderOption<N>,
956) -> Diagnostic {
957    let message = match option {
958        PlaceholderOption::Sep(_) => format!(
959            "type mismatch for placeholder option `sep`: expected type `Array[P]` where P: any \
960             primitive type, but found {ty:#}"
961        ),
962        PlaceholderOption::Default(_) => format!(
963            "type mismatch for placeholder option `default`: expected any primitive type, but \
964             found {ty:#}"
965        ),
966        PlaceholderOption::TrueFalse(_) => format!(
967            "type mismatch for placeholder option `true/false`: expected type `Boolean`, but \
968             found {ty:#}"
969        ),
970    };
971
972    Diagnostic::error(message).with_label(format!("this is {ty:#}"), span)
973}
974
975/// Creates an invalid regex pattern diagnostic.
976pub fn invalid_regex_pattern(
977    function: &str,
978    pattern: &str,
979    error: &regex::Error,
980    span: Span,
981) -> Diagnostic {
982    Diagnostic::error(format!(
983        "invalid regular expression `{pattern}` used in function `{function}`: {error}"
984    ))
985    .with_label("invalid regular expression", span)
986}
987
988/// Creates a "not a custom type" diagnostic.
989pub fn not_a_custom_type<T: TreeToken>(name: &Ident<T>) -> Diagnostic {
990    Diagnostic::error(format!("`{}` is not a custom type", name.text())).with_label(
991        "only struct and enum types can be referenced as values",
992        name.span(),
993    )
994}
995
996/// Creates a "no common inferred type for enum" diagnostic.
997///
998/// This diagnostic occurs during enum type calculation when no common type can
999/// be inferred from the choice types.
1000pub fn no_common_inferred_type_for_enum(
1001    enum_name: &str,
1002    common_type: &Type,
1003    common_span: Span,
1004    discordant_type: &Type,
1005    discordant_span: Span,
1006) -> Diagnostic {
1007    Diagnostic::error(format!("cannot infer a common type for enum `{enum_name}`"))
1008        .with_label(
1009            format!(
1010                "this is the first choice with {discordant_type:#} that has no common type with \
1011                 {common_type:#}"
1012            ),
1013            discordant_span,
1014        )
1015        .with_label(
1016            format!("this is the last choice with a common {common_type:#}"),
1017            common_span,
1018        )
1019}
1020
1021/// Creates an "enum choice does not coerce to type" diagnostic.
1022pub fn enum_choice_does_not_coerce_to_type(
1023    enum_name: &str,
1024    enum_span: Span,
1025    choice_name: &str,
1026    choice_span: Span,
1027    expected: &Type,
1028    actual: &Type,
1029) -> Diagnostic {
1030    Diagnostic::error(format!(
1031        "cannot coerce choice `{choice_name}` in enum `{enum_name}` from {actual:#} to \
1032         {expected:#}"
1033    ))
1034    .with_label(format!("this is the `{enum_name}` enum"), enum_span)
1035    .with_label(format!("this is the `{choice_name}` choice"), choice_span)
1036    .with_fix(format!(
1037        "change the value to something that coerces to {expected:#} or explicitly set the enum's \
1038         inner type"
1039    ))
1040}