wdl_analysis/config.rs
1//! Configuration for this crate.
2
3use std::sync::Arc;
4
5use tracing::warn;
6use wdl_ast::Severity;
7use wdl_ast::SupportedVersion;
8use wdl_ast::SyntaxNode;
9
10use crate::Exceptable as _;
11use crate::MisleadingDeclarationOrderRule;
12use crate::Rule;
13use crate::UnnecessaryFunctionCall;
14use crate::UnusedCallRule;
15use crate::UnusedDeclarationRule;
16use crate::UnusedImportRule;
17use crate::UnusedInputRule;
18use crate::UsingFallbackVersion;
19use crate::rules;
20
21/// Configuration for `wdl-analysis`.
22///
23/// This type is a wrapper around an `Arc`, and so can be cheaply cloned and
24/// sent between threads.
25#[derive(Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
26pub struct Config {
27 /// The actual fields, `Arc`ed up for easy cloning.
28 #[serde(flatten)]
29 inner: Arc<ConfigInner>,
30}
31
32// Custom `Debug` impl for the `Config` wrapper type that simplifies away the
33// arc and the private inner struct
34impl std::fmt::Debug for Config {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 f.debug_struct("Config")
37 .field("diagnostics", &self.inner.diagnostics)
38 .field("fallback_version", &self.inner.fallback_version)
39 .finish()
40 }
41}
42
43impl Default for Config {
44 fn default() -> Self {
45 Self {
46 inner: Arc::new(ConfigInner {
47 diagnostics: Default::default(),
48 fallback_version: None,
49 ignore_filename: None,
50 all_rules: Default::default(),
51 feature_flags: FeatureFlags::default(),
52 }),
53 }
54 }
55}
56
57impl Config {
58 /// Get this configuration's [`DiagnosticsConfig`].
59 pub fn diagnostics_config(&self) -> &DiagnosticsConfig {
60 &self.inner.diagnostics
61 }
62
63 /// Get this configuration's fallback version; see
64 /// [`Config::with_fallback_version()`].
65 pub fn fallback_version(&self) -> Option<SupportedVersion> {
66 self.inner.fallback_version
67 }
68
69 /// Get this configuration's ignore filename.
70 pub fn ignore_filename(&self) -> Option<&str> {
71 self.inner.ignore_filename.as_deref()
72 }
73
74 /// Gets the list of all known rule identifiers.
75 pub fn all_rules(&self) -> &[String] {
76 &self.inner.all_rules
77 }
78
79 /// Gets the feature flags.
80 pub fn feature_flags(&self) -> &FeatureFlags {
81 &self.inner.feature_flags
82 }
83
84 /// Return a new configuration with the previous [`DiagnosticsConfig`]
85 /// replaced by the argument.
86 pub fn with_diagnostics_config(&self, diagnostics: DiagnosticsConfig) -> Self {
87 let mut inner = (*self.inner).clone();
88 inner.diagnostics = diagnostics;
89 Self {
90 inner: Arc::new(inner),
91 }
92 }
93
94 /// Return a new configuration with the previous version fallback option
95 /// replaced by the argument.
96 ///
97 /// This option controls what happens when analyzing a WDL document with a
98 /// syntactically valid but unrecognized version in the version
99 /// statement. The default value is `None`, with no fallback behavior.
100 ///
101 /// Configured with `Some(fallback_version)`, analysis will proceed as
102 /// normal if the version statement contains a recognized version. If
103 /// the version is unrecognized, analysis will continue as if the
104 /// version statement contained `fallback_version`, though the concrete
105 /// syntax of the version statement will remain unchanged.
106 ///
107 /// <div class="warning">
108 ///
109 /// # Warnings
110 ///
111 /// This option is intended only for situations where unexpected behavior
112 /// due to unsupported syntax is acceptable, such as when providing
113 /// best-effort editor hints via `wdl-lsp`. The semantics of executing a
114 /// WDL workflow with an unrecognized version is undefined and not
115 /// recommended.
116 ///
117 /// Once this option has been configured for an `Analyzer`, it should not be
118 /// changed. A document that was initially parsed and analyzed with one
119 /// fallback option may cause errors if subsequent operations are
120 /// performed with a different fallback option.
121 ///
122 /// </div>
123 pub fn with_fallback_version(&self, fallback_version: Option<SupportedVersion>) -> Self {
124 let mut inner = (*self.inner).clone();
125 inner.fallback_version = fallback_version;
126 Self {
127 inner: Arc::new(inner),
128 }
129 }
130
131 /// Return a new configuration with the previous ignore filename replaced by
132 /// the argument.
133 ///
134 /// Specifying `None` for `filename` disables ignore behavior. This is also
135 /// the default.
136 ///
137 /// `Some(filename)` will use `filename` as the ignorefile basename to
138 /// search for. Child directories _and_ parent directories are searched
139 /// for a file with the same basename as `filename` and if a match is
140 /// found it will attempt to be parsed as an ignorefile with a syntax
141 /// similar to `.gitignore` files.
142 pub fn with_ignore_filename(&self, filename: Option<String>) -> Self {
143 let mut inner = (*self.inner).clone();
144 inner.ignore_filename = filename;
145 Self {
146 inner: Arc::new(inner),
147 }
148 }
149
150 /// Returns a new configuration with the list of all known rule identifiers
151 /// replaced by the argument.
152 ///
153 /// This is used internally to populate the `#@ except:` snippet.
154 pub fn with_all_rules(&self, rules: Vec<String>) -> Self {
155 let mut inner = (*self.inner).clone();
156 inner.all_rules = rules;
157 Self {
158 inner: Arc::new(inner),
159 }
160 }
161
162 /// Return a new configuration with the previous [`FeatureFlags`]
163 /// replaced by the argument.
164 pub fn with_feature_flags(&self, feature_flags: FeatureFlags) -> Self {
165 let mut inner = (*self.inner).clone();
166 inner.feature_flags = feature_flags;
167 Self {
168 inner: Arc::new(inner),
169 }
170 }
171}
172
173/// The actual configuration fields inside the [`Config`] wrapper.
174#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
175struct ConfigInner {
176 /// See [`DiagnosticsConfig`].
177 #[serde(default)]
178 diagnostics: DiagnosticsConfig,
179 /// See [`Config::with_fallback_version()`]
180 #[serde(default)]
181 fallback_version: Option<SupportedVersion>,
182 /// See [`Config::with_ignore_filename()`]
183 ignore_filename: Option<String>,
184 /// A list of all known rule identifiers.
185 #[serde(default)]
186 all_rules: Vec<String>,
187 /// The set of feature flags that can be enabled or disabled.
188 #[serde(default)]
189 feature_flags: FeatureFlags,
190}
191
192/// A set of feature flags that can be enabled.
193#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
194pub struct FeatureFlags {
195 /// Formerly enabled experimental WDL 1.3 features.
196 ///
197 /// This flag is now a no-op as WDL 1.3 is fully supported. Setting this to
198 /// `false` will emit a warning.
199 #[serde(default = "default_wdl_1_3")]
200 wdl_1_3: bool,
201 /// Enables experimental WDL 1.4 features.
202 ///
203 /// Defaults to `false`. While `false`, `wdl-analysis` reports an error for
204 /// any document declaring `version 1.4`.
205 #[serde(default = "default_wdl_1_4")]
206 wdl_1_4: bool,
207}
208
209/// Returns the default value for the `wdl_1_3` feature flag.
210fn default_wdl_1_3() -> bool {
211 true
212}
213
214/// Returns the default value for the `wdl_1_4` feature flag.
215fn default_wdl_1_4() -> bool {
216 false
217}
218
219impl Default for FeatureFlags {
220 fn default() -> Self {
221 Self {
222 wdl_1_3: true,
223 wdl_1_4: false,
224 }
225 }
226}
227
228impl FeatureFlags {
229 /// Returns whether WDL 1.3 is enabled.
230 ///
231 /// WDL 1.3 is now fully supported and defaults to `true`. Setting this to
232 /// `false` will emit a deprecation warning.
233 pub fn wdl_1_3(&self) -> bool {
234 self.wdl_1_3
235 }
236
237 /// Returns a new `FeatureFlags` with WDL 1.3 features enabled.
238 #[deprecated(note = "WDL 1.3 is now enabled by default; this method is a no-op")]
239 pub fn with_wdl_1_3(self) -> Self {
240 self
241 }
242
243 /// Returns whether WDL 1.4 is enabled.
244 pub fn wdl_1_4(&self) -> bool {
245 self.wdl_1_4
246 }
247
248 /// Returns a new `FeatureFlags` with WDL 1.4 features enabled.
249 pub fn with_wdl_1_4(mut self) -> Self {
250 self.wdl_1_4 = true;
251 self
252 }
253}
254
255/// Configuration for analysis diagnostics.
256///
257/// Only the analysis diagnostics that aren't inherently treated as errors are
258/// represented here.
259///
260/// These diagnostics default to a warning severity.
261#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
262pub struct DiagnosticsConfig {
263 /// The severity for the unused import diagnostic.
264 ///
265 /// A value of `None` disables the diagnostic.
266 pub unused_import: Option<Severity>,
267 /// The severity for the unused input diagnostic.
268 ///
269 /// A value of `None` disables the diagnostic.
270 pub unused_input: Option<Severity>,
271 /// The severity for the unused declaration diagnostic.
272 ///
273 /// A value of `None` disables the diagnostic.
274 pub unused_declaration: Option<Severity>,
275 /// The severity for the unused call diagnostic.
276 ///
277 /// A value of `None` disables the diagnostic.
278 pub unused_call: Option<Severity>,
279 /// The severity for the unnecessary function call diagnostic.
280 ///
281 /// A value of `None` disables the diagnostic.
282 pub unnecessary_function_call: Option<Severity>,
283 /// The severity for the using fallback version diagnostic.
284 ///
285 /// A value of `None` disables the diagnostic. If there is no version
286 /// configured with [`Config::with_fallback_version()`], this diagnostic
287 /// will not be emitted.
288 pub using_fallback_version: Option<Severity>,
289 /// The severity for the misleading declaration order diagnostic.
290 ///
291 /// A value of `None` disables the diagnostic.
292 pub misleading_declaration_order: Option<Severity>,
293}
294
295impl Default for DiagnosticsConfig {
296 fn default() -> Self {
297 Self::new(rules())
298 }
299}
300
301impl DiagnosticsConfig {
302 /// Creates a new diagnostics configuration from a rule set.
303 pub fn new<T: AsRef<dyn Rule>>(rules: impl IntoIterator<Item = T>) -> Self {
304 let mut unused_import = None;
305 let mut unused_input = None;
306 let mut unused_declaration = None;
307 let mut unused_call = None;
308 let mut unnecessary_function_call = None;
309 let mut using_fallback_version = None;
310 let mut misleading_declaration_order = None;
311
312 for rule in rules {
313 let rule = rule.as_ref();
314 match rule.id() {
315 UnusedImportRule::ID => unused_import = Some(rule.severity()),
316 UnusedInputRule::ID => unused_input = Some(rule.severity()),
317 UnusedDeclarationRule::ID => unused_declaration = Some(rule.severity()),
318 UnusedCallRule::ID => unused_call = Some(rule.severity()),
319 UnnecessaryFunctionCall::ID => unnecessary_function_call = Some(rule.severity()),
320 UsingFallbackVersion::ID => using_fallback_version = Some(rule.severity()),
321 MisleadingDeclarationOrderRule::ID => {
322 misleading_declaration_order = Some(rule.severity())
323 }
324 unrecognized => {
325 warn!(unrecognized, "unrecognized rule");
326 if cfg!(test) {
327 panic!("unrecognized rule: {unrecognized}");
328 }
329 }
330 }
331 }
332
333 Self {
334 unused_import,
335 unused_input,
336 unused_declaration,
337 unused_call,
338 unnecessary_function_call,
339 using_fallback_version,
340 misleading_declaration_order,
341 }
342 }
343
344 /// Returns a modified set of diagnostics that accounts for any `#@ except`
345 /// comments that precede the given syntax node.
346 pub fn excepted_for_node(mut self, node: &SyntaxNode) -> Self {
347 let exceptions = node.rule_exceptions();
348
349 if exceptions.contains(UnusedImportRule::ID) {
350 self.unused_import = None;
351 }
352
353 if exceptions.contains(UnusedInputRule::ID) {
354 self.unused_input = None;
355 }
356
357 if exceptions.contains(UnusedDeclarationRule::ID) {
358 self.unused_declaration = None;
359 }
360
361 if exceptions.contains(UnusedCallRule::ID) {
362 self.unused_call = None;
363 }
364
365 if exceptions.contains(UnnecessaryFunctionCall::ID) {
366 self.unnecessary_function_call = None;
367 }
368
369 if exceptions.contains(UsingFallbackVersion::ID) {
370 self.using_fallback_version = None;
371 }
372
373 self
374 }
375
376 /// Excepts all of the diagnostics.
377 pub fn except_all() -> Self {
378 Self {
379 unused_import: None,
380 unused_input: None,
381 unused_declaration: None,
382 unused_call: None,
383 unnecessary_function_call: None,
384 using_fallback_version: None,
385 misleading_declaration_order: None,
386 }
387 }
388}