Skip to main content

wdl_analysis/
config.rs

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