Skip to main content

wdl_analysis/
config.rs

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