wdl_lint/
lib.rs

1//! Lint rules for Workflow Description Language (WDL) documents.
2#![doc = include_str!("../RULES.md")]
3//! # Definitions
4#![doc = include_str!("../DEFINITIONS.md")]
5//! # Examples
6//!
7//! An example of parsing a WDL document and linting it:
8//!
9//! ```rust
10//! # let source = "version 1.1\nworkflow test {}";
11//! use wdl_lint::Linter;
12//! use wdl_lint::analysis::Validator;
13//! use wdl_lint::analysis::document::Document;
14//!
15//! let mut validator = Validator::default();
16//! validator.add_visitor(Linter::default());
17//! ```
18
19#![warn(missing_docs)]
20#![warn(rust_2018_idioms)]
21#![warn(rust_2021_compatibility)]
22#![warn(missing_debug_implementations)]
23#![warn(clippy::missing_docs_in_private_items)]
24#![warn(rustdoc::broken_intra_doc_links)]
25
26use wdl_analysis::Visitor;
27use wdl_ast::SyntaxKind;
28
29pub(crate) mod fix;
30mod linter;
31pub mod rules;
32mod tags;
33pub(crate) mod util;
34
35pub use linter::*;
36pub use tags::*;
37pub use util::find_nearest_rule;
38pub use wdl_analysis as analysis;
39pub use wdl_ast as ast;
40
41/// The definitions of WDL concepts and terminology used in the linting rules.
42pub const DEFINITIONS_TEXT: &str = include_str!("../DEFINITIONS.md");
43
44/// A trait implemented by lint rules.
45pub trait Rule: Visitor {
46    /// The unique identifier for the lint rule.
47    ///
48    /// The identifier is required to be pascal case.
49    ///
50    /// This is what will show up in style guides and is the identifier by which
51    /// a lint rule is disabled.
52    fn id(&self) -> &'static str;
53
54    /// A short, single sentence description of the lint rule.
55    fn description(&self) -> &'static str;
56
57    /// Get the long-form explanation of the lint rule.
58    fn explanation(&self) -> &'static str;
59
60    /// Get the tags of the lint rule.
61    fn tags(&self) -> TagSet;
62
63    /// Gets the optional URL of the lint rule.
64    fn url(&self) -> Option<&'static str> {
65        None
66    }
67
68    /// Gets the nodes that are exceptable for this rule.
69    ///
70    /// If `None` is returned, all nodes are exceptable.
71    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]>;
72
73    /// Gets the ID of rules that are related to this rule.
74    ///
75    /// This can be used by tools (like `sprocket explain`) to suggest other
76    /// relevant rules to the user based on potential logical connections or
77    /// common co-occurrences of issues.
78    fn related_rules(&self) -> &[&'static str];
79}
80
81/// Gets all of the lint rules.
82pub fn rules() -> Vec<Box<dyn Rule>> {
83    let rules: Vec<Box<dyn Rule>> = vec![
84        Box::<rules::DoubleQuotesRule>::default(),
85        Box::<rules::HereDocCommandsRule>::default(),
86        Box::<rules::SnakeCaseRule>::default(),
87        Box::<rules::RuntimeSectionRule>::default(),
88        Box::<rules::EndingNewlineRule>::default(),
89        Box::<rules::PreambleFormattedRule>::default(),
90        Box::<rules::ParameterMetaMatchedRule>::default(),
91        Box::<rules::WhitespaceRule>::default(),
92        Box::<rules::CommandSectionIndentationRule>::default(),
93        Box::<rules::ImportPlacementRule>::default(),
94        Box::<rules::PascalCaseRule>::default(),
95        Box::<rules::ImportWhitespaceRule>::default(),
96        Box::<rules::MetaSectionsRule>::default(),
97        Box::<rules::ImportSortedRule>::default(),
98        Box::<rules::InputSortedRule>::default(),
99        Box::<rules::LineWidthRule>::default(),
100        Box::<rules::ConsistentNewlinesRule>::default(),
101        Box::<rules::CallInputSpacingRule>::default(),
102        Box::<rules::CallInputKeywordRule>::default(),
103        Box::<rules::SectionOrderingRule>::default(),
104        Box::<rules::DeprecatedObjectRule>::default(),
105        Box::<rules::MetaDescriptionRule>::default(),
106        Box::<rules::DeprecatedPlaceholderRule>::default(),
107        Box::<rules::ExpectedRuntimeKeysRule>::default(),
108        Box::<rules::TodoCommentRule>::default(),
109        Box::<rules::MatchingOutputMetaRule<'_>>::default(),
110        Box::<rules::CommentWhitespaceRule>::default(),
111        Box::<rules::TrailingCommaRule>::default(),
112        Box::<rules::ElementSpacingRule>::default(),
113        Box::<rules::MetaKeyValueFormattingRule>::default(),
114        Box::<rules::ExpressionSpacingRule>::default(),
115        Box::<rules::InputNameRule>::default(),
116        Box::<rules::OutputNameRule>::default(),
117        Box::<rules::DeclarationNameRule>::default(),
118        Box::<rules::RedundantNone>::default(),
119        Box::<rules::ContainerUriRule>::default(),
120        Box::<rules::RequirementsSectionRule>::default(),
121        Box::<rules::KnownRulesRule>::default(),
122        Box::<rules::LintDirectiveValidRule>::default(),
123        Box::<rules::VersionStatementFormattedRule>::default(),
124        Box::<rules::PreambleCommentPlacementRule>::default(),
125        Box::<rules::LintDirectiveFormattedRule>::default(),
126        Box::<rules::ConciseInputRule>::default(),
127        Box::<rules::ShellCheckRule>::default(),
128        Box::<rules::DescriptionLengthRule>::default(),
129    ];
130
131    // Ensure all the rule IDs are unique and pascal case and that related rules are
132    // valid, exist and not self-referential.
133    #[cfg(debug_assertions)]
134    {
135        use std::collections::HashSet;
136
137        use convert_case::Case;
138        use convert_case::Casing;
139        let mut lint_set = HashSet::new();
140        let analysis_set: HashSet<&str> =
141            HashSet::from_iter(analysis::rules().iter().map(|r| r.id()));
142        for r in &rules {
143            if r.id().to_case(Case::Pascal) != r.id() {
144                panic!("lint rule id `{id}` is not pascal case", id = r.id());
145            }
146
147            if !lint_set.insert(r.id()) {
148                panic!("duplicate rule id `{id}`", id = r.id());
149            }
150
151            if analysis_set.contains(r.id()) {
152                panic!("rule id `{id}` is in use by wdl-analysis", id = r.id());
153            }
154            let self_id = &r.id();
155            for related_id in r.related_rules() {
156                if related_id == self_id {
157                    panic!(
158                        "Rule `{self_id}` refers to itself in its related rules. This is not \
159                         allowed."
160                    );
161                }
162            }
163        }
164    }
165
166    rules
167}