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