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