1use 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
22type InitCb = Box<dyn Fn() + 'static>;
24
25type ProgressCb =
27 Box<dyn Fn(ProgressKind, usize, usize) -> BoxFuture<'static, ()> + Send + Sync + 'static>;
28
29pub struct Analysis {
31 sources: Vec<Source>,
35
36 exceptions: HashSet<String>,
38
39 lint: bool,
41
42 ignore_filename: Option<String>,
44
45 init: InitCb,
47
48 progress: ProgressCb,
50}
51
52impl Analysis {
53 pub fn add_source(mut self, source: Source) -> Self {
55 self.sources.push(source);
56 self
57 }
58
59 pub fn extend_sources(mut self, source: impl IntoIterator<Item = Source>) -> Self {
61 self.sources.extend(source);
62 self
63 }
64
65 pub fn add_exception(mut self, rule: impl Into<String>) -> Self {
67 self.exceptions.insert(rule.into());
68 self
69 }
70
71 pub fn extend_exceptions(mut self, rules: impl IntoIterator<Item = String>) -> Self {
73 self.exceptions.extend(rules);
74 self
75 }
76
77 pub fn lint(mut self, value: bool) -> Self {
79 self.lint = value;
80 self
81 }
82
83 pub fn ignore_filename(mut self, filename: Option<String>) -> Self {
85 self.ignore_filename = filename;
86 self
87 }
88
89 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 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 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
161fn 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
187fn 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
197fn 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}