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::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}