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