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