1use 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
25type InitCb = Box<dyn Fn() + 'static>;
27
28type ProgressCb =
30 Box<dyn Fn(ProgressKind, usize, usize) -> BoxFuture<'static, ()> + Send + Sync + 'static>;
31
32pub struct Analysis {
34 sources: Vec<Source>,
38
39 exceptions: HashSet<String>,
41
42 enabled_lint_tags: TagSet,
44
45 disabled_lint_tags: TagSet,
47
48 ignore_filename: Option<String>,
50
51 init: InitCb,
53
54 progress: ProgressCb,
56}
57
58impl Analysis {
59 pub fn add_source(mut self, source: Source) -> Self {
61 self.sources.push(source);
62 self
63 }
64
65 pub fn extend_sources(mut self, source: impl IntoIterator<Item = Source>) -> Self {
67 self.sources.extend(source);
68 self
69 }
70
71 pub fn add_exception(mut self, rule: impl Into<String>) -> Self {
73 self.exceptions.insert(rule.into());
74 self
75 }
76
77 pub fn extend_exceptions(mut self, rules: impl IntoIterator<Item = String>) -> Self {
79 self.exceptions.extend(rules);
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 fn enabled_lint_tags(mut self, tags: TagSet) -> Self {
109 self.enabled_lint_tags = tags;
110 self
111 }
112
113 pub fn disabled_lint_tags(mut self, tags: TagSet) -> Self {
115 self.disabled_lint_tags = tags;
116 self
117 }
118
119 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
196fn 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
222fn 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
232fn 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
246fn 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}