Skip to main content

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