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}