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