Skip to main content

wdl_analysis/
validation.rs

1//! Validator for WDL documents.
2
3use 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
39/// Finds the nearest known rule ID to the given unknown rule ID,
40/// or `None` if no rule ID is close enough.
41pub 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/// Represents a collection of validation diagnostics.
62///
63/// Validation visitors receive a diagnostics collection during
64/// visitation of the AST.
65#[derive(Clone, Debug, Default)]
66pub struct Diagnostics {
67    /// Diagnostics to emit.
68    pub(crate) diagnostics: Vec<Diagnostic>,
69    /// `#@ except:` directives discovered during traversal.
70    ///
71    /// `HashMap<Rule, applied>`
72    exceptions: HashMap<ExceptRule, bool>,
73}
74
75impl Diagnostics {
76    /// Adds a diagnostic to the collection.
77    ///
78    /// NOTE: This is intended for diagnostics that cannot be suppressed.
79    /// Otherwise, [`Diagnostics::exceptable_add()`] should be used.
80    pub fn add(&mut self, diagnostic: Diagnostic) {
81        self.diagnostics.push(diagnostic);
82    }
83
84    /// Adds rule exceptions to the collection.
85    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    /// Adds a diagnostic to the collection, unless the diagnostic is for an
92    /// element that has an exception for the given rule.
93    ///
94    /// If the diagnostic does not have a rule, the diagnostic is always added.
95    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    /// Returns whether the collection is empty.
132    pub fn is_empty(&self) -> bool {
133        self.diagnostics.is_empty()
134    }
135
136    /// Sorts the diagnostics in the collection.
137    pub fn sort(&mut self) {
138        self.diagnostics.sort();
139    }
140
141    /// Iterate the diagnostics emitted so far.
142    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/// Implements an AST validator.
169///
170/// A validator operates on a set of AST visitors.
171///
172/// See the [validate](Self::validate) method to perform the validation.
173#[allow(missing_debug_implementations)]
174pub struct Validator {
175    /// The set of validation visitors.
176    visitors: Vec<Box<dyn Visitor>>,
177    /// The known rules visitor.
178    known_rules: known_rules::KnownRules,
179}
180
181impl Validator {
182    /// Creates a validator with an empty visitors set.
183    pub fn empty() -> Self {
184        Self {
185            visitors: Vec::new(),
186            // Analysis rules are always known
187            known_rules: known_rules::KnownRules::new(ALL_RULE_IDS.iter().cloned().collect()),
188        }
189    }
190
191    /// Adds a visitor to the validator.
192    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    /// Adds multiple visitors to the validator.
197    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    /// Adds rule names to the validator's known rules set.
205    pub fn extend_known_rules(&mut self, rules: impl IntoIterator<Item = String>) {
206        self.known_rules.extend(rules);
207    }
208
209    /// Catch any unapplied lint exceptions.
210    ///
211    /// When the [`Validator`] is created, it is made aware of all `#@ except`
212    /// comments in the document. As it runs, exceptable diagnostics are
213    /// passed through [`Diagnostics::exceptable_add()`], which
214    /// tracks whether any `#@ except` comment suppresses it and marks the
215    /// comment as used.
216    ///
217    /// Any unmarked comments, with exception to the special cases below, will
218    /// be reported as `MeaninglessLintDirective`s.
219    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        // `ExceptDirectiveValid` does a different job of checking whether a lint
230        // exception is *ever* applicable to the applied node.
231        // `MeaninglessLintDirective` should only fire if the exception
232        // comment is valid to begin with.
233        let invalid_directives = diagnostics
234            .iter()
235            .filter_map(|d| {
236                // Unfortunately, somewhat hacky since `ExceptDirectiveValid` comes from
237                // `wdl-lint`
238                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                // Try not to clash with `ExceptDirectiveValid`
249                || invalid_directives.contains(&exception.span)
250                // If none of the visitors know the rule, it can't ever fire
251                || (!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    /// Validates the given document and returns the validation errors upon
272    /// failure.
273    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    /// Finds the nearest known rule ID to the given unknown rule ID,
301    /// or `None` if no rule ID is close enough.
302    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    /// Creates a validator with the default validation visitors.
312    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            // Global exceptions are always considered applied
385            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        // Test exact match
689        let nearest = validator.find_nearest_rule("UnusedInput");
690        pretty_assertions::assert_eq!(nearest.as_deref(), Some("UnusedInput"));
691
692        // Test close match
693        let nearest = validator.find_nearest_rule("UnusedInputt");
694        pretty_assertions::assert_eq!(nearest.as_deref(), Some("UnusedInput"));
695
696        // Test another exact match
697        let nearest = validator.find_nearest_rule("UnusedCall");
698        pretty_assertions::assert_eq!(nearest.as_deref(), Some("UnusedCall"));
699
700        // Test a typo
701        let nearest = validator.find_nearest_rule("UnusedKall");
702        pretty_assertions::assert_eq!(nearest.as_deref(), Some("UnusedCall"));
703
704        // Test a more significant typo
705        let nearest = validator.find_nearest_rule("UnnecessaryFunctionAl");
706        pretty_assertions::assert_eq!(nearest.as_deref(), Some("UnnecessaryFunctionCall"));
707
708        // Test a completely different string
709        let nearest = validator.find_nearest_rule("CompletelyDifferentRule");
710        pretty_assertions::assert_eq!(nearest.as_deref(), None);
711    }
712}