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::warn;
10use wdl_analysis::Analyzer;
11use wdl_analysis::DiagnosticsConfig;
12use wdl_analysis::ProgressKind;
13use wdl_analysis::Validator;
14use wdl_lint::Linter;
15
16mod results;
17mod source;
18
19pub use results::AnalysisResults;
20pub use source::Source;
21
22/// The type of the initialization callback.
23type InitCb = Box<dyn Fn() + 'static>;
24
25/// The type of the progress callback.
26type ProgressCb =
27    Box<dyn Fn(ProgressKind, usize, usize) -> BoxFuture<'static, ()> + Send + Sync + 'static>;
28
29/// An analysis.
30pub struct Analysis {
31    /// The set of root nodes to analyze.
32    ///
33    /// Can be files, directories, or URLs.
34    sources: Vec<Source>,
35
36    /// A list of rules to except.
37    exceptions: HashSet<String>,
38
39    /// Whether or not to enable linting.
40    lint: bool,
41
42    /// Basename for any ignorefiles which should be respected.
43    ignore_filename: Option<String>,
44
45    /// The initialization callback.
46    init: InitCb,
47
48    /// The progress callback.
49    progress: ProgressCb,
50}
51
52impl Analysis {
53    /// Adds a source to the analysis.
54    pub fn add_source(mut self, source: Source) -> Self {
55        self.sources.push(source);
56        self
57    }
58
59    /// Adds multiple sources to the analysis.
60    pub fn extend_sources(mut self, source: impl IntoIterator<Item = Source>) -> Self {
61        self.sources.extend(source);
62        self
63    }
64
65    /// Adds a rule to the excepted rules list.
66    pub fn add_exception(mut self, rule: impl Into<String>) -> Self {
67        self.exceptions.insert(rule.into());
68        self
69    }
70
71    /// Adds multiple rules to the excepted rules list.
72    pub fn extend_exceptions(mut self, rules: impl IntoIterator<Item = String>) -> Self {
73        self.exceptions.extend(rules);
74        self
75    }
76
77    /// Sets whether linting is enabled.
78    pub fn lint(mut self, value: bool) -> Self {
79        self.lint = value;
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    /// Runs the analysis and returns all results (if any exist).
108    pub async fn run(self) -> std::result::Result<AnalysisResults, NonEmpty<Arc<Error>>> {
109        warn_unknown_rules(&self.exceptions);
110        let config = wdl_analysis::Config::default()
111            .with_diagnostics_config(get_diagnostics_config(&self.exceptions))
112            .with_ignore_filename(self.ignore_filename);
113
114        (self.init)();
115
116        let validator = Box::new(move || {
117            let mut validator = Validator::default();
118
119            if self.lint {
120                let visitor = get_lint_visitor(&self.exceptions);
121                validator.add_visitor(visitor);
122            }
123
124            validator
125        });
126
127        let mut analyzer = Analyzer::new_with_validator(
128            config,
129            move |_, kind, count, total| (self.progress)(kind, count, total),
130            validator,
131        );
132
133        for source in self.sources {
134            if let Err(error) = source.register(&mut analyzer).await {
135                return Err(NonEmpty::new(Arc::new(error)));
136            }
137        }
138
139        let results = analyzer
140            .analyze(())
141            .await
142            .map_err(|error| NonEmpty::new(Arc::new(error)))?;
143
144        AnalysisResults::try_new(results)
145    }
146}
147
148impl Default for Analysis {
149    fn default() -> Self {
150        Self {
151            sources: Default::default(),
152            exceptions: Default::default(),
153            lint: Default::default(),
154            ignore_filename: None,
155            init: Box::new(|| {}),
156            progress: Box::new(|_, _, _| Box::pin(async {})),
157        }
158    }
159}
160
161/// Warns about any unknown rules.
162fn warn_unknown_rules(exceptions: &HashSet<String>) {
163    let mut names = wdl_analysis::rules()
164        .iter()
165        .map(|rule| rule.id().to_owned())
166        .collect::<Vec<_>>();
167
168    names.extend(wdl_lint::rules().iter().map(|rule| rule.id().to_owned()));
169
170    let mut unknown = exceptions
171        .iter()
172        .filter(|rule| !names.iter().any(|name| name.eq_ignore_ascii_case(rule)))
173        .map(|rule| format!("`{rule}`"))
174        .collect::<Vec<_>>();
175
176    if !unknown.is_empty() {
177        unknown.sort();
178
179        warn!(
180            "ignoring unknown excepted rule{s}: {rules}",
181            s = if unknown.len() == 1 { "" } else { "s" },
182            rules = unknown.join(", ")
183        );
184    }
185}
186
187/// Gets the rules as a diagnositics configuration with the excepted rules
188/// removed.
189fn get_diagnostics_config(exceptions: &HashSet<String>) -> DiagnosticsConfig {
190    DiagnosticsConfig::new(wdl_analysis::rules().into_iter().filter(|rule| {
191        !exceptions
192            .iter()
193            .any(|exception| exception.eq_ignore_ascii_case(rule.id()))
194    }))
195}
196
197/// Gets a lint visitor with the excepted rules removed.
198fn get_lint_visitor(exceptions: &HashSet<String>) -> Linter {
199    Linter::new(wdl_lint::rules().into_iter().filter(|rule| {
200        !exceptions
201            .iter()
202            .any(|exception| exception.eq_ignore_ascii_case(rule.id()))
203    }))
204}