Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/

### Added

- Added `diagnostics.severity` configuration option for configuring diagnostic severity levels
- Added `pythonpath` configuration option for specifying additional Python import paths
- Added documentation for VS Code extension
- Added documentation for Zed extension
Expand Down
1 change: 1 addition & 0 deletions crates/djls-bench/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ version = "0.0.0"
edition = "2021"

[dependencies]
djls-conf = { workspace = true }
djls-source = { workspace = true }
djls-templates = { workspace = true }
djls-semantic = { workspace = true }
Expand Down
4 changes: 4 additions & 0 deletions crates/djls-bench/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,8 @@ impl SemanticDb for Db {
fn template_dirs(&self) -> Option<Vec<Utf8PathBuf>> {
None
}

fn diagnostics_config(&self) -> djls_conf::DiagnosticsConfig {
djls_conf::DiagnosticsConfig::default()
}
}
261 changes: 261 additions & 0 deletions crates/djls-conf/src/diagnostics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
use std::collections::HashMap;

use serde::Deserialize;

/// Diagnostic severity level for LSP diagnostics.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DiagnosticSeverity {
Off,
Error,
Warning,
Info,
Hint,
}

/// Configuration for diagnostic severity levels.
///
/// All diagnostics are enabled by default at "error" severity.
/// Configure severity per diagnostic code or prefix pattern.
/// Specific codes override prefix patterns.
///
/// Example configuration:
/// ```toml
/// [tool.djls.diagnostics.severity]
/// # Individual codes
/// S101 = "warning"
/// S102 = "off"
///
/// # Prefixes for bulk configuration
/// "T" = "off" # Disable all template errors
/// T100 = "hint" # But show parser errors as hints (specific overrides prefix)
/// ```
#[derive(Debug, Clone, PartialEq, Deserialize, Default)]
pub struct DiagnosticsConfig {
/// Map of diagnostic codes/prefixes to severity levels.
/// Supports:
/// - Specific codes: "S100", "T100"
/// - Prefixes: "S" (all S-series), "T" (all T-series), "S1" (S100-S199)
/// - More specific patterns override less specific ones
#[serde(default)]
severity: HashMap<String, DiagnosticSeverity>,
}

impl DiagnosticsConfig {
/// Get the severity level for a diagnostic code.
///
/// Resolution order (most specific wins):
/// 1. Exact match (e.g., "S100")
/// 2. Longest prefix match (e.g., "S1" over "S")
/// 3. Default: Error
#[must_use]
pub fn get_severity(&self, code: &str) -> DiagnosticSeverity {
// First, check for exact match
if let Some(&severity) = self.severity.get(code) {
return severity;
}

// Then, find the longest matching prefix
let mut best_match: Option<(&str, DiagnosticSeverity)> = None;

for (pattern, &severity) in &self.severity {
if code.starts_with(pattern) {
match best_match {
None => best_match = Some((pattern, severity)),
Some((existing_pattern, _)) => {
// Longer patterns are more specific
if pattern.len() > existing_pattern.len() {
best_match = Some((pattern, severity));
}
}
}
}
}

best_match.map_or(DiagnosticSeverity::Error, |(_, severity)| severity)
}

/// Check if a diagnostic should be shown (severity is not Off).
#[must_use]
pub fn is_enabled(&self, code: &str) -> bool {
self.get_severity(code) != DiagnosticSeverity::Off
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_get_severity_default() {
let config = DiagnosticsConfig::default();
assert_eq!(config.get_severity("S100"), DiagnosticSeverity::Error);
assert_eq!(config.get_severity("T100"), DiagnosticSeverity::Error);
}

#[test]
fn test_get_severity_exact_match() {
let mut severity = HashMap::new();
severity.insert("S100".to_string(), DiagnosticSeverity::Warning);
severity.insert("S101".to_string(), DiagnosticSeverity::Off);

let config = DiagnosticsConfig { severity };

assert_eq!(config.get_severity("S100"), DiagnosticSeverity::Warning);
assert_eq!(config.get_severity("S101"), DiagnosticSeverity::Off);
assert_eq!(config.get_severity("S102"), DiagnosticSeverity::Error);
}

#[test]
fn test_get_severity_prefix_match() {
let mut severity = HashMap::new();
severity.insert("S".to_string(), DiagnosticSeverity::Warning);
severity.insert("T".to_string(), DiagnosticSeverity::Off);

let config = DiagnosticsConfig { severity };

assert_eq!(config.get_severity("S100"), DiagnosticSeverity::Warning);
assert_eq!(config.get_severity("S101"), DiagnosticSeverity::Warning);
assert_eq!(config.get_severity("T100"), DiagnosticSeverity::Off);
assert_eq!(config.get_severity("T900"), DiagnosticSeverity::Off);
}

#[test]
fn test_get_severity_longest_prefix_wins() {
let mut severity = HashMap::new();
severity.insert("S".to_string(), DiagnosticSeverity::Warning);
severity.insert("S1".to_string(), DiagnosticSeverity::Off);
severity.insert("S10".to_string(), DiagnosticSeverity::Hint);

let config = DiagnosticsConfig { severity };

// S10 is most specific for S100
assert_eq!(config.get_severity("S100"), DiagnosticSeverity::Hint);
assert_eq!(config.get_severity("S101"), DiagnosticSeverity::Hint);
// S1 is most specific for S110
assert_eq!(config.get_severity("S110"), DiagnosticSeverity::Off);
assert_eq!(config.get_severity("S199"), DiagnosticSeverity::Off);
// S is most specific for S200
assert_eq!(config.get_severity("S200"), DiagnosticSeverity::Warning);
}

#[test]
fn test_get_severity_exact_overrides_prefix() {
let mut severity = HashMap::new();
severity.insert("S".to_string(), DiagnosticSeverity::Warning);
severity.insert("S1".to_string(), DiagnosticSeverity::Off);
severity.insert("S100".to_string(), DiagnosticSeverity::Error);

let config = DiagnosticsConfig { severity };

// Exact match wins
assert_eq!(config.get_severity("S100"), DiagnosticSeverity::Error);
// S1 prefix for other S1xx codes
assert_eq!(config.get_severity("S101"), DiagnosticSeverity::Off);
// S prefix for S2xx codes
assert_eq!(config.get_severity("S200"), DiagnosticSeverity::Warning);
}

#[test]
fn test_is_enabled_default() {
let config = DiagnosticsConfig::default();
assert!(config.is_enabled("S100"));
assert!(config.is_enabled("T100"));
}

#[test]
fn test_is_enabled_with_off() {
let mut severity = HashMap::new();
severity.insert("S100".to_string(), DiagnosticSeverity::Off);

let config = DiagnosticsConfig { severity };

assert!(!config.is_enabled("S100"));
assert!(config.is_enabled("S101"));
}

#[test]
fn test_is_enabled_with_prefix_off() {
let mut severity = HashMap::new();
severity.insert("T".to_string(), DiagnosticSeverity::Off);

let config = DiagnosticsConfig { severity };

assert!(!config.is_enabled("T100"));
assert!(!config.is_enabled("T900"));
assert!(config.is_enabled("S100"));
}

#[test]
fn test_is_enabled_prefix_off_with_specific_override() {
let mut severity = HashMap::new();
severity.insert("T".to_string(), DiagnosticSeverity::Off);
severity.insert("T100".to_string(), DiagnosticSeverity::Hint);

let config = DiagnosticsConfig { severity };

// T100 has specific override, so it's enabled
assert!(config.is_enabled("T100"));
// Other T codes are off
assert!(!config.is_enabled("T900"));
assert!(!config.is_enabled("T901"));
}

#[test]
fn test_deserialize_diagnostics_config() {
let toml = r#"
[severity]
S100 = "off"
S101 = "warning"
S102 = "hint"
"T" = "off"
T100 = "info"
"#;

let config: DiagnosticsConfig = toml::from_str(toml).unwrap();
assert_eq!(config.get_severity("S100"), DiagnosticSeverity::Off);
assert_eq!(config.get_severity("S101"), DiagnosticSeverity::Warning);
assert_eq!(config.get_severity("S102"), DiagnosticSeverity::Hint);
// T prefix applies to T900
assert_eq!(config.get_severity("T900"), DiagnosticSeverity::Off);
// T100 has specific override
assert_eq!(config.get_severity("T100"), DiagnosticSeverity::Info);
}

#[test]
fn test_complex_scenario() {
let mut severity = HashMap::new();
// Disable all template errors
severity.insert("T".to_string(), DiagnosticSeverity::Off);
// But show parser errors as hints
severity.insert("T100".to_string(), DiagnosticSeverity::Hint);
// Make all semantic errors warnings
severity.insert("S".to_string(), DiagnosticSeverity::Warning);
// Except S100 which is completely off
severity.insert("S100".to_string(), DiagnosticSeverity::Off);
// And S10x (S100-S109) should be info
severity.insert("S10".to_string(), DiagnosticSeverity::Info);

let config = DiagnosticsConfig { severity };

// S100 is exact match - off
assert_eq!(config.get_severity("S100"), DiagnosticSeverity::Off);
assert!(!config.is_enabled("S100"));

// S101 matches S10 prefix - info
assert_eq!(config.get_severity("S101"), DiagnosticSeverity::Info);
assert!(config.is_enabled("S101"));

// S200 matches S prefix - warning
assert_eq!(config.get_severity("S200"), DiagnosticSeverity::Warning);
assert!(config.is_enabled("S200"));

// T100 has exact match - hint
assert_eq!(config.get_severity("T100"), DiagnosticSeverity::Hint);
assert!(config.is_enabled("T100"));

// T900 matches T prefix - off
assert_eq!(config.get_severity("T900"), DiagnosticSeverity::Off);
assert!(!config.is_enabled("T900"));
}
}
51 changes: 51 additions & 0 deletions crates/djls-conf/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod diagnostics;
pub mod tagspecs;

use std::fs;
Expand All @@ -14,6 +15,8 @@ use directories::ProjectDirs;
use serde::Deserialize;
use thiserror::Error;

pub use crate::diagnostics::DiagnosticSeverity;
pub use crate::diagnostics::DiagnosticsConfig;
pub use crate::tagspecs::ArgTypeDef;
pub use crate::tagspecs::EndTagDef;
pub use crate::tagspecs::IntermediateTagDef;
Expand Down Expand Up @@ -65,6 +68,8 @@ pub struct Settings {
pythonpath: Vec<String>,
#[serde(default)]
tagspecs: Vec<TagSpecDef>,
#[serde(default)]
diagnostics: DiagnosticsConfig,
}

impl Settings {
Expand All @@ -86,6 +91,10 @@ impl Settings {
if !overrides.tagspecs.is_empty() {
settings.tagspecs = overrides.tagspecs;
}
// For diagnostics, override if the config is non-default
if overrides.diagnostics != DiagnosticsConfig::default() {
settings.diagnostics = overrides.diagnostics;
}
}

Ok(settings)
Expand Down Expand Up @@ -158,6 +167,11 @@ impl Settings {
pub fn tagspecs(&self) -> &[TagSpecDef] {
&self.tagspecs
}

#[must_use]
pub fn diagnostics(&self) -> &DiagnosticsConfig {
&self.diagnostics
}
}

#[cfg(test)]
Expand All @@ -184,6 +198,7 @@ mod tests {
django_settings_module: None,
pythonpath: vec![],
tagspecs: vec![],
diagnostics: DiagnosticsConfig::default(),
}
);
}
Expand Down Expand Up @@ -267,6 +282,42 @@ mod tests {
}
);
}

#[test]
fn test_load_diagnostics_config() {
let dir = tempdir().unwrap();
fs::write(
dir.path().join("djls.toml"),
r#"
[diagnostics.severity]
S100 = "off"
S101 = "warning"
"T" = "off"
T100 = "hint"
"#,
)
.unwrap();
let settings = Settings::new(Utf8Path::from_path(dir.path()).unwrap(), None).unwrap();
// Test via public API
assert_eq!(
settings.diagnostics.get_severity("S100"),
DiagnosticSeverity::Off
);
assert_eq!(
settings.diagnostics.get_severity("S101"),
DiagnosticSeverity::Warning
);
// T prefix applies to T900
assert_eq!(
settings.diagnostics.get_severity("T900"),
DiagnosticSeverity::Off
);
// T100 has specific override
assert_eq!(
settings.diagnostics.get_severity("T100"),
DiagnosticSeverity::Hint
);
}
}

mod priority {
Expand Down
1 change: 1 addition & 0 deletions crates/djls-ide/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ version = "0.0.0"
edition = "2021"

[dependencies]
djls-conf = { workspace = true }
djls-project = { workspace = true }
djls-semantic = { workspace = true }
djls-source = { workspace = true }
Expand Down
Loading