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