Skip to main content

wdl_analysis/
validation.rs

1//! Validator for WDL documents.
2
3use 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
41/// Finds the nearest known rule ID to the given unknown rule ID,
42/// or `None` if no rule ID is close enough.
43pub 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/// Represents a collection of validation diagnostics.
64///
65/// Validation visitors receive a diagnostics collection during
66/// visitation of the AST.
67#[derive(Clone, Debug, Default)]
68pub struct Diagnostics {
69    /// Diagnostics to emit.
70    pub(crate) diagnostics: Vec<Diagnostic>,
71    /// `#@ except:` directives discovered during traversal.
72    ///
73    /// `HashMap<Rule, applied>`
74    exceptions: HashMap<ExceptRule, bool>,
75}
76
77impl Diagnostics {
78    /// Adds a diagnostic to the collection.
79    ///
80    /// NOTE: This is intended for diagnostics that cannot be suppressed.
81    /// Otherwise, [`Diagnostics::exceptable_add()`] should be used.
82    pub fn add(&mut self, diagnostic: Diagnostic) {
83        self.diagnostics.push(diagnostic);
84    }
85
86    /// Adds rule exceptions to the collection.
87    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    /// Adds a diagnostic to the collection, unless the diagnostic is for an
94    /// element that has an exception for the given rule.
95    ///
96    /// If the diagnostic does not have a rule, the diagnostic is always added.
97    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    /// Returns whether the collection is empty.
134    pub fn is_empty(&self) -> bool {
135        self.diagnostics.is_empty()
136    }
137
138    /// Returns whether any diagnostics have a severity of [`Severity::Error`].
139    pub fn has_errors(&self) -> bool {
140        self.iter()
141            .any(|diagnostic| diagnostic.severity() == Severity::Error)
142    }
143
144    /// Sorts the diagnostics in the collection.
145    pub fn sort(&mut self) {
146        self.diagnostics.sort();
147    }
148
149    /// Iterate the diagnostics emitted so far.
150    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/// Implements an AST validator.
177///
178/// A validator operates on a set of AST visitors.
179///
180/// See the [validate](Self::validate) method to perform the validation.
181#[allow(missing_debug_implementations)]
182pub struct Validator {
183    /// The set of validation visitors.
184    visitors: Vec<Box<dyn Visitor>>,
185    /// The exceptions visitor.
186    exceptions: exceptions::Exceptions,
187}
188
189impl Validator {
190    /// Creates a validator with an empty visitors set.
191    pub fn empty() -> Self {
192        Self {
193            visitors: Vec::new(),
194            // Analysis rules are always known
195            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    /// Adds a visitor to the validator.
205    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    /// Adds multiple visitors to the validator.
210    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    /// Adds rule names to the validator's known rules set.
218    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    /// Catch any unapplied lint exceptions.
226    ///
227    /// When the [`Validator`] is created, it is made aware of all `#@ except`
228    /// comments in the document. As it runs, exceptable diagnostics are
229    /// passed through [`Diagnostics::exceptable_add()`], which
230    /// tracks whether any `#@ except` comment suppresses it and marks the
231    /// comment as used.
232    ///
233    /// Any unmarked comments, with exception to the special cases below, will
234    /// be reported as `MeaninglessLintDirective`s.
235    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        // `ExceptDirectiveValid` does a different job of checking whether a lint
246        // exception is *ever* applicable to the applied node.
247        // `MeaninglessLintDirective` should only fire if the exception
248        // comment is valid to begin with.
249        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                // Try not to clash with `ExceptDirectiveValid`
263                || invalid_directives.contains(&exception.span)
264                // If none of the visitors know the rule, it can't ever fire
265                || (!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    /// Validates the given document and returns the validation errors upon
286    /// failure.
287    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    /// Finds the nearest known rule ID to the given unknown rule ID,
315    /// or `None` if no rule ID is close enough.
316    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    /// Creates a validator with the default validation visitors.
326    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            // Global exceptions are always considered applied
399            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        // Test exact match
703        let nearest = validator.find_nearest_rule("UnusedInput");
704        pretty_assertions::assert_eq!(nearest.as_deref(), Some("UnusedInput"));
705
706        // Test close match
707        let nearest = validator.find_nearest_rule("UnusedInputt");
708        pretty_assertions::assert_eq!(nearest.as_deref(), Some("UnusedInput"));
709
710        // Test another exact match
711        let nearest = validator.find_nearest_rule("UnusedCall");
712        pretty_assertions::assert_eq!(nearest.as_deref(), Some("UnusedCall"));
713
714        // Test a typo
715        let nearest = validator.find_nearest_rule("UnusedKall");
716        pretty_assertions::assert_eq!(nearest.as_deref(), Some("UnusedCall"));
717
718        // Test a more significant typo
719        let nearest = validator.find_nearest_rule("UnnecessaryFunctionAl");
720        pretty_assertions::assert_eq!(nearest.as_deref(), Some("UnnecessaryFunctionCall"));
721
722        // Test a completely different string
723        let nearest = validator.find_nearest_rule("CompletelyDifferentRule");
724        pretty_assertions::assert_eq!(nearest.as_deref(), None);
725    }
726}