1use std::collections::HashMap;
4
5use strsim::levenshtein;
6use wdl_ast::AstNode;
7use wdl_ast::Comment;
8use wdl_ast::Diagnostic;
9use wdl_ast::ExceptRule;
10use wdl_ast::SupportedVersion;
11use wdl_ast::TreeNode;
12use wdl_ast::VersionStatement;
13use wdl_ast::Whitespace;
14use wdl_ast::v1;
15use wdl_grammar::Severity;
16use wdl_grammar::SyntaxKind;
17
18use crate::ALL_RULE_IDS;
19use crate::Config;
20use crate::ExceptDirectiveValidRule;
21use crate::Exceptable;
22use crate::MeaninglessLintDirective;
23use crate::RuleMap;
24use crate::VisitReason;
25use crate::Visitor;
26use crate::diagnostics::meaningless_lint_directive;
27use crate::document::Document;
28use crate::rules::RULE_MAP;
29
30mod counts;
31mod env;
32mod exceptions;
33mod exprs;
34mod imports;
35mod keys;
36mod numbers;
37mod requirements;
38mod strings;
39mod version;
40
41pub fn find_nearest_rule<'a>(
44 known_rules: impl IntoIterator<Item = &'a str>,
45 unknown_rule_id: &str,
46) -> Option<String> {
47 let threshold = if unknown_rule_id.len() <= 3 {
48 1
49 } else if unknown_rule_id.len() <= 10 {
50 unknown_rule_id.len() / 3 + 1
51 } else {
52 5
53 };
54
55 known_rules
56 .into_iter()
57 .map(|rule_id| (rule_id, levenshtein(unknown_rule_id, rule_id)))
58 .filter(|(_, distance)| *distance <= threshold)
59 .min_by_key(|(_, distance)| *distance)
60 .map(|(rule_id, _)| rule_id.to_string())
61}
62
63#[derive(Clone, Debug, Default)]
68pub struct Diagnostics {
69 pub(crate) diagnostics: Vec<Diagnostic>,
71 exceptions: HashMap<ExceptRule, bool>,
75}
76
77impl Diagnostics {
78 pub fn add(&mut self, diagnostic: Diagnostic) {
83 self.diagnostics.push(diagnostic);
84 }
85
86 pub fn add_exceptions(&mut self, exceptions: impl IntoIterator<Item = ExceptRule>) {
88 for e in exceptions {
89 self.exceptions.entry(e).or_insert(false);
90 }
91 }
92
93 pub fn exceptable_add<N: TreeNode + Exceptable>(
98 &mut self,
99 diagnostic: Diagnostic,
100 element: &N,
101 exceptable_nodes: &Option<&'static [SyntaxKind]>,
102 ) {
103 let Some(target_rule) = diagnostic.rule() else {
104 self.add(diagnostic);
105 return;
106 };
107
108 for node in element.ancestors().filter(|node| {
109 exceptable_nodes
110 .as_ref()
111 .is_none_or(|nodes| nodes.contains(&node.kind()))
112 }) {
113 let mut rule_excepted = false;
114 for rule in node
115 .rule_exceptions()
116 .into_iter()
117 .filter(|rule| rule.name == target_rule)
118 {
119 rule_excepted = true;
120 self.exceptions
121 .entry(rule)
122 .and_modify(|applied| *applied = true);
123 }
124
125 if rule_excepted {
126 return;
127 }
128 }
129
130 self.add(diagnostic);
131 }
132
133 pub fn is_empty(&self) -> bool {
135 self.diagnostics.is_empty()
136 }
137
138 pub fn has_errors(&self) -> bool {
140 self.iter()
141 .any(|diagnostic| diagnostic.severity() == Severity::Error)
142 }
143
144 pub fn sort(&mut self) {
146 self.diagnostics.sort();
147 }
148
149 pub fn iter(&self) -> std::slice::Iter<'_, Diagnostic> {
151 self.diagnostics.iter()
152 }
153}
154
155impl Extend<Diagnostic> for Diagnostics {
156 fn extend<I: IntoIterator<Item = Diagnostic>>(&mut self, iter: I) {
157 self.diagnostics.extend(iter);
158 }
159}
160
161impl IntoIterator for Diagnostics {
162 type IntoIter = std::vec::IntoIter<Self::Item>;
163 type Item = Diagnostic;
164
165 fn into_iter(self) -> Self::IntoIter {
166 self.diagnostics.into_iter()
167 }
168}
169
170impl From<Diagnostics> for Vec<Diagnostic> {
171 fn from(input: Diagnostics) -> Self {
172 input.diagnostics
173 }
174}
175
176#[allow(missing_debug_implementations)]
182pub struct Validator {
183 visitors: Vec<Box<dyn Visitor>>,
185 exceptions: exceptions::Exceptions,
187}
188
189impl Validator {
190 pub fn empty() -> Self {
192 Self {
193 visitors: Vec::new(),
194 exceptions: exceptions::Exceptions::new(
196 RULE_MAP
197 .iter()
198 .map(|(name, exceptable_nodes)| (name.to_string(), *exceptable_nodes))
199 .collect(),
200 ),
201 }
202 }
203
204 pub fn add_visitor<V: Visitor + 'static>(&mut self, visitor: V) {
206 self.add_visitors([Box::new(visitor) as Box<dyn Visitor>]);
207 }
208
209 pub fn add_visitors(&mut self, visitors: impl IntoIterator<Item = Box<dyn Visitor>>) {
211 for visitor in visitors {
212 self.exceptions.extend_rules(visitor.rules());
213 self.visitors.push(visitor);
214 }
215 }
216
217 pub fn extend_rules(
219 &mut self,
220 rules: impl IntoIterator<Item = (String, Option<&'static [SyntaxKind]>)>,
221 ) {
222 self.exceptions.extend_rules(rules);
223 }
224
225 fn check_meaningless_lint_directives(
236 &self,
237 document: &Document,
238 diagnostics: &mut Diagnostics,
239 severity: Severity,
240 ) {
241 let mut meaningless_lint_directives = Diagnostics::default();
242
243 let visitor_known_rules = self.rules();
244
245 let invalid_directives = diagnostics
250 .iter()
251 .filter_map(|d| {
252 if d.rule() == Some(ExceptDirectiveValidRule::ID) {
253 d.labels().next().map(|l| l.span())
254 } else {
255 None
256 }
257 })
258 .collect::<Vec<_>>();
259
260 for (exception, applied) in &diagnostics.exceptions {
261 if *applied
262 || invalid_directives.contains(&exception.span)
264 || (!ALL_RULE_IDS.iter().any(|r| r == &exception.name) && !visitor_known_rules.contains_key(&exception.name))
266 {
267 continue;
268 }
269
270 let diagnostic = meaningless_lint_directive(&exception.name, exception.span, severity);
271 if let Some(target) = exception.target_node(&document.root()) {
272 meaningless_lint_directives.exceptable_add(
273 diagnostic,
274 &target,
275 &MeaninglessLintDirective::EXCEPTABLE_NODES,
276 );
277 } else {
278 meaningless_lint_directives.add(diagnostic);
279 }
280 }
281
282 diagnostics.extend(meaningless_lint_directives.diagnostics);
283 }
284
285 pub fn validate(&mut self, document: &Document, config: &Config) -> Result<(), Diagnostics> {
288 let mut diagnostics = Diagnostics {
289 exceptions: document.analysis_diagnostics().exceptions.clone(),
290 ..Default::default()
291 };
292
293 self.register(config);
294 document.visit(&mut diagnostics, self);
295
296 if let Some(severity) = document
297 .config()
298 .diagnostics_config()
299 .meaningless_lint_directive
300 {
301 self.check_meaningless_lint_directives(document, &mut diagnostics, severity);
302 }
303
304 self.reset();
305
306 if diagnostics.is_empty() {
307 Ok(())
308 } else {
309 diagnostics.sort();
310 Err(diagnostics)
311 }
312 }
313
314 pub fn find_nearest_rule(&self, unknown_rule_id: &str) -> Option<String> {
317 find_nearest_rule(
318 self.exceptions.known_rules().keys().map(String::as_str),
319 unknown_rule_id,
320 )
321 }
322}
323
324impl Default for Validator {
325 fn default() -> Self {
327 let mut validator = Self::empty();
328 validator.add_visitors([
329 Box::new(strings::LiteralTextVisitor) as Box<dyn Visitor>,
330 Box::<counts::CountingVisitor>::default(),
331 Box::<keys::UniqueKeysVisitor>::default(),
332 Box::<numbers::NumberVisitor>::default(),
333 Box::<version::VersionVisitor>::default(),
334 Box::<requirements::RequirementsVisitor>::default(),
335 Box::<exprs::ScopedExprVisitor>::default(),
336 Box::<imports::ImportsVisitor>::default(),
337 Box::<env::EnvVisitor>::default(),
338 ]);
339 validator
340 }
341}
342
343impl Visitor for Validator {
344 fn rules(&self) -> RuleMap {
345 let mut rules = HashMap::new();
346 for visitor in &self.visitors {
347 rules.extend(visitor.rules());
348 }
349 rules
350 }
351
352 fn register(&mut self, config: &crate::Config) {
353 for visitor in self.visitors.iter_mut() {
354 visitor.register(config);
355 }
356 }
357
358 fn reset(&mut self) {
359 self.exceptions.reset();
360 for visitor in self.visitors.iter_mut() {
361 visitor.reset();
362 }
363 }
364
365 fn document(
366 &mut self,
367 diagnostics: &mut Diagnostics,
368 reason: VisitReason,
369 doc: &Document,
370 version: SupportedVersion,
371 ) {
372 self.exceptions.document(diagnostics, reason, doc, version);
373 for visitor in self.visitors.iter_mut() {
374 visitor.document(diagnostics, reason, doc, version);
375 }
376 }
377
378 fn whitespace(&mut self, diagnostics: &mut Diagnostics, whitespace: &Whitespace) {
379 for visitor in self.visitors.iter_mut() {
380 visitor.whitespace(diagnostics, whitespace);
381 }
382 }
383
384 fn comment(&mut self, diagnostics: &mut Diagnostics, comment: &Comment) {
385 self.exceptions.comment(diagnostics, comment);
386 for visitor in self.visitors.iter_mut() {
387 visitor.comment(diagnostics, comment);
388 }
389 }
390
391 fn version_statement(
392 &mut self,
393 diagnostics: &mut Diagnostics,
394 reason: VisitReason,
395 stmt: &VersionStatement,
396 ) {
397 if reason == VisitReason::Enter {
398 for (rule, applied) in &mut diagnostics.exceptions {
400 if rule.span < stmt.span() {
401 *applied = true;
402 }
403 }
404 }
405
406 for visitor in self.visitors.iter_mut() {
407 visitor.version_statement(diagnostics, reason, stmt);
408 }
409 }
410
411 fn import_statement(
412 &mut self,
413 diagnostics: &mut Diagnostics,
414 reason: VisitReason,
415 stmt: &v1::ImportStatement,
416 ) {
417 for visitor in self.visitors.iter_mut() {
418 visitor.import_statement(diagnostics, reason, stmt);
419 }
420 }
421
422 fn struct_definition(
423 &mut self,
424 diagnostics: &mut Diagnostics,
425 reason: VisitReason,
426 def: &v1::StructDefinition,
427 ) {
428 for visitor in self.visitors.iter_mut() {
429 visitor.struct_definition(diagnostics, reason, def);
430 }
431 }
432
433 fn enum_definition(
434 &mut self,
435 diagnostics: &mut Diagnostics,
436 reason: VisitReason,
437 def: &v1::EnumDefinition,
438 ) {
439 for visitor in self.visitors.iter_mut() {
440 visitor.enum_definition(diagnostics, reason, def);
441 }
442 }
443
444 fn task_definition(
445 &mut self,
446 diagnostics: &mut Diagnostics,
447 reason: VisitReason,
448 task: &v1::TaskDefinition,
449 ) {
450 for visitor in self.visitors.iter_mut() {
451 visitor.task_definition(diagnostics, reason, task);
452 }
453 }
454
455 fn workflow_definition(
456 &mut self,
457 diagnostics: &mut Diagnostics,
458 reason: VisitReason,
459 workflow: &v1::WorkflowDefinition,
460 ) {
461 for visitor in self.visitors.iter_mut() {
462 visitor.workflow_definition(diagnostics, reason, workflow);
463 }
464 }
465
466 fn input_section(
467 &mut self,
468 diagnostics: &mut Diagnostics,
469 reason: VisitReason,
470 section: &v1::InputSection,
471 ) {
472 for visitor in self.visitors.iter_mut() {
473 visitor.input_section(diagnostics, reason, section);
474 }
475 }
476
477 fn output_section(
478 &mut self,
479 diagnostics: &mut Diagnostics,
480 reason: VisitReason,
481 section: &v1::OutputSection,
482 ) {
483 for visitor in self.visitors.iter_mut() {
484 visitor.output_section(diagnostics, reason, section);
485 }
486 }
487
488 fn command_section(
489 &mut self,
490 diagnostics: &mut Diagnostics,
491 reason: VisitReason,
492 section: &v1::CommandSection,
493 ) {
494 for visitor in self.visitors.iter_mut() {
495 visitor.command_section(diagnostics, reason, section);
496 }
497 }
498
499 fn command_text(&mut self, diagnostics: &mut Diagnostics, text: &v1::CommandText) {
500 for visitor in self.visitors.iter_mut() {
501 visitor.command_text(diagnostics, text);
502 }
503 }
504
505 fn requirements_section(
506 &mut self,
507 diagnostics: &mut Diagnostics,
508 reason: VisitReason,
509 section: &v1::RequirementsSection,
510 ) {
511 for visitor in self.visitors.iter_mut() {
512 visitor.requirements_section(diagnostics, reason, section);
513 }
514 }
515
516 fn task_hints_section(
517 &mut self,
518 diagnostics: &mut Diagnostics,
519 reason: VisitReason,
520 section: &v1::TaskHintsSection,
521 ) {
522 for visitor in self.visitors.iter_mut() {
523 visitor.task_hints_section(diagnostics, reason, section);
524 }
525 }
526
527 fn workflow_hints_section(
528 &mut self,
529 diagnostics: &mut Diagnostics,
530 reason: VisitReason,
531 section: &v1::WorkflowHintsSection,
532 ) {
533 for visitor in self.visitors.iter_mut() {
534 visitor.workflow_hints_section(diagnostics, reason, section);
535 }
536 }
537
538 fn runtime_section(
539 &mut self,
540 diagnostics: &mut Diagnostics,
541 reason: VisitReason,
542 section: &v1::RuntimeSection,
543 ) {
544 for visitor in self.visitors.iter_mut() {
545 visitor.runtime_section(diagnostics, reason, section);
546 }
547 }
548
549 fn runtime_item(
550 &mut self,
551 diagnostics: &mut Diagnostics,
552 reason: VisitReason,
553 item: &v1::RuntimeItem,
554 ) {
555 for visitor in self.visitors.iter_mut() {
556 visitor.runtime_item(diagnostics, reason, item);
557 }
558 }
559
560 fn metadata_section(
561 &mut self,
562 diagnostics: &mut Diagnostics,
563 reason: VisitReason,
564 section: &v1::MetadataSection,
565 ) {
566 for visitor in self.visitors.iter_mut() {
567 visitor.metadata_section(diagnostics, reason, section);
568 }
569 }
570
571 fn parameter_metadata_section(
572 &mut self,
573 diagnostics: &mut Diagnostics,
574 reason: VisitReason,
575 section: &v1::ParameterMetadataSection,
576 ) {
577 for visitor in self.visitors.iter_mut() {
578 visitor.parameter_metadata_section(diagnostics, reason, section);
579 }
580 }
581
582 fn metadata_object(
583 &mut self,
584 diagnostics: &mut Diagnostics,
585 reason: VisitReason,
586 object: &v1::MetadataObject,
587 ) {
588 for visitor in self.visitors.iter_mut() {
589 visitor.metadata_object(diagnostics, reason, object);
590 }
591 }
592
593 fn metadata_object_item(
594 &mut self,
595 diagnostics: &mut Diagnostics,
596 reason: VisitReason,
597 item: &v1::MetadataObjectItem,
598 ) {
599 for visitor in self.visitors.iter_mut() {
600 visitor.metadata_object_item(diagnostics, reason, item);
601 }
602 }
603
604 fn metadata_array(
605 &mut self,
606 diagnostics: &mut Diagnostics,
607 reason: VisitReason,
608 item: &v1::MetadataArray,
609 ) {
610 for visitor in self.visitors.iter_mut() {
611 visitor.metadata_array(diagnostics, reason, item);
612 }
613 }
614
615 fn unbound_decl(
616 &mut self,
617 diagnostics: &mut Diagnostics,
618 reason: VisitReason,
619 decl: &v1::UnboundDecl,
620 ) {
621 for visitor in self.visitors.iter_mut() {
622 visitor.unbound_decl(diagnostics, reason, decl);
623 }
624 }
625
626 fn bound_decl(
627 &mut self,
628 diagnostics: &mut Diagnostics,
629 reason: VisitReason,
630 decl: &v1::BoundDecl,
631 ) {
632 for visitor in self.visitors.iter_mut() {
633 visitor.bound_decl(diagnostics, reason, decl);
634 }
635 }
636
637 fn expr(&mut self, diagnostics: &mut Diagnostics, reason: VisitReason, expr: &v1::Expr) {
638 for visitor in self.visitors.iter_mut() {
639 visitor.expr(diagnostics, reason, expr);
640 }
641 }
642
643 fn string_text(&mut self, diagnostics: &mut Diagnostics, text: &v1::StringText) {
644 for visitor in self.visitors.iter_mut() {
645 visitor.string_text(diagnostics, text);
646 }
647 }
648
649 fn placeholder(
650 &mut self,
651 diagnostics: &mut Diagnostics,
652 reason: VisitReason,
653 placeholder: &v1::Placeholder,
654 ) {
655 for visitor in self.visitors.iter_mut() {
656 visitor.placeholder(diagnostics, reason, placeholder);
657 }
658 }
659
660 fn conditional_statement(
661 &mut self,
662 diagnostics: &mut Diagnostics,
663 reason: VisitReason,
664 stmt: &v1::ConditionalStatement,
665 ) {
666 for visitor in self.visitors.iter_mut() {
667 visitor.conditional_statement(diagnostics, reason, stmt);
668 }
669 }
670
671 fn scatter_statement(
672 &mut self,
673 diagnostics: &mut Diagnostics,
674 reason: VisitReason,
675 stmt: &v1::ScatterStatement,
676 ) {
677 for visitor in self.visitors.iter_mut() {
678 visitor.scatter_statement(diagnostics, reason, stmt);
679 }
680 }
681
682 fn call_statement(
683 &mut self,
684 diagnostics: &mut Diagnostics,
685 reason: VisitReason,
686 stmt: &v1::CallStatement,
687 ) {
688 for visitor in self.visitors.iter_mut() {
689 visitor.call_statement(diagnostics, reason, stmt);
690 }
691 }
692}
693
694#[cfg(test)]
695mod tests {
696 use super::*;
697
698 #[test_log::test]
699 fn test_find_nearest_rule() {
700 let validator = Validator::default();
701
702 let nearest = validator.find_nearest_rule("UnusedInput");
704 pretty_assertions::assert_eq!(nearest.as_deref(), Some("UnusedInput"));
705
706 let nearest = validator.find_nearest_rule("UnusedInputt");
708 pretty_assertions::assert_eq!(nearest.as_deref(), Some("UnusedInput"));
709
710 let nearest = validator.find_nearest_rule("UnusedCall");
712 pretty_assertions::assert_eq!(nearest.as_deref(), Some("UnusedCall"));
713
714 let nearest = validator.find_nearest_rule("UnusedKall");
716 pretty_assertions::assert_eq!(nearest.as_deref(), Some("UnusedCall"));
717
718 let nearest = validator.find_nearest_rule("UnnecessaryFunctionAl");
720 pretty_assertions::assert_eq!(nearest.as_deref(), Some("UnnecessaryFunctionCall"));
721
722 let nearest = validator.find_nearest_rule("CompletelyDifferentRule");
724 pretty_assertions::assert_eq!(nearest.as_deref(), None);
725 }
726}