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