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::Rule;
11use crate::SyntaxNodeExt as _;
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}
201
202/// Returns the default value for the `wdl_1_3` feature flag.
203fn default_wdl_1_3() -> bool {
204    true
205}
206
207impl Default for FeatureFlags {
208    fn default() -> Self {
209        Self { wdl_1_3: true }
210    }
211}
212
213impl FeatureFlags {
214    /// Returns whether WDL 1.3 is enabled.
215    ///
216    /// WDL 1.3 is now fully supported and defaults to `true`. Setting this to
217    /// `false` will emit a deprecation warning.
218    pub fn wdl_1_3(&self) -> bool {
219        self.wdl_1_3
220    }
221
222    /// Returns a new `FeatureFlags` with WDL 1.3 features enabled.
223    #[deprecated(note = "WDL 1.3 is now enabled by default; this method is a no-op")]
224    pub fn with_wdl_1_3(self) -> Self {
225        self
226    }
227}
228
229/// Configuration for analysis diagnostics.
230///
231/// Only the analysis diagnostics that aren't inherently treated as errors are
232/// represented here.
233///
234/// These diagnostics default to a warning severity.
235#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
236pub struct DiagnosticsConfig {
237    /// The severity for the unused import diagnostic.
238    ///
239    /// A value of `None` disables the diagnostic.
240    pub unused_import: Option<Severity>,
241    /// The severity for the unused input diagnostic.
242    ///
243    /// A value of `None` disables the diagnostic.
244    pub unused_input: Option<Severity>,
245    /// The severity for the unused declaration diagnostic.
246    ///
247    /// A value of `None` disables the diagnostic.
248    pub unused_declaration: Option<Severity>,
249    /// The severity for the unused call diagnostic.
250    ///
251    /// A value of `None` disables the diagnostic.
252    pub unused_call: Option<Severity>,
253    /// The severity for the unnecessary function call diagnostic.
254    ///
255    /// A value of `None` disables the diagnostic.
256    pub unnecessary_function_call: Option<Severity>,
257    /// The severity for the using fallback version diagnostic.
258    ///
259    /// A value of `None` disables the diagnostic. If there is no version
260    /// configured with [`Config::with_fallback_version()`], this diagnostic
261    /// will not be emitted.
262    pub using_fallback_version: Option<Severity>,
263}
264
265impl Default for DiagnosticsConfig {
266    fn default() -> Self {
267        Self::new(rules())
268    }
269}
270
271impl DiagnosticsConfig {
272    /// Creates a new diagnostics configuration from a rule set.
273    pub fn new<T: AsRef<dyn Rule>>(rules: impl IntoIterator<Item = T>) -> Self {
274        let mut unused_import = None;
275        let mut unused_input = None;
276        let mut unused_declaration = None;
277        let mut unused_call = None;
278        let mut unnecessary_function_call = None;
279        let mut using_fallback_version = None;
280
281        for rule in rules {
282            let rule = rule.as_ref();
283            match rule.id() {
284                UNUSED_IMPORT_RULE_ID => unused_import = Some(rule.severity()),
285                UNUSED_INPUT_RULE_ID => unused_input = Some(rule.severity()),
286                UNUSED_DECL_RULE_ID => unused_declaration = Some(rule.severity()),
287                UNUSED_CALL_RULE_ID => unused_call = Some(rule.severity()),
288                UNNECESSARY_FUNCTION_CALL => unnecessary_function_call = Some(rule.severity()),
289                USING_FALLBACK_VERSION => using_fallback_version = Some(rule.severity()),
290                unrecognized => {
291                    warn!(unrecognized, "unrecognized rule");
292                    if cfg!(test) {
293                        panic!("unrecognized rule: {unrecognized}");
294                    }
295                }
296            }
297        }
298
299        Self {
300            unused_import,
301            unused_input,
302            unused_declaration,
303            unused_call,
304            unnecessary_function_call,
305            using_fallback_version,
306        }
307    }
308
309    /// Returns a modified set of diagnostics that accounts for any `#@ except`
310    /// comments that precede the given syntax node.
311    pub fn excepted_for_node(mut self, node: &SyntaxNode) -> Self {
312        let exceptions = node.rule_exceptions();
313
314        if exceptions.contains(UNUSED_IMPORT_RULE_ID) {
315            self.unused_import = None;
316        }
317
318        if exceptions.contains(UNUSED_INPUT_RULE_ID) {
319            self.unused_input = None;
320        }
321
322        if exceptions.contains(UNUSED_DECL_RULE_ID) {
323            self.unused_declaration = None;
324        }
325
326        if exceptions.contains(UNUSED_CALL_RULE_ID) {
327            self.unused_call = None;
328        }
329
330        if exceptions.contains(UNNECESSARY_FUNCTION_CALL) {
331            self.unnecessary_function_call = None;
332        }
333
334        if exceptions.contains(USING_FALLBACK_VERSION) {
335            self.using_fallback_version = None;
336        }
337
338        self
339    }
340
341    /// Excepts all of the diagnostics.
342    pub fn except_all() -> Self {
343        Self {
344            unused_import: None,
345            unused_input: None,
346            unused_declaration: None,
347            unused_call: None,
348            unnecessary_function_call: None,
349            using_fallback_version: None,
350        }
351    }
352}