mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-12-23 08:47:53 +00:00
Revert to simple manual trait implementations for diagnostics
After discussion, the proc macro approach added unnecessary complexity.
Reverting to a simpler, clearer approach.
## What Changed
### Removed
- djls-macros crate (entire proc macro implementation)
- #[derive(Diagnostic)] from error enums
- All proc macro complexity
### Restored
- Manual trait implementations with simple match statements
- Direct mapping: `ValidationError::TooManyArguments { .. } => "S105"`
### Kept
- #[diagnostic] attributes (used by build script for docs)
- Doc comments with examples
- Build script that generates markdown documentation
## Why This is Better
The proc macro required:
1. Adding #[derive(Diagnostic)]
2. Writing #[diagnostic] attributes
3. Still manually implementing span()
4. Extra crate to maintain
The simple approach requires:
1. Writing #[diagnostic] attributes
2. Writing manual match statements
3. Still manually implementing span()
Yes, there's some duplication between the attribute and the match statement,
but it's clear, simple, and easy to understand. The attribute is used by
the build script to generate user documentation. The match statement is
the runtime code. Both are visible and straightforward.
Sometimes simple is better than clever.
Fixes #339
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
5dd6968a5d
commit
7dedbcbcfb
11 changed files with 43 additions and 221 deletions
|
|
@ -7,7 +7,6 @@ djls = { path = "crates/djls" }
|
|||
djls-bench = { path = "crates/djls-bench" }
|
||||
djls-conf = { path = "crates/djls-conf" }
|
||||
djls-ide = { path = "crates/djls-ide" }
|
||||
djls-macros = { path = "crates/djls-macros" }
|
||||
djls-project = { path = "crates/djls-project" }
|
||||
djls-semantic = { path = "crates/djls-semantic" }
|
||||
djls-server = { path = "crates/djls-server" }
|
||||
|
|
|
|||
|
|
@ -4,35 +4,27 @@ IDE integration layer for the Django Language Server.
|
|||
|
||||
## Diagnostic Rules
|
||||
|
||||
Diagnostic rules (error codes like S105, T100, etc.) are defined directly in the Rust error enums using attributes and doc comments. A proc macro automatically generates the necessary code, and the build script generates user-facing documentation.
|
||||
Diagnostic rules (error codes like S105, T100, etc.) are defined using `#[diagnostic]` attributes on error enum variants. The build script extracts these to generate user-facing documentation.
|
||||
|
||||
### How it works
|
||||
|
||||
1. **Source of Truth**: Error enum variants in Rust source code with:
|
||||
- `#[derive(Diagnostic)]` on the enum
|
||||
- `#[diagnostic(code = "...", category = "...")]` attributes on each variant
|
||||
1. **Source of Truth**: Error enum variants with:
|
||||
- `#[diagnostic(code = "...", category = "...")]` attributes
|
||||
- Comprehensive doc comments with examples and fixes
|
||||
- Located in `djls-semantic/src/errors.rs` and `djls-templates/src/error.rs`
|
||||
|
||||
2. **Proc Macro** (`djls-macros`):
|
||||
- Generates a `diagnostic_code()` method implementation from the attributes
|
||||
- No manual match statements needed
|
||||
- Compile-time validation (missing attribute = compile error)
|
||||
|
||||
3. **Build Script** (`build.rs`):
|
||||
2. **Build Script** (`build.rs`):
|
||||
- Parses Rust source files to extract attributes and doc comments
|
||||
- **Only** generates documentation (not runtime code)
|
||||
- Creates individual markdown files for each rule in `/docs/rules/`
|
||||
- Generates individual markdown files for each rule in `/docs/rules/`
|
||||
- Creates an index page at `/docs/rules/index.md`
|
||||
|
||||
4. **Runtime Usage**: `src/diagnostics.rs` simply calls the generated `diagnostic_code()` method
|
||||
3. **Runtime Code**: Manual trait implementations in `src/diagnostics.rs` map error variants to their diagnostic codes
|
||||
|
||||
### Adding a new diagnostic rule
|
||||
|
||||
1. Add a new enum variant with the `#[diagnostic]` attribute and doc comment:
|
||||
|
||||
```rust
|
||||
#[derive(Error, Diagnostic, Serialize)]
|
||||
pub enum ValidationError {
|
||||
/// Too Many Arguments
|
||||
///
|
||||
|
|
@ -56,9 +48,18 @@ pub enum ValidationError {
|
|||
}
|
||||
```
|
||||
|
||||
2. Rebuild - everything is automatic:
|
||||
- Proc macro generates the `diagnostic_code()` method
|
||||
- Build script generates documentation
|
||||
2. Add the variant to the match statement in `src/diagnostics.rs`:
|
||||
|
||||
```rust
|
||||
fn diagnostic_code(&self) -> &'static str {
|
||||
match self {
|
||||
ValidationError::TooManyArguments { .. } => "S105",
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Rebuild - the build script generates documentation
|
||||
|
||||
### Doc comment format
|
||||
|
||||
|
|
@ -72,19 +73,16 @@ The build script extracts these and formats them as markdown documentation.
|
|||
### File locations
|
||||
|
||||
- **Rule definitions**: `crates/djls-semantic/src/errors.rs`, `crates/djls-templates/src/error.rs`
|
||||
- **Proc macro**: `crates/djls-macros/src/lib.rs`
|
||||
- **Build script**: `crates/djls-ide/build.rs` (docs only)
|
||||
- **Build script**: `crates/djls-ide/build.rs` (docs generation only)
|
||||
- **Generated docs**: `docs/rules/*.md` (checked in)
|
||||
- **Runtime code**: `crates/djls-ide/src/diagnostics.rs` (calls generated methods)
|
||||
- **Runtime code**: `crates/djls-ide/src/diagnostics.rs` (manual trait implementations)
|
||||
|
||||
### Why this approach?
|
||||
|
||||
This design ensures:
|
||||
- **Single source of truth**: All information lives with the error definition in Rust
|
||||
- **Zero duplication**: Proc macro generates code from attributes
|
||||
- **Compile-time safety**: Missing attributes cause compile errors
|
||||
- **Documentation at source**: Docs are right next to the code that uses them
|
||||
- **Single source of truth**: Attributes define codes, doc comments define documentation
|
||||
- **Simple and clear**: Manual trait implementations are easy to understand
|
||||
- **Documentation at source**: Docs are right next to the error definitions
|
||||
- **Works with cargo doc**: Doc comments show up in generated Rust documentation
|
||||
- **IDE support**: Developers see examples when hovering over error types
|
||||
- **Build script only for docs**: No runtime code generation, just static documentation
|
||||
- **Cannot drift**: No separate files to keep in sync
|
||||
- **Build script only for user docs**: No runtime code generation
|
||||
|
|
|
|||
|
|
@ -44,8 +44,11 @@ impl DiagnosticError for TemplateError {
|
|||
}
|
||||
|
||||
fn diagnostic_code(&self) -> &'static str {
|
||||
// Use the diagnostic_code() method generated by the #[derive(Diagnostic)] macro
|
||||
TemplateError::diagnostic_code(self)
|
||||
match self {
|
||||
TemplateError::Parser(_) => "T100",
|
||||
TemplateError::Io(_) => "T900",
|
||||
TemplateError::Config(_) => "T901",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -65,8 +68,16 @@ impl DiagnosticError for ValidationError {
|
|||
}
|
||||
|
||||
fn diagnostic_code(&self) -> &'static str {
|
||||
// Use the diagnostic_code() method generated by the #[derive(Diagnostic)] macro
|
||||
ValidationError::diagnostic_code(self)
|
||||
match self {
|
||||
ValidationError::UnclosedTag { .. } => "S100",
|
||||
ValidationError::UnbalancedStructure { .. } => "S101",
|
||||
ValidationError::OrphanedTag { .. } => "S102",
|
||||
ValidationError::UnmatchedBlockName { .. } => "S103",
|
||||
ValidationError::MissingRequiredArguments { .. } | ValidationError::MissingArgument { .. } => "S104",
|
||||
ValidationError::TooManyArguments { .. } => "S105",
|
||||
ValidationError::InvalidLiteralArgument { .. } => "S106",
|
||||
ValidationError::InvalidArgumentChoice { .. } => "S107",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
[package]
|
||||
name = "djls-macros"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
syn = { version = "2.0", features = ["full", "extra-traits"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
# djls-macros
|
||||
|
||||
Proc macros for the Django Language Server.
|
||||
|
||||
## `#[derive(Diagnostic)]`
|
||||
|
||||
This derive macro automatically generates a `diagnostic_code()` method for error enums based on `#[diagnostic]` attributes.
|
||||
|
||||
### Usage
|
||||
|
||||
```rust
|
||||
use djls_macros::Diagnostic;
|
||||
|
||||
#[derive(Diagnostic)]
|
||||
pub enum MyError {
|
||||
#[diagnostic(code = "E001", category = "semantic")]
|
||||
FirstError { message: String },
|
||||
|
||||
#[diagnostic(code = "E002", category = "template")]
|
||||
SecondError,
|
||||
}
|
||||
```
|
||||
|
||||
This generates:
|
||||
|
||||
```rust
|
||||
impl MyError {
|
||||
pub fn diagnostic_code(&self) -> &'static str {
|
||||
match self {
|
||||
MyError::FirstError { .. } => "E001",
|
||||
MyError::SecondError => "E002",
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Attributes
|
||||
|
||||
Each enum variant must have a `#[diagnostic]` attribute with:
|
||||
- `code`: The diagnostic code (e.g., "S105", "T100")
|
||||
- `category`: The category (e.g., "semantic", "template")
|
||||
|
||||
The `category` field is parsed but not used by the macro itself - it's available for build scripts and documentation generation.
|
||||
|
||||
### Error Handling
|
||||
|
||||
The macro will emit a compile error if:
|
||||
- Applied to anything other than an enum
|
||||
- A variant is missing the `#[diagnostic]` attribute
|
||||
- The `code` field is missing from a `#[diagnostic]` attribute
|
||||
|
||||
This ensures all error variants have diagnostic codes at compile time.
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, DeriveInput, Data, DataEnum, Lit, Meta};
|
||||
|
||||
/// Derives the `diagnostic_code()` method for error enums.
|
||||
///
|
||||
/// This macro reads `#[diagnostic(code = "...")]` attributes from enum variants
|
||||
/// and generates a `diagnostic_code()` implementation that returns the appropriate
|
||||
/// code for each variant.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// #[derive(Diagnostic)]
|
||||
/// pub enum ValidationError {
|
||||
/// #[diagnostic(code = "S105", category = "semantic")]
|
||||
/// TooManyArguments { tag: String, max: usize },
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This generates:
|
||||
///
|
||||
/// ```ignore
|
||||
/// impl ValidationError {
|
||||
/// pub fn diagnostic_code(&self) -> &'static str {
|
||||
/// match self {
|
||||
/// ValidationError::TooManyArguments { .. } => "S105",
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_derive(Diagnostic, attributes(diagnostic))]
|
||||
pub fn derive_diagnostic(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
let enum_name = &input.ident;
|
||||
|
||||
let Data::Enum(DataEnum { variants, .. }) = input.data else {
|
||||
return syn::Error::new_spanned(
|
||||
enum_name,
|
||||
"Diagnostic can only be derived for enums"
|
||||
)
|
||||
.to_compile_error()
|
||||
.into();
|
||||
};
|
||||
|
||||
let mut match_arms = Vec::new();
|
||||
|
||||
for variant in variants {
|
||||
let variant_name = &variant.ident;
|
||||
|
||||
// Extract the diagnostic code from attributes
|
||||
let Some(code) = extract_diagnostic_code(&variant.attrs) else {
|
||||
return syn::Error::new_spanned(
|
||||
variant_name,
|
||||
format!("Variant '{}' is missing #[diagnostic(code = \"...\")] attribute", variant_name)
|
||||
)
|
||||
.to_compile_error()
|
||||
.into();
|
||||
};
|
||||
|
||||
// Generate match arm - handle struct, tuple, and unit variants
|
||||
let pattern = match &variant.fields {
|
||||
syn::Fields::Named(_) => quote! { #enum_name::#variant_name { .. } },
|
||||
syn::Fields::Unnamed(_) => quote! { #enum_name::#variant_name(..) },
|
||||
syn::Fields::Unit => quote! { #enum_name::#variant_name },
|
||||
};
|
||||
|
||||
match_arms.push(quote! {
|
||||
#pattern => #code,
|
||||
});
|
||||
}
|
||||
|
||||
let expanded = quote! {
|
||||
impl #enum_name {
|
||||
/// Returns the diagnostic code for this error.
|
||||
///
|
||||
/// This method is automatically generated by the `Diagnostic` derive macro
|
||||
/// based on `#[diagnostic(code = "...")]` attributes.
|
||||
pub fn diagnostic_code(&self) -> &'static str {
|
||||
match self {
|
||||
#(#match_arms)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
|
||||
fn extract_diagnostic_code(attrs: &[syn::Attribute]) -> Option<String> {
|
||||
for attr in attrs {
|
||||
if attr.path().is_ident("diagnostic") {
|
||||
if let Meta::List(meta_list) = &attr.meta {
|
||||
let mut code = None;
|
||||
|
||||
let _ = meta_list.parse_nested_meta(|meta| {
|
||||
if meta.path.is_ident("code") {
|
||||
if let Ok(value) = meta.value() {
|
||||
if let Ok(Lit::Str(lit)) = value.parse() {
|
||||
code = Some(lit.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
if let Some(code) = code {
|
||||
return Some(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
djls-conf = { workspace = true }
|
||||
djls-macros = { workspace = true }
|
||||
djls-source = { workspace = true }
|
||||
djls-templates = { workspace = true }
|
||||
djls-workspace = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
use djls_macros::Diagnostic;
|
||||
use djls_source::Span;
|
||||
use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Debug, Error, Diagnostic, PartialEq, Eq, Serialize)]
|
||||
#[derive(Clone, Debug, Error, PartialEq, Eq, Serialize)]
|
||||
pub enum ValidationError {
|
||||
/// Unclosed Tag
|
||||
///
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
djls-conf = { workspace = true }
|
||||
djls-macros = { workspace = true }
|
||||
djls-source = { workspace = true }
|
||||
djls-workspace = { workspace = true }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
use djls_macros::Diagnostic;
|
||||
use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::parser::ParseError;
|
||||
|
||||
#[derive(Clone, Debug, Error, Diagnostic, PartialEq, Eq, Serialize)]
|
||||
#[derive(Clone, Debug, Error, PartialEq, Eq, Serialize)]
|
||||
pub enum TemplateError {
|
||||
/// Parser Error
|
||||
///
|
||||
|
|
|
|||
|
|
@ -70,13 +70,12 @@ def main():
|
|||
print("\n" + "="*70)
|
||||
print("✓ All diagnostic attributes verified!")
|
||||
print("="*70)
|
||||
print("\nThe #[derive(Diagnostic)] proc macro will:")
|
||||
print(" 1. Generate diagnostic_code() method implementations")
|
||||
print(" 2. Provide compile-time validation of attributes")
|
||||
print("\nThe build.rs script will:")
|
||||
print(" 1. Parse these Rust files with syn")
|
||||
print(" 2. Extract diagnostic codes and doc comments")
|
||||
print(" 3. Generate markdown docs in docs/rules/*.md")
|
||||
print("\nThe diagnostic codes are manually implemented in:")
|
||||
print(" - crates/djls-ide/src/diagnostics.rs")
|
||||
print("\nTo test the full build, run: cargo build -p djls-ide")
|
||||
|
||||
return 0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue