Skip to content

Commit cf60dd6

Browse files
Add configuration option to disable individual diagnostics (#347)
1 parent f36fb51 commit cf60dd6

File tree

13 files changed

+532
-4
lines changed

13 files changed

+532
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
2020

2121
### Added
2222

23+
- Added `diagnostics.severity` configuration option for configuring diagnostic severity levels
2324
- Added `pythonpath` configuration option for specifying additional Python import paths
2425
- Added documentation for VS Code extension
2526
- Added documentation for Zed extension

crates/djls-bench/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ version = "0.0.0"
44
edition = "2021"
55

66
[dependencies]
7+
djls-conf = { workspace = true }
78
djls-source = { workspace = true }
89
djls-templates = { workspace = true }
910
djls-semantic = { workspace = true }

crates/djls-bench/src/db.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,8 @@ impl SemanticDb for Db {
8585
fn template_dirs(&self) -> Option<Vec<Utf8PathBuf>> {
8686
None
8787
}
88+
89+
fn diagnostics_config(&self) -> djls_conf::DiagnosticsConfig {
90+
djls_conf::DiagnosticsConfig::default()
91+
}
8892
}
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
use std::collections::HashMap;
2+
3+
use serde::Deserialize;
4+
5+
/// Diagnostic severity level for LSP diagnostics.
6+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
7+
#[serde(rename_all = "lowercase")]
8+
pub enum DiagnosticSeverity {
9+
Off,
10+
Error,
11+
Warning,
12+
Info,
13+
Hint,
14+
}
15+
16+
/// Configuration for diagnostic severity levels.
17+
///
18+
/// All diagnostics are enabled by default at "error" severity.
19+
/// Configure severity per diagnostic code or prefix pattern.
20+
/// Specific codes override prefix patterns.
21+
///
22+
/// Example configuration:
23+
/// ```toml
24+
/// [tool.djls.diagnostics.severity]
25+
/// # Individual codes
26+
/// S101 = "warning"
27+
/// S102 = "off"
28+
///
29+
/// # Prefixes for bulk configuration
30+
/// "T" = "off" # Disable all template errors
31+
/// T100 = "hint" # But show parser errors as hints (specific overrides prefix)
32+
/// ```
33+
#[derive(Debug, Clone, PartialEq, Deserialize, Default)]
34+
pub struct DiagnosticsConfig {
35+
/// Map of diagnostic codes/prefixes to severity levels.
36+
/// Supports:
37+
/// - Specific codes: "S100", "T100"
38+
/// - Prefixes: "S" (all S-series), "T" (all T-series), "S1" (S100-S199)
39+
/// - More specific patterns override less specific ones
40+
#[serde(default)]
41+
severity: HashMap<String, DiagnosticSeverity>,
42+
}
43+
44+
impl DiagnosticsConfig {
45+
/// Get the severity level for a diagnostic code.
46+
///
47+
/// Resolution order (most specific wins):
48+
/// 1. Exact match (e.g., "S100")
49+
/// 2. Longest prefix match (e.g., "S1" over "S")
50+
/// 3. Default: Error
51+
#[must_use]
52+
pub fn get_severity(&self, code: &str) -> DiagnosticSeverity {
53+
// First, check for exact match
54+
if let Some(&severity) = self.severity.get(code) {
55+
return severity;
56+
}
57+
58+
// Then, find the longest matching prefix
59+
let mut best_match: Option<(&str, DiagnosticSeverity)> = None;
60+
61+
for (pattern, &severity) in &self.severity {
62+
if code.starts_with(pattern) {
63+
match best_match {
64+
None => best_match = Some((pattern, severity)),
65+
Some((existing_pattern, _)) => {
66+
// Longer patterns are more specific
67+
if pattern.len() > existing_pattern.len() {
68+
best_match = Some((pattern, severity));
69+
}
70+
}
71+
}
72+
}
73+
}
74+
75+
best_match.map_or(DiagnosticSeverity::Error, |(_, severity)| severity)
76+
}
77+
78+
/// Check if a diagnostic should be shown (severity is not Off).
79+
#[must_use]
80+
pub fn is_enabled(&self, code: &str) -> bool {
81+
self.get_severity(code) != DiagnosticSeverity::Off
82+
}
83+
}
84+
85+
#[cfg(test)]
86+
mod tests {
87+
use super::*;
88+
89+
#[test]
90+
fn test_get_severity_default() {
91+
let config = DiagnosticsConfig::default();
92+
assert_eq!(config.get_severity("S100"), DiagnosticSeverity::Error);
93+
assert_eq!(config.get_severity("T100"), DiagnosticSeverity::Error);
94+
}
95+
96+
#[test]
97+
fn test_get_severity_exact_match() {
98+
let mut severity = HashMap::new();
99+
severity.insert("S100".to_string(), DiagnosticSeverity::Warning);
100+
severity.insert("S101".to_string(), DiagnosticSeverity::Off);
101+
102+
let config = DiagnosticsConfig { severity };
103+
104+
assert_eq!(config.get_severity("S100"), DiagnosticSeverity::Warning);
105+
assert_eq!(config.get_severity("S101"), DiagnosticSeverity::Off);
106+
assert_eq!(config.get_severity("S102"), DiagnosticSeverity::Error);
107+
}
108+
109+
#[test]
110+
fn test_get_severity_prefix_match() {
111+
let mut severity = HashMap::new();
112+
severity.insert("S".to_string(), DiagnosticSeverity::Warning);
113+
severity.insert("T".to_string(), DiagnosticSeverity::Off);
114+
115+
let config = DiagnosticsConfig { severity };
116+
117+
assert_eq!(config.get_severity("S100"), DiagnosticSeverity::Warning);
118+
assert_eq!(config.get_severity("S101"), DiagnosticSeverity::Warning);
119+
assert_eq!(config.get_severity("T100"), DiagnosticSeverity::Off);
120+
assert_eq!(config.get_severity("T900"), DiagnosticSeverity::Off);
121+
}
122+
123+
#[test]
124+
fn test_get_severity_longest_prefix_wins() {
125+
let mut severity = HashMap::new();
126+
severity.insert("S".to_string(), DiagnosticSeverity::Warning);
127+
severity.insert("S1".to_string(), DiagnosticSeverity::Off);
128+
severity.insert("S10".to_string(), DiagnosticSeverity::Hint);
129+
130+
let config = DiagnosticsConfig { severity };
131+
132+
// S10 is most specific for S100
133+
assert_eq!(config.get_severity("S100"), DiagnosticSeverity::Hint);
134+
assert_eq!(config.get_severity("S101"), DiagnosticSeverity::Hint);
135+
// S1 is most specific for S110
136+
assert_eq!(config.get_severity("S110"), DiagnosticSeverity::Off);
137+
assert_eq!(config.get_severity("S199"), DiagnosticSeverity::Off);
138+
// S is most specific for S200
139+
assert_eq!(config.get_severity("S200"), DiagnosticSeverity::Warning);
140+
}
141+
142+
#[test]
143+
fn test_get_severity_exact_overrides_prefix() {
144+
let mut severity = HashMap::new();
145+
severity.insert("S".to_string(), DiagnosticSeverity::Warning);
146+
severity.insert("S1".to_string(), DiagnosticSeverity::Off);
147+
severity.insert("S100".to_string(), DiagnosticSeverity::Error);
148+
149+
let config = DiagnosticsConfig { severity };
150+
151+
// Exact match wins
152+
assert_eq!(config.get_severity("S100"), DiagnosticSeverity::Error);
153+
// S1 prefix for other S1xx codes
154+
assert_eq!(config.get_severity("S101"), DiagnosticSeverity::Off);
155+
// S prefix for S2xx codes
156+
assert_eq!(config.get_severity("S200"), DiagnosticSeverity::Warning);
157+
}
158+
159+
#[test]
160+
fn test_is_enabled_default() {
161+
let config = DiagnosticsConfig::default();
162+
assert!(config.is_enabled("S100"));
163+
assert!(config.is_enabled("T100"));
164+
}
165+
166+
#[test]
167+
fn test_is_enabled_with_off() {
168+
let mut severity = HashMap::new();
169+
severity.insert("S100".to_string(), DiagnosticSeverity::Off);
170+
171+
let config = DiagnosticsConfig { severity };
172+
173+
assert!(!config.is_enabled("S100"));
174+
assert!(config.is_enabled("S101"));
175+
}
176+
177+
#[test]
178+
fn test_is_enabled_with_prefix_off() {
179+
let mut severity = HashMap::new();
180+
severity.insert("T".to_string(), DiagnosticSeverity::Off);
181+
182+
let config = DiagnosticsConfig { severity };
183+
184+
assert!(!config.is_enabled("T100"));
185+
assert!(!config.is_enabled("T900"));
186+
assert!(config.is_enabled("S100"));
187+
}
188+
189+
#[test]
190+
fn test_is_enabled_prefix_off_with_specific_override() {
191+
let mut severity = HashMap::new();
192+
severity.insert("T".to_string(), DiagnosticSeverity::Off);
193+
severity.insert("T100".to_string(), DiagnosticSeverity::Hint);
194+
195+
let config = DiagnosticsConfig { severity };
196+
197+
// T100 has specific override, so it's enabled
198+
assert!(config.is_enabled("T100"));
199+
// Other T codes are off
200+
assert!(!config.is_enabled("T900"));
201+
assert!(!config.is_enabled("T901"));
202+
}
203+
204+
#[test]
205+
fn test_deserialize_diagnostics_config() {
206+
let toml = r#"
207+
[severity]
208+
S100 = "off"
209+
S101 = "warning"
210+
S102 = "hint"
211+
"T" = "off"
212+
T100 = "info"
213+
"#;
214+
215+
let config: DiagnosticsConfig = toml::from_str(toml).unwrap();
216+
assert_eq!(config.get_severity("S100"), DiagnosticSeverity::Off);
217+
assert_eq!(config.get_severity("S101"), DiagnosticSeverity::Warning);
218+
assert_eq!(config.get_severity("S102"), DiagnosticSeverity::Hint);
219+
// T prefix applies to T900
220+
assert_eq!(config.get_severity("T900"), DiagnosticSeverity::Off);
221+
// T100 has specific override
222+
assert_eq!(config.get_severity("T100"), DiagnosticSeverity::Info);
223+
}
224+
225+
#[test]
226+
fn test_complex_scenario() {
227+
let mut severity = HashMap::new();
228+
// Disable all template errors
229+
severity.insert("T".to_string(), DiagnosticSeverity::Off);
230+
// But show parser errors as hints
231+
severity.insert("T100".to_string(), DiagnosticSeverity::Hint);
232+
// Make all semantic errors warnings
233+
severity.insert("S".to_string(), DiagnosticSeverity::Warning);
234+
// Except S100 which is completely off
235+
severity.insert("S100".to_string(), DiagnosticSeverity::Off);
236+
// And S10x (S100-S109) should be info
237+
severity.insert("S10".to_string(), DiagnosticSeverity::Info);
238+
239+
let config = DiagnosticsConfig { severity };
240+
241+
// S100 is exact match - off
242+
assert_eq!(config.get_severity("S100"), DiagnosticSeverity::Off);
243+
assert!(!config.is_enabled("S100"));
244+
245+
// S101 matches S10 prefix - info
246+
assert_eq!(config.get_severity("S101"), DiagnosticSeverity::Info);
247+
assert!(config.is_enabled("S101"));
248+
249+
// S200 matches S prefix - warning
250+
assert_eq!(config.get_severity("S200"), DiagnosticSeverity::Warning);
251+
assert!(config.is_enabled("S200"));
252+
253+
// T100 has exact match - hint
254+
assert_eq!(config.get_severity("T100"), DiagnosticSeverity::Hint);
255+
assert!(config.is_enabled("T100"));
256+
257+
// T900 matches T prefix - off
258+
assert_eq!(config.get_severity("T900"), DiagnosticSeverity::Off);
259+
assert!(!config.is_enabled("T900"));
260+
}
261+
}

crates/djls-conf/src/lib.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub mod diagnostics;
12
pub mod tagspecs;
23

34
use std::fs;
@@ -14,6 +15,8 @@ use directories::ProjectDirs;
1415
use serde::Deserialize;
1516
use thiserror::Error;
1617

18+
pub use crate::diagnostics::DiagnosticSeverity;
19+
pub use crate::diagnostics::DiagnosticsConfig;
1720
pub use crate::tagspecs::ArgTypeDef;
1821
pub use crate::tagspecs::EndTagDef;
1922
pub use crate::tagspecs::IntermediateTagDef;
@@ -65,6 +68,8 @@ pub struct Settings {
6568
pythonpath: Vec<String>,
6669
#[serde(default)]
6770
tagspecs: Vec<TagSpecDef>,
71+
#[serde(default)]
72+
diagnostics: DiagnosticsConfig,
6873
}
6974

7075
impl Settings {
@@ -86,6 +91,10 @@ impl Settings {
8691
if !overrides.tagspecs.is_empty() {
8792
settings.tagspecs = overrides.tagspecs;
8893
}
94+
// For diagnostics, override if the config is non-default
95+
if overrides.diagnostics != DiagnosticsConfig::default() {
96+
settings.diagnostics = overrides.diagnostics;
97+
}
8998
}
9099

91100
Ok(settings)
@@ -158,6 +167,11 @@ impl Settings {
158167
pub fn tagspecs(&self) -> &[TagSpecDef] {
159168
&self.tagspecs
160169
}
170+
171+
#[must_use]
172+
pub fn diagnostics(&self) -> &DiagnosticsConfig {
173+
&self.diagnostics
174+
}
161175
}
162176

163177
#[cfg(test)]
@@ -184,6 +198,7 @@ mod tests {
184198
django_settings_module: None,
185199
pythonpath: vec![],
186200
tagspecs: vec![],
201+
diagnostics: DiagnosticsConfig::default(),
187202
}
188203
);
189204
}
@@ -267,6 +282,42 @@ mod tests {
267282
}
268283
);
269284
}
285+
286+
#[test]
287+
fn test_load_diagnostics_config() {
288+
let dir = tempdir().unwrap();
289+
fs::write(
290+
dir.path().join("djls.toml"),
291+
r#"
292+
[diagnostics.severity]
293+
S100 = "off"
294+
S101 = "warning"
295+
"T" = "off"
296+
T100 = "hint"
297+
"#,
298+
)
299+
.unwrap();
300+
let settings = Settings::new(Utf8Path::from_path(dir.path()).unwrap(), None).unwrap();
301+
// Test via public API
302+
assert_eq!(
303+
settings.diagnostics.get_severity("S100"),
304+
DiagnosticSeverity::Off
305+
);
306+
assert_eq!(
307+
settings.diagnostics.get_severity("S101"),
308+
DiagnosticSeverity::Warning
309+
);
310+
// T prefix applies to T900
311+
assert_eq!(
312+
settings.diagnostics.get_severity("T900"),
313+
DiagnosticSeverity::Off
314+
);
315+
// T100 has specific override
316+
assert_eq!(
317+
settings.diagnostics.get_severity("T100"),
318+
DiagnosticSeverity::Hint
319+
);
320+
}
270321
}
271322

272323
mod priority {

crates/djls-ide/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ version = "0.0.0"
44
edition = "2021"
55

66
[dependencies]
7+
djls-conf = { workspace = true }
78
djls-project = { workspace = true }
89
djls-semantic = { workspace = true }
910
djls-source = { workspace = true }

0 commit comments

Comments
 (0)