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}