wdl_lint/
lib.rs

1//! Lint rules for Workflow Description Language (WDL) documents.
2#![doc = include_str!("../RULES.md")]
3//! # Examples
4//!
5//! An example of parsing a WDL document and linting it:
6//!
7//! ```rust
8//! # let source = "version 1.1\nworkflow test {}";
9//! use wdl_lint::LintVisitor;
10//! use wdl_lint::ast::Document;
11//! use wdl_lint::ast::Validator;
12//!
13//! let (document, diagnostics) = Document::parse(source);
14//! if !diagnostics.is_empty() {
15//!     // Handle the failure to parse
16//! }
17//!
18//! let mut validator = Validator::default();
19//! validator.add_visitor(LintVisitor::default());
20//! if let Err(diagnostics) = validator.validate(&document) {
21//!     // Handle the failure to validate
22//! }
23//! ```
24
25#![warn(missing_docs)]
26#![warn(rust_2018_idioms)]
27#![warn(rust_2021_compatibility)]
28#![warn(missing_debug_implementations)]
29#![warn(clippy::missing_docs_in_private_items)]
30#![warn(rustdoc::broken_intra_doc_links)]
31
32use wdl_ast::Diagnostics;
33use wdl_ast::SyntaxKind;
34use wdl_ast::Visitor;
35
36pub(crate) mod fix;
37pub mod rules;
38mod tags;
39pub(crate) mod util;
40mod visitor;
41
42pub use tags::*;
43pub use visitor::*;
44pub use wdl_ast as ast;
45
46/// The reserved rule identifiers that are used by analysis.
47pub const RESERVED_RULE_IDS: &[&str] = &[
48    "UnusedImport",
49    "UnusedInput",
50    "UnusedDeclaration",
51    "UnusedCall",
52    "UnnecessaryFunctionCall",
53];
54
55/// A trait implemented by lint rules.
56pub trait Rule: Visitor<State = Diagnostics> {
57    /// The unique identifier for the lint rule.
58    ///
59    /// The identifier is required to be pascal case.
60    ///
61    /// This is what will show up in style guides and is the identifier by which
62    /// a lint rule is disabled.
63    fn id(&self) -> &'static str;
64
65    /// A short, single sentence description of the lint rule.
66    fn description(&self) -> &'static str;
67
68    /// Get the long-form explanation of the lint rule.
69    fn explanation(&self) -> &'static str;
70
71    /// Get the tags of the lint rule.
72    fn tags(&self) -> TagSet;
73
74    /// Gets the optional URL of the lint rule.
75    fn url(&self) -> Option<&'static str> {
76        None
77    }
78
79    /// Gets the nodes that are exceptable for this rule.
80    ///
81    /// If `None` is returned, all nodes are exceptable.
82    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]>;
83}
84
85/// Gets the default rule set.
86pub fn rules() -> Vec<Box<dyn Rule>> {
87    let rules: Vec<Box<dyn Rule>> = vec![
88        Box::<rules::DoubleQuotesRule>::default(),
89        Box::<rules::NoCurlyCommandsRule>::default(),
90        Box::<rules::SnakeCaseRule>::default(),
91        Box::<rules::MissingRuntimeRule>::default(),
92        Box::<rules::EndingNewlineRule>::default(),
93        Box::<rules::PreambleFormattingRule>::default(),
94        Box::<rules::MatchingParameterMetaRule>::default(),
95        Box::<rules::WhitespaceRule>::default(),
96        Box::<rules::CommandSectionMixedIndentationRule>::default(),
97        Box::<rules::ImportPlacementRule>::default(),
98        Box::<rules::PascalCaseRule>::default(),
99        Box::<rules::ImportWhitespaceRule>::default(),
100        Box::<rules::MissingMetasRule>::default(),
101        Box::<rules::MissingOutputRule>::default(),
102        Box::<rules::ImportSortRule>::default(),
103        Box::<rules::InputNotSortedRule>::default(),
104        Box::<rules::LineWidthRule>::default(),
105        Box::<rules::InconsistentNewlinesRule>::default(),
106        Box::<rules::CallInputSpacingRule>::default(),
107        Box::<rules::SectionOrderingRule>::default(),
108        Box::<rules::DeprecatedObjectRule>::default(),
109        Box::<rules::DescriptionMissingRule>::default(),
110        Box::<rules::DeprecatedPlaceholderOptionRule>::default(),
111        Box::<rules::RuntimeSectionKeysRule>::default(),
112        Box::<rules::TodoRule>::default(),
113        Box::<rules::NonmatchingOutputRule<'_>>::default(),
114        Box::<rules::CommentWhitespaceRule>::default(),
115        Box::<rules::TrailingCommaRule>::default(),
116        Box::<rules::BlankLinesBetweenElementsRule>::default(),
117        Box::<rules::KeyValuePairsRule>::default(),
118        Box::<rules::ExpressionSpacingRule>::default(),
119        Box::<rules::DisallowedInputNameRule>::default(),
120        Box::<rules::DisallowedOutputNameRule>::default(),
121        Box::<rules::ContainerValue>::default(),
122        Box::<rules::MissingRequirementsRule>::default(),
123        Box::<rules::UnknownRule>::default(),
124        Box::<rules::MisplacedLintDirectiveRule>::default(),
125        Box::<rules::VersionFormattingRule>::default(),
126        Box::<rules::PreambleCommentAfterVersionRule>::default(),
127        Box::<rules::MalformedLintDirectiveRule>::default(),
128        Box::<rules::RedundantInputAssignment>::default(),
129    ];
130
131    // Ensure all the rule ids are unique and pascal case
132    #[cfg(debug_assertions)]
133    {
134        use convert_case::Case;
135        use convert_case::Casing;
136        let mut set = std::collections::HashSet::new();
137        for r in rules.iter() {
138            if r.id().to_case(Case::Pascal) != r.id() {
139                panic!("lint rule id `{id}` is not pascal case", id = r.id());
140            }
141
142            if !set.insert(r.id()) {
143                panic!("duplicate rule id `{id}`", id = r.id());
144            }
145
146            if RESERVED_RULE_IDS.contains(&r.id()) {
147                panic!("rule id `{id}` is reserved", id = r.id());
148            }
149        }
150    }
151
152    rules
153}
154
155/// Gets the optional rule set.
156pub fn optional_rules() -> Vec<Box<dyn Rule>> {
157    let opt_rules: Vec<Box<dyn Rule>> = vec![Box::<rules::ShellCheckRule>::default()];
158
159    // Ensure all the rule ids are unique and pascal case
160    #[cfg(debug_assertions)]
161    {
162        use convert_case::Case;
163        use convert_case::Casing;
164
165        use crate::rules;
166        let mut set: std::collections::HashSet<&str> =
167            std::collections::HashSet::from_iter(rules().iter().map(|r| r.id()));
168        for r in opt_rules.iter() {
169            if r.id().to_case(Case::Pascal) != r.id() {
170                panic!("lint rule id `{id}` is not pascal case", id = r.id());
171            }
172
173            if !set.insert(r.id()) {
174                panic!("duplicate rule id `{id}`", id = r.id());
175            }
176
177            if RESERVED_RULE_IDS.contains(&r.id()) {
178                panic!("rule id `{id}` is reserved", id = r.id());
179            }
180        }
181    }
182
183    opt_rules
184}