Skip to content

Commit 1ceac47

Browse files
fix template analysis after semantic crate refactor (#225)
1 parent 6919f2e commit 1ceac47

File tree

11 files changed

+247
-188
lines changed

11 files changed

+247
-188
lines changed

crates/djls-semantic/src/db.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,27 @@ use std::sync::Arc;
22

33
use djls_templates::Db as TemplateDb;
44
use djls_workspace::Db as WorkspaceDb;
5+
use tower_lsp_server::lsp_types;
56

67
use crate::specs::TagSpecs;
78

8-
/// Semantic database trait extending the template and workspace databases
99
#[salsa::db]
1010
pub trait SemanticDb: TemplateDb + WorkspaceDb {
1111
/// Get the Django tag specifications for semantic analysis
1212
fn tag_specs(&self) -> Arc<TagSpecs>;
1313
}
14+
15+
#[salsa::accumulator]
16+
pub struct SemanticDiagnostic(pub lsp_types::Diagnostic);
17+
18+
impl From<SemanticDiagnostic> for lsp_types::Diagnostic {
19+
fn from(diagnostic: SemanticDiagnostic) -> Self {
20+
diagnostic.0
21+
}
22+
}
23+
24+
impl From<&SemanticDiagnostic> for lsp_types::Diagnostic {
25+
fn from(diagnostic: &SemanticDiagnostic) -> Self {
26+
diagnostic.0.clone()
27+
}
28+
}

crates/djls-semantic/src/lib.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ pub mod validation;
66

77
pub use builtins::django_builtin_specs;
88
pub use db::SemanticDb;
9+
pub use db::SemanticDiagnostic;
10+
use salsa::Accumulator;
911
pub use snippets::generate_partial_snippet;
1012
pub use snippets::generate_snippet_for_tag;
1113
pub use snippets::generate_snippet_for_tag_with_end;
@@ -17,6 +19,7 @@ pub use specs::SimpleArgType;
1719
pub use specs::TagArg;
1820
pub use specs::TagSpec;
1921
pub use specs::TagSpecs;
22+
use tower_lsp_server::lsp_types;
2023
pub use validation::TagValidator;
2124

2225
pub enum TagType {
@@ -40,3 +43,46 @@ impl TagType {
4043
}
4144
}
4245
}
46+
47+
/// Validate a Django template node list and return validation errors.
48+
///
49+
/// This function runs the TagValidator on the parsed node list to check for:
50+
/// - Unclosed block tags
51+
/// - Mismatched tag pairs
52+
/// - Orphaned intermediate tags
53+
/// - Invalid argument counts
54+
/// - Unmatched block names
55+
#[salsa::tracked]
56+
pub fn validate_nodelist(db: &dyn SemanticDb, nodelist: djls_templates::NodeList<'_>) {
57+
if nodelist.nodelist(db).is_empty() {
58+
return;
59+
}
60+
61+
let validation_errors = TagValidator::new(db, nodelist).validate();
62+
63+
let line_offsets = nodelist.line_offsets(db);
64+
for error in validation_errors {
65+
let code = error.diagnostic_code();
66+
let range = error
67+
.span()
68+
.map(|(start, length)| {
69+
let span = djls_templates::nodelist::Span::new(start, length);
70+
span.to_lsp_range(line_offsets)
71+
})
72+
.unwrap_or_default();
73+
74+
let diagnostic = lsp_types::Diagnostic {
75+
range,
76+
severity: Some(lsp_types::DiagnosticSeverity::ERROR),
77+
code: Some(lsp_types::NumberOrString::String(code.to_string())),
78+
code_description: None,
79+
source: Some("Django Language Server".to_string()),
80+
message: error.to_string(),
81+
related_information: None,
82+
tags: None,
83+
data: None,
84+
};
85+
86+
SemanticDiagnostic(diagnostic).accumulate(db);
87+
}
88+
}

crates/djls-semantic/src/validation.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515
//! ## Architecture
1616
//!
1717
//! The `TagValidator` follows the same pattern as the Parser and Lexer,
18-
//! maintaining minimal state and walking through the AST to accumulate errors.
18+
//! maintaining minimal state and walking through the node list to accumulate errors.
1919
20-
use djls_templates::ast::Node;
21-
use djls_templates::ast::NodeListError;
22-
use djls_templates::ast::Span;
23-
use djls_templates::ast::TagBit;
24-
use djls_templates::ast::TagName;
20+
use djls_templates::nodelist::Node;
21+
use djls_templates::nodelist::NodeListError;
22+
use djls_templates::nodelist::Span;
23+
use djls_templates::nodelist::TagBit;
24+
use djls_templates::nodelist::TagName;
2525
use djls_templates::NodeList;
2626

2727
use crate::db::SemanticDb;

crates/djls-server/src/server.rs

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ use std::future::Future;
22
use std::sync::Arc;
33

44
use djls_project::Db as ProjectDb;
5-
use djls_templates::analyze_template;
5+
use djls_semantic::validate_nodelist;
6+
use djls_semantic::SemanticDiagnostic;
7+
use djls_templates::parse_template;
68
use djls_templates::TemplateDiagnostic;
79
use djls_workspace::paths;
810
use djls_workspace::FileKind;
@@ -96,14 +98,26 @@ impl DjangoLanguageServer {
9698
let file = session.get_or_create_file(&path);
9799

98100
session.with_db(|db| {
99-
// Parse and validate the template (triggers accumulation)
100-
// This should be a cheap call since salsa should cache the function
101-
// call, but we may need to revisit if that assumption is incorrect
102-
let _ast = analyze_template(db, file);
101+
let Some(nodelist) = parse_template(db, file) else {
102+
// If parsing failed completely, just return syntax errors
103+
return parse_template::accumulated::<TemplateDiagnostic>(db, file)
104+
.into_iter()
105+
.map(Into::into)
106+
.collect();
107+
};
108+
109+
validate_nodelist(db, nodelist);
103110

104-
let diagnostics = analyze_template::accumulated::<TemplateDiagnostic>(db, file);
111+
let syntax_diagnostics =
112+
parse_template::accumulated::<TemplateDiagnostic>(db, file);
113+
let semantic_diagnostics =
114+
validate_nodelist::accumulated::<SemanticDiagnostic>(db, nodelist);
105115

106-
diagnostics.into_iter().map(Into::into).collect()
116+
syntax_diagnostics
117+
.into_iter()
118+
.map(Into::into)
119+
.chain(semantic_diagnostics.into_iter().map(Into::into))
120+
.collect()
107121
})
108122
})
109123
.await;
@@ -240,7 +254,6 @@ impl LanguageServer for DjangoLanguageServer {
240254
})
241255
.await;
242256

243-
// Publish diagnostics for template files
244257
if let Some((url, version)) = url_version {
245258
self.publish_diagnostics(&url, Some(version)).await;
246259
}
@@ -262,7 +275,6 @@ impl LanguageServer for DjangoLanguageServer {
262275
})
263276
.await;
264277

265-
// Publish diagnostics for template files
266278
if let Some((url, version)) = url_version {
267279
self.publish_diagnostics(&url, version).await;
268280
}
@@ -426,14 +438,25 @@ impl LanguageServer for DjangoLanguageServer {
426438
return vec![];
427439
};
428440

429-
// Parse and validate the template (triggers accumulation)
430-
let _ast = analyze_template(db, file);
441+
let Some(nodelist) = parse_template(db, file) else {
442+
return parse_template::accumulated::<TemplateDiagnostic>(db, file)
443+
.into_iter()
444+
.map(Into::into)
445+
.collect();
446+
};
447+
448+
validate_nodelist(db, nodelist);
431449

432-
// Get accumulated diagnostics directly - they're already LSP diagnostics!
433-
let diagnostics = analyze_template::accumulated::<TemplateDiagnostic>(db, file);
450+
let syntax_diagnostics =
451+
parse_template::accumulated::<TemplateDiagnostic>(db, file);
452+
let semantic_diagnostics =
453+
validate_nodelist::accumulated::<SemanticDiagnostic>(db, nodelist);
434454

435-
// Convert from TemplateDiagnostic wrapper to lsp_types::Diagnostic
436-
diagnostics.into_iter().map(Into::into).collect()
455+
syntax_diagnostics
456+
.into_iter()
457+
.map(Into::into)
458+
.chain(semantic_diagnostics.into_iter().map(Into::into))
459+
.collect()
437460
})
438461
})
439462
.await;

crates/djls-templates/src/db.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,30 @@
1414
//! ## Key Components
1515
//!
1616
//! - [`Db`]: Database trait extending the workspace database
17-
//! - [`analyze_template`]: Main entry point for template analysis
17+
//! - [`parse_template`]: Main entry point for template parsing
1818
//! - [`TemplateDiagnostic`]: Accumulator for collecting LSP diagnostics
1919
//!
2020
//! ## Incremental Computation
2121
//!
2222
//! When a template file changes:
2323
//! 1. Salsa invalidates the cached AST for that file
24-
//! 2. Next access to `analyze_template` triggers reparse
24+
//! 2. Next access to `parse_template` triggers reparse
2525
//! 3. Diagnostics are accumulated during parse/validation
2626
//! 4. Other files remain cached unless they also changed
2727
//!
2828
//! ## Example
2929
//!
3030
//! ```ignore
31-
//! // Analyze a template and get its AST
32-
//! let ast = analyze_template(db, file);
31+
//! // Parse a template and get its AST
32+
//! let nodelist = parse_template(db, file);
3333
//!
3434
//! // Retrieve accumulated diagnostics
35-
//! let diagnostics = analyze_template::accumulated::<TemplateDiagnostic>(db, file);
35+
//! let diagnostics = parse_template::accumulated::<TemplateDiagnostic>(db, file);
3636
//!
3737
//! // Get diagnostics for all workspace files
3838
//! for file in workspace.files() {
39-
//! let _ = analyze_template(db, file); // Trigger analysis
40-
//! let diags = analyze_template::accumulated::<TemplateDiagnostic>(db, file);
39+
//! let _ = parse_template(db, file); // Trigger parsing
40+
//! let diags = parse_template::accumulated::<TemplateDiagnostic>(db, file);
4141
//! // Process diagnostics...
4242
//! }
4343
//! ```

crates/djls-templates/src/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use serde::Serialize;
22
use thiserror::Error;
33

4-
use crate::ast::NodeListError;
4+
use crate::nodelist::NodeListError;
55
use crate::parser::ParserError;
66

77
#[derive(Clone, Debug, Error, PartialEq, Eq, Serialize)]

crates/djls-templates/src/lexer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
use crate::ast::LineOffsets;
21
use crate::db::Db as TemplateDb;
2+
use crate::nodelist::LineOffsets;
33
use crate::tokens::Token;
44
use crate::tokens::TokenContent;
55

0 commit comments

Comments
 (0)