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            }),
49        }
50    }
51}
52
53impl Config {
54    /// Get this configuration's [`DiagnosticsConfig`].
55    pub fn diagnostics_config(&self) -> &DiagnosticsConfig {
56        &self.inner.diagnostics
57    }
58
59    /// Get this configuration's fallback version; see
60    /// [`Config::with_fallback_version()`].
61    pub fn fallback_version(&self) -> Option<SupportedVersion> {
62        self.inner.fallback_version
63    }
64
65    /// Return a new configuration with the previous [`DiagnosticsConfig`]
66    /// replaced by the argument.
67    pub fn with_diagnostics_config(&self, diagnostics: DiagnosticsConfig) -> Self {
68        let mut inner = (*self.inner).clone();
69        inner.diagnostics = diagnostics;
70        Self {
71            inner: Arc::new(inner),
72        }
73    }
74
75    /// Return a new configuration with the previous version fallback option
76    /// replaced by the argument.
77    ///
78    /// This option controls what happens when analyzing a WDL document with a
79    /// syntactically valid but unrecognized version in the version
80    /// statement. The default value is `None`, with no fallback behavior.
81    ///
82    /// Configured with `Some(fallback_version)`, analysis will proceed as
83    /// normal if the version statement contains a recognized version. If
84    /// the version is unrecognized, analysis will continue as if the
85    /// version statement contained `fallback_version`, though the concrete
86    /// syntax of the version statement will remain unchanged.
87    ///
88    /// <div class="warning">
89    ///
90    /// # Warnings
91    ///
92    /// This option is intended only for situations where unexpected behavior
93    /// due to unsupported syntax is acceptable, such as when providing
94    /// best-effort editor hints via `wdl-lsp`. The semantics of executing a
95    /// WDL workflow with an unrecognized version is undefined and not
96    /// recommended.
97    ///
98    /// Once this option has been configured for an `Analyzer`, it should not be
99    /// changed. A document that was initially parsed and analyzed with one
100    /// fallback option may cause errors if subsequent operations are
101    /// performed with a different fallback option.
102    ///
103    /// </div>
104    pub fn with_fallback_version(&self, fallback_version: Option<SupportedVersion>) -> Self {
105        let mut inner = (*self.inner).clone();
106        inner.fallback_version = fallback_version;
107        Self {
108            inner: Arc::new(inner),
109        }
110    }
111}
112
113/// The actual configuration fields inside the [`Config`] wrapper.
114#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
115struct ConfigInner {
116    /// See [`DiagnosticsConfig`].
117    #[serde(default)]
118    diagnostics: DiagnosticsConfig,
119    /// See [`Config::with_fallback_version()`]
120    #[serde(default)]
121    fallback_version: Option<SupportedVersion>,
122}
123
124/// Configuration for analysis diagnostics.
125///
126/// Only the analysis diagnostics that aren't inherently treated as errors are
127/// represented here.
128///
129/// These diagnostics default to a warning severity.
130#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
131pub struct DiagnosticsConfig {
132    /// The severity for the unused import diagnostic.
133    ///
134    /// A value of `None` disables the diagnostic.
135    pub unused_import: Option<Severity>,
136    /// The severity for the unused input diagnostic.
137    ///
138    /// A value of `None` disables the diagnostic.
139    pub unused_input: Option<Severity>,
140    /// The severity for the unused declaration diagnostic.
141    ///
142    /// A value of `None` disables the diagnostic.
143    pub unused_declaration: Option<Severity>,
144    /// The severity for the unused call diagnostic.
145    ///
146    /// A value of `None` disables the diagnostic.
147    pub unused_call: Option<Severity>,
148    /// The severity for the unnecessary function call diagnostic.
149    ///
150    /// A value of `None` disables the diagnostic.
151    pub unnecessary_function_call: Option<Severity>,
152    /// The severity for the using fallback version diagnostic.
153    ///
154    /// A value of `None` disables the diagnostic. If there is no version
155    /// configured with [`Config::with_fallback_version()`], this diagnostic
156    /// will not be emitted.
157    pub using_fallback_version: Option<Severity>,
158}
159
160impl Default for DiagnosticsConfig {
161    fn default() -> Self {
162        Self::new(rules())
163    }
164}
165
166impl DiagnosticsConfig {
167    /// Creates a new diagnostics configuration from a rule set.
168    pub fn new<T: AsRef<dyn Rule>>(rules: impl IntoIterator<Item = T>) -> Self {
169        let mut unused_import = None;
170        let mut unused_input = None;
171        let mut unused_declaration = None;
172        let mut unused_call = None;
173        let mut unnecessary_function_call = None;
174        let mut using_fallback_version = None;
175
176        for rule in rules {
177            let rule = rule.as_ref();
178            match rule.id() {
179                UNUSED_IMPORT_RULE_ID => unused_import = Some(rule.severity()),
180                UNUSED_INPUT_RULE_ID => unused_input = Some(rule.severity()),
181                UNUSED_DECL_RULE_ID => unused_declaration = Some(rule.severity()),
182                UNUSED_CALL_RULE_ID => unused_call = Some(rule.severity()),
183                UNNECESSARY_FUNCTION_CALL => unnecessary_function_call = Some(rule.severity()),
184                USING_FALLBACK_VERSION => using_fallback_version = Some(rule.severity()),
185                unrecognized => {
186                    warn!(unrecognized, "unrecognized rule");
187                    if cfg!(test) {
188                        panic!("unrecognized rule: {unrecognized}");
189                    }
190                }
191            }
192        }
193
194        Self {
195            unused_import,
196            unused_input,
197            unused_declaration,
198            unused_call,
199            unnecessary_function_call,
200            using_fallback_version,
201        }
202    }
203
204    /// Returns a modified set of diagnostics that accounts for any `#@ except`
205    /// comments that precede the given syntax node.
206    pub fn excepted_for_node(mut self, node: &SyntaxNode) -> Self {
207        let exceptions = node.rule_exceptions();
208
209        if exceptions.contains(UNUSED_IMPORT_RULE_ID) {
210            self.unused_import = None;
211        }
212
213        if exceptions.contains(UNUSED_INPUT_RULE_ID) {
214            self.unused_input = None;
215        }
216
217        if exceptions.contains(UNUSED_DECL_RULE_ID) {
218            self.unused_declaration = None;
219        }
220
221        if exceptions.contains(UNUSED_CALL_RULE_ID) {
222            self.unused_call = None;
223        }
224
225        if exceptions.contains(UNNECESSARY_FUNCTION_CALL) {
226            self.unnecessary_function_call = None;
227        }
228
229        if exceptions.contains(USING_FALLBACK_VERSION) {
230            self.using_fallback_version = None;
231        }
232
233        self
234    }
235
236    /// Excepts all of the diagnostics.
237    pub fn except_all() -> Self {
238        Self {
239            unused_import: None,
240            unused_input: None,
241            unused_declaration: None,
242            unused_call: None,
243            unnecessary_function_call: None,
244            using_fallback_version: None,
245        }
246    }
247}