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