Skip to main content

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 std::collections::HashSet;
27use std::sync::LazyLock;
28
29use wdl_analysis::Visitor;
30use wdl_ast::SyntaxKind;
31
32mod config;
33pub(crate) mod fix;
34mod linter;
35pub mod rules;
36mod tags;
37pub(crate) mod util;
38
39pub use config::Config;
40pub use linter::*;
41pub use tags::*;
42pub use util::find_nearest_rule;
43pub use wdl_analysis as analysis;
44pub use wdl_ast as ast;
45
46/// The definitions of WDL concepts and terminology used in the linting rules.
47pub const DEFINITIONS_TEXT: &str = include_str!("../DEFINITIONS.md");
48
49/// All rule IDs sorted alphabetically.
50pub static ALL_RULE_IDS: LazyLock<Vec<String>> = LazyLock::new(|| {
51    let mut ids: Vec<String> = rules(&Config::default())
52        .iter()
53        .map(|r| r.id().to_string())
54        .collect();
55    ids.sort();
56    ids
57});
58
59/// All tag names sorted alphabetically.
60pub static ALL_TAG_NAMES: LazyLock<Vec<String>> = LazyLock::new(|| {
61    let mut tags: HashSet<Tag> = HashSet::new();
62    for rule in rules(&Config::default()) {
63        for tag in rule.tags().iter() {
64            tags.insert(tag);
65        }
66    }
67    let mut tag_names: Vec<String> = tags.into_iter().map(|t| t.to_string()).collect();
68    tag_names.sort();
69    tag_names
70});
71
72/// A trait implemented by lint rules.
73pub trait Rule: Visitor {
74    /// The unique identifier for the lint rule.
75    ///
76    /// The identifier is required to be pascal case.
77    ///
78    /// This is what will show up in style guides and is the identifier by which
79    /// a lint rule is disabled.
80    fn id(&self) -> &'static str;
81
82    /// A short, single sentence description of the lint rule.
83    fn description(&self) -> &'static str;
84
85    /// Get the long-form explanation of the lint rule.
86    fn explanation(&self) -> &'static str;
87
88    /// Get the tags of the lint rule.
89    fn tags(&self) -> TagSet;
90
91    /// Gets the optional URL of the lint rule.
92    fn url(&self) -> Option<&'static str> {
93        None
94    }
95
96    /// Gets the nodes that are exceptable for this rule.
97    ///
98    /// If `None` is returned, all nodes are exceptable.
99    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]>;
100
101    /// Gets the ID of rules that are related to this rule.
102    ///
103    /// This can be used by tools (like `sprocket explain`) to suggest other
104    /// relevant rules to the user based on potential logical connections or
105    /// common co-occurrences of issues.
106    fn related_rules(&self) -> &[&'static str];
107}
108
109/// Gets all of the lint rules.
110pub fn rules(config: &Config) -> Vec<Box<dyn Rule>> {
111    let rules: Vec<Box<dyn Rule>> = vec![
112        Box::<rules::DoubleQuotesRule>::default(),
113        Box::<rules::HereDocCommandsRule>::default(),
114        Box::new(rules::SnakeCaseRule::new(config)),
115        Box::<rules::RuntimeSectionRule>::default(),
116        Box::<rules::ParameterMetaMatchedRule>::default(),
117        Box::<rules::CommandSectionIndentationRule>::default(),
118        Box::<rules::ImportPlacementRule>::default(),
119        Box::<rules::PascalCaseRule>::default(),
120        Box::<rules::MetaSectionsRule>::default(),
121        Box::<rules::InputSortedRule>::default(),
122        Box::<rules::ConsistentNewlinesRule>::default(),
123        Box::<rules::CallInputKeywordRule>::default(),
124        Box::<rules::SectionOrderingRule>::default(),
125        Box::<rules::DeprecatedObjectRule>::default(),
126        Box::<rules::MetaDescriptionRule>::default(),
127        Box::<rules::DeprecatedPlaceholderRule>::default(),
128        Box::new(rules::ExpectedRuntimeKeysRule::new(config)),
129        Box::<rules::DocMetaStringsRule>::default(),
130        Box::<rules::TodoCommentRule>::default(),
131        Box::<rules::MatchingOutputMetaRule<'_>>::default(),
132        Box::<rules::InputNameRule>::default(),
133        Box::<rules::OutputNameRule>::default(),
134        Box::new(rules::DeclarationNameRule::new(config)),
135        Box::<rules::RedundantNone>::default(),
136        Box::<rules::ContainerUriRule>::default(),
137        Box::<rules::RequirementsSectionRule>::default(),
138        Box::<rules::KnownRulesRule>::default(),
139        Box::<rules::ExceptDirectiveValidRule>::default(),
140        Box::<rules::ConciseInputRule>::default(),
141        Box::<rules::ShellCheckRule>::default(),
142        Box::<rules::DescriptionLengthRule>::default(),
143        Box::<rules::DocCommentTabsRule>::default(),
144        Box::<rules::UnusedDocCommentsRule>::default(),
145    ];
146
147    // Ensure all the rule IDs are unique and pascal case and that related rules are
148    // valid, exist and not self-referential.
149    #[cfg(debug_assertions)]
150    {
151        use std::collections::HashSet;
152
153        use convert_case::Case;
154        use convert_case::Casing;
155        let mut lint_set = HashSet::new();
156        let analysis_set: HashSet<&str> =
157            HashSet::from_iter(analysis::rules().iter().map(|r| r.id()));
158        for r in &rules {
159            if r.id().to_case(Case::Pascal) != r.id() {
160                panic!("lint rule id `{id}` is not pascal case", id = r.id());
161            }
162
163            if !lint_set.insert(r.id()) {
164                panic!("duplicate rule id `{id}`", id = r.id());
165            }
166
167            if analysis_set.contains(r.id()) {
168                panic!("rule id `{id}` is in use by wdl-analysis", id = r.id());
169            }
170            let self_id = &r.id();
171            for related_id in r.related_rules() {
172                if related_id == self_id {
173                    panic!(
174                        "Rule `{self_id}` refers to itself in its related rules. This is not \
175                         allowed."
176                    );
177                }
178            }
179        }
180    }
181
182    rules
183}