wdl_cli/
analysis.rs

1//! Facilities for performing a typical analysis using the `wdl-*` crates.
2
3use std::collections::HashSet;
4use std::sync::Arc;
5
6use anyhow::Error;
7use futures::future::BoxFuture;
8use nonempty::NonEmpty;
9use tracing::info;
10use tracing::warn;
11use wdl_analysis::Analyzer;
12use wdl_analysis::DiagnosticsConfig;
13use wdl_analysis::ProgressKind;
14use wdl_analysis::Validator;
15use wdl_lint::Linter;
16
17mod results;
18mod source;
19
20pub use results::AnalysisResults;
21pub use source::Source;
22use wdl_lint::Rule;
23use wdl_lint::TagSet;
24
25/// The type of the initialization callback.
26type InitCb = Box<dyn Fn() + 'static>;
27
28/// The type of the progress callback.
29type ProgressCb =
30    Box<dyn Fn(ProgressKind, usize, usize) -> BoxFuture<'static, ()> + Send + Sync + 'static>;
31
32/// An analysis.
33pub struct Analysis {
34    /// The set of root nodes to analyze.
35    ///
36    /// Can be files, directories, or URLs.
37    sources: Vec<Source>,
38
39    /// A list of rules to except.
40    exceptions: HashSet<String>,
41
42    /// Which lint rules to enable, as specified via a [`TagSet`].
43    enabled_lint_tags: TagSet,
44
45    /// Which lint rules to disable, as specified via a [`TagSet`].
46    disabled_lint_tags: TagSet,
47
48    /// Basename for any ignorefiles which should be respected.
49    ignore_filename: Option<String>,
50
51    /// The initialization callback.
52    init: InitCb,
53
54    /// The progress callback.
55    progress: ProgressCb,
56}
57
58impl Analysis {
59    /// Adds a source to the analysis.
60    pub fn add_source(mut self, source: Source) -> Self {
61        self.sources.push(source);
62        self
63    }
64
65    /// Adds multiple sources to the analysis.
66    pub fn extend_sources(mut self, source: impl IntoIterator<Item = Source>) -> Self {
67        self.sources.extend(source);
68        self
69    }
70
71    /// Adds a rule to the excepted rules list.
72    pub fn add_exception(mut self, rule: impl Into<String>) -> Self {
73        self.exceptions.insert(rule.into());
74        self
75    }
76
77    /// Adds multiple rules to the excepted rules list.
78    pub fn extend_exceptions(mut self, rules: impl IntoIterator<Item = String>) -> Self {
79        self.exceptions.extend(rules);
80        self
81    }
82
83    /// Sets the ignorefile basename.
84    pub fn ignore_filename(mut self, filename: Option<String>) -> Self {
85        self.ignore_filename = filename;
86        self
87    }
88
89    /// Sets the initialization callback.
90    pub fn init<F>(mut self, init: F) -> Self
91    where
92        F: Fn() + 'static,
93    {
94        self.init = Box::new(init);
95        self
96    }
97
98    /// Sets the progress callback.
99    pub fn progress<F>(mut self, progress: F) -> Self
100    where
101        F: Fn(ProgressKind, usize, usize) -> BoxFuture<'static, ()> + Send + Sync + 'static,
102    {
103        self.progress = Box::new(progress);
104        self
105    }
106
107    /// Sets the enabled lint tags.
108    pub fn enabled_lint_tags(mut self, tags: TagSet) -> Self {
109        self.enabled_lint_tags = tags;
110        self
111    }
112
113    /// Sets the disabled lint tags.
114    pub fn disabled_lint_tags(mut self, tags: TagSet) -> Self {
115        self.disabled_lint_tags = tags;
116        self
117    }
118
119    /// Runs the analysis and returns all results (if any exist).
120    pub async fn run(self) -> std::result::Result<AnalysisResults, NonEmpty<Arc<Error>>> {
121        warn_unknown_rules(&self.exceptions);
122        if self.enabled_lint_tags.count() > 0 && tracing::enabled!(tracing::Level::INFO) {
123            let mut enabled_rules = vec![];
124            let mut disabled_rules = vec![];
125            for rule in wdl_lint::rules() {
126                if is_rule_enabled(
127                    &self.enabled_lint_tags,
128                    &self.disabled_lint_tags,
129                    &self.exceptions,
130                    rule.as_ref(),
131                ) {
132                    enabled_rules.push(rule.id());
133                } else {
134                    disabled_rules.push(rule.id());
135                }
136            }
137            info!("enabled lint rules: {:?}", enabled_rules);
138            info!("disabled lint rules: {:?}", disabled_rules);
139        }
140        let config = wdl_analysis::Config::default()
141            .with_diagnostics_config(get_diagnostics_config(&self.exceptions))
142            .with_ignore_filename(self.ignore_filename);
143
144        (self.init)();
145
146        let validator = Box::new(move || {
147            let mut validator = Validator::default();
148
149            if self.enabled_lint_tags.count() > 0 {
150                let visitor = get_lint_visitor(
151                    &self.enabled_lint_tags,
152                    &self.disabled_lint_tags,
153                    &self.exceptions,
154                );
155                validator.add_visitor(visitor);
156            }
157
158            validator
159        });
160
161        let mut analyzer = Analyzer::new_with_validator(
162            config,
163            move |_, kind, count, total| (self.progress)(kind, count, total),
164            validator,
165        );
166
167        for source in self.sources {
168            if let Err(error) = source.register(&mut analyzer).await {
169                return Err(NonEmpty::new(Arc::new(error)));
170            }
171        }
172
173        let results = analyzer
174            .analyze(())
175            .await
176            .map_err(|error| NonEmpty::new(Arc::new(error)))?;
177
178        AnalysisResults::try_new(results)
179    }
180}
181
182impl Default for Analysis {
183    fn default() -> Self {
184        Self {
185            sources: Default::default(),
186            exceptions: Default::default(),
187            enabled_lint_tags: TagSet::new(&[]),
188            disabled_lint_tags: TagSet::new(&[]),
189            ignore_filename: None,
190            init: Box::new(|| {}),
191            progress: Box::new(|_, _, _| Box::pin(async {})),
192        }
193    }
194}
195
196/// Warns about any unknown rules.
197fn warn_unknown_rules(exceptions: &HashSet<String>) {
198    let mut names = wdl_analysis::rules()
199        .iter()
200        .map(|rule| rule.id().to_owned())
201        .collect::<Vec<_>>();
202
203    names.extend(wdl_lint::rules().iter().map(|rule| rule.id().to_owned()));
204
205    let mut unknown = exceptions
206        .iter()
207        .filter(|rule| !names.iter().any(|name| name.eq_ignore_ascii_case(rule)))
208        .map(|rule| format!("`{rule}`"))
209        .collect::<Vec<_>>();
210
211    if !unknown.is_empty() {
212        unknown.sort();
213
214        warn!(
215            "ignoring unknown excepted rule{s}: {rules}",
216            s = if unknown.len() == 1 { "" } else { "s" },
217            rules = unknown.join(", ")
218        );
219    }
220}
221
222/// Gets the rules as a diagnositics configuration with the excepted rules
223/// removed.
224fn get_diagnostics_config(exceptions: &HashSet<String>) -> DiagnosticsConfig {
225    DiagnosticsConfig::new(wdl_analysis::rules().into_iter().filter(|rule| {
226        !exceptions
227            .iter()
228            .any(|exception| exception.eq_ignore_ascii_case(rule.id()))
229    }))
230}
231
232/// Determines if a rule should be enabled.
233fn is_rule_enabled(
234    enabled_lint_tags: &TagSet,
235    disabled_lint_tags: &TagSet,
236    exceptions: &HashSet<String>,
237    rule: &dyn Rule,
238) -> bool {
239    enabled_lint_tags.intersect(rule.tags()).count() > 0
240        && disabled_lint_tags.intersect(rule.tags()).count() == 0
241        && !exceptions
242            .iter()
243            .any(|exception| exception.eq_ignore_ascii_case(rule.id()))
244}
245
246/// Gets a lint visitor with the rules depending on provided options.
247///
248/// `enabled_lint_tags` controls which rules are considered for being added to
249/// the visitor. `disabled_lint_tags` and `exceptions` act as filters on the set
250/// considerd by `enabled_lint_tags`.
251fn get_lint_visitor(
252    enabled_lint_tags: &TagSet,
253    disabled_lint_tags: &TagSet,
254    exceptions: &HashSet<String>,
255) -> Linter {
256    Linter::new(wdl_lint::rules().into_iter().filter(|rule| {
257        is_rule_enabled(
258            enabled_lint_tags,
259            disabled_lint_tags,
260            exceptions,
261            rule.as_ref(),
262        )
263    }))
264}