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