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