1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Io {
27 Input,
29 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum Context {
45 Workflow(Span),
47 Task(Span),
49 Struct(Span),
51 StructMember(Span),
53 Name(NameContext),
55}
56
57impl Context {
58 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum NameContext {
85 Input(Span),
87 Output(Span),
89 Decl(Span),
91 Call(Span),
93 ScatterVariable(Span),
95}
96
97impl NameContext {
98 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
128pub 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
141pub 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
147pub fn unknown_name(name: &str, span: Span) -> Diagnostic {
149 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
160pub 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
167pub 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
182pub 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
194pub 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
216pub 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
237pub fn unknown_namespace(ns: &Ident) -> Diagnostic {
239 Diagnostic::error(format!("unknown namespace `{ns}`", ns = ns.as_str()))
240 .with_highlight(ns.span())
241}
242
243pub 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
249pub 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
255pub 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
260pub 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
280pub fn import_missing_version(span: Span) -> Diagnostic {
282 Diagnostic::error("imported document is missing a version statement").with_highlight(span)
283}
284
285pub fn invalid_relative_import(error: &url::ParseError, span: Span) -> Diagnostic {
287 Diagnostic::error(format!("{error:?}")).with_highlight(span)
288}
289
290pub 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
299pub 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
320pub 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
330pub 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
340pub 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
347pub fn unknown_type(name: &str, span: Span) -> Diagnostic {
349 Diagnostic::error(format!("unknown type name `{name}`")).with_highlight(span)
350}
351
352pub 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
366pub 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
373pub 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
387pub 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
401pub 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
422pub 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
431pub 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
441pub 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
450pub 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
460pub 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
470pub 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
477pub 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
486pub 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
495pub 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
504pub 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
513pub 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
522pub 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
539pub 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
556pub 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
564pub 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
572pub 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
580pub 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
590pub 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
612pub 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
621pub 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
629pub 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
637pub 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
645pub 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
651pub 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
657pub 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
678pub 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
689pub 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
698pub 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
707pub 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
716pub 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
723pub 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
730pub 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
737pub 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
744pub 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}