diff --git a/crates/djls-ide/src/completions.rs b/crates/djls-ide/src/completions.rs index c462342b..32e5989a 100644 --- a/crates/djls-ide/src/completions.rs +++ b/crates/djls-ide/src/completions.rs @@ -4,8 +4,7 @@ //! and generating appropriate completion items for Django templates. use djls_project::TemplateTags; -use djls_semantic::ArgType; -use djls_semantic::SimpleArgType; +use djls_semantic::TagArg; use djls_semantic::TagSpecs; use djls_workspace::FileKind; use djls_workspace::PositionEncoding; @@ -410,15 +409,15 @@ fn generate_tag_name_completions( } completions.push(lsp_types::CompletionItem { - label: end_tag.name.clone(), + label: end_tag.name.to_string(), kind: Some(lsp_types::CompletionItemKind::KEYWORD), detail: Some(format!("End tag for {opener_name}")), text_edit: Some(tower_lsp_server::lsp_types::CompletionTextEdit::Edit( lsp_types::TextEdit::new(replacement_range, insert_text.clone()), )), insert_text_format: Some(lsp_types::InsertTextFormat::PLAIN_TEXT), - filter_text: Some(end_tag.name.clone()), - sort_text: Some(format!("0_{}", end_tag.name)), // Priority sort + filter_text: Some(end_tag.name.to_string()), + sort_text: Some(format!("0_{}", end_tag.name.as_ref())), // Priority sort ..Default::default() }); } @@ -534,11 +533,11 @@ fn generate_argument_completions( let arg = &spec.args[position]; let mut completions = Vec::new(); - match &arg.arg_type { - ArgType::Simple(SimpleArgType::Literal) => { + match arg { + TagArg::Literal { lit, .. } => { // For literals, complete the exact text - if arg.name.starts_with(partial) { - let mut insert_text = arg.name.clone(); + if lit.starts_with(partial) { + let mut insert_text = lit.to_string(); // Add closing if needed match closing { @@ -547,7 +546,7 @@ fn generate_argument_completions( } completions.push(lsp_types::CompletionItem { - label: arg.name.clone(), + label: lit.to_string(), kind: Some(lsp_types::CompletionItemKind::KEYWORD), detail: Some("literal argument".to_string()), insert_text: Some(insert_text), @@ -556,11 +555,11 @@ fn generate_argument_completions( }); } } - ArgType::Choice { choice } => { + TagArg::Choice { name, choices, .. } => { // For choices, offer each option - for option in choice { + for option in choices.iter() { if option.starts_with(partial) { - let mut insert_text = option.clone(); + let mut insert_text = option.to_string(); // Add closing if needed match closing { @@ -570,9 +569,9 @@ fn generate_argument_completions( } completions.push(lsp_types::CompletionItem { - label: option.clone(), + label: option.to_string(), kind: Some(lsp_types::CompletionItemKind::ENUM_MEMBER), - detail: Some(format!("choice for {}", arg.name)), + detail: Some(format!("choice for {}", name.as_ref())), insert_text: Some(insert_text), insert_text_format: Some(lsp_types::InsertTextFormat::PLAIN_TEXT), ..Default::default() @@ -580,12 +579,12 @@ fn generate_argument_completions( } } } - ArgType::Simple(SimpleArgType::Variable) => { + TagArg::Var { name, .. } => { // For variables, we could offer variable completions from context // For now, just provide a hint if partial.is_empty() { completions.push(lsp_types::CompletionItem { - label: format!("<{}>", arg.name), + label: format!("<{}>", name.as_ref()), kind: Some(lsp_types::CompletionItemKind::VARIABLE), detail: Some("variable argument".to_string()), insert_text: None, // Don't insert placeholder @@ -594,12 +593,12 @@ fn generate_argument_completions( }); } } - ArgType::Simple(SimpleArgType::String) => { + TagArg::String { name, .. } => { // For strings, could offer template name completions // For now, just provide a hint if partial.is_empty() { completions.push(lsp_types::CompletionItem { - label: format!("\"{}\"", arg.name), + label: format!("\"{}\"", name.as_ref()), kind: Some(lsp_types::CompletionItemKind::TEXT), detail: Some("string argument".to_string()), insert_text: None, // Don't insert placeholder @@ -608,8 +607,8 @@ fn generate_argument_completions( }); } } - ArgType::Simple(_) => { - // Other argument types not handled yet + _ => { + // Other argument types (Expr, Assignment, VarArgs) not handled yet } } diff --git a/crates/djls-ide/src/snippets.rs b/crates/djls-ide/src/snippets.rs index f167f0b3..103a0c0a 100644 --- a/crates/djls-ide/src/snippets.rs +++ b/crates/djls-ide/src/snippets.rs @@ -1,7 +1,5 @@ -use djls_semantic::specs::ArgType; -use djls_semantic::specs::SimpleArgType; -use djls_semantic::specs::TagArg; -use djls_semantic::specs::TagSpec; +use djls_semantic::TagArg; +use djls_semantic::TagSpec; /// Generate an LSP snippet pattern from an array of arguments #[must_use] @@ -12,50 +10,49 @@ pub fn generate_snippet_from_args(args: &[TagArg]) -> String { for arg in args { // Skip optional literals entirely - they're usually flags like "reversed" or "only" // that the user can add manually if needed - if !arg.required && matches!(&arg.arg_type, ArgType::Simple(SimpleArgType::Literal)) { + if !arg.is_required() && matches!(arg, TagArg::Literal { .. }) { continue; } // Skip other optional args if we haven't seen any required args yet // This prevents generating snippets like: "{% for %}" when everything is optional - if !arg.required && parts.is_empty() { + if !arg.is_required() && parts.is_empty() { continue; } - let snippet_part = match &arg.arg_type { - ArgType::Simple(simple_type) => match simple_type { - SimpleArgType::Literal => { - // At this point, we know it's required (optional literals were skipped above) - arg.name.clone() - } - SimpleArgType::Variable | SimpleArgType::Expression => { - // Variables and expressions become placeholders - let result = format!("${{{}:{}}}", placeholder_index, arg.name); - placeholder_index += 1; - result - } - SimpleArgType::String => { - // Strings get quotes around them - let result = format!("\"${{{}:{}}}\"", placeholder_index, arg.name); - placeholder_index += 1; - result - } - SimpleArgType::Assignment => { - // Assignments use the name as-is (e.g., "var=value") - let result = format!("${{{}:{}}}", placeholder_index, arg.name); - placeholder_index += 1; - result - } - SimpleArgType::VarArgs => { - // Variable arguments, just use the name - let result = format!("${{{}:{}}}", placeholder_index, arg.name); - placeholder_index += 1; - result - } - }, - ArgType::Choice { choice } => { + let snippet_part = match arg { + TagArg::Literal { lit, .. } => { + // At this point, we know it's required (optional literals were skipped above) + lit.to_string() + } + TagArg::Var { name, .. } | TagArg::Expr { name, .. } => { + // Variables and expressions become placeholders + let result = format!("${{{}:{}}}", placeholder_index, name.as_ref()); + placeholder_index += 1; + result + } + TagArg::String { name, .. } => { + // Strings get quotes around them + let result = format!("\"${{{}:{}}}\"", placeholder_index, name.as_ref()); + placeholder_index += 1; + result + } + TagArg::Assignment { name, .. } => { + // Assignments use the name as-is (e.g., "var=value") + let result = format!("${{{}:{}}}", placeholder_index, name.as_ref()); + placeholder_index += 1; + result + } + TagArg::VarArgs { name, .. } => { + // Variable arguments, just use the name + let result = format!("${{{}:{}}}", placeholder_index, name.as_ref()); + placeholder_index += 1; + result + } + TagArg::Choice { choices, .. } => { // Choice placeholders with options - let result = format!("${{{}|{}|}}", placeholder_index, choice.join(",")); + let options: Vec<_> = choices.iter().map(std::convert::AsRef::as_ref).collect(); + let result = format!("${{{}|{}|}}", placeholder_index, options.join(",")); placeholder_index += 1; result } @@ -121,32 +118,28 @@ pub fn generate_partial_snippet(spec: &TagSpec, starting_from_position: usize) - #[cfg(test)] mod tests { - use djls_semantic::specs::EndTag; + use djls_semantic::EndTag; use super::*; #[test] fn test_snippet_for_for_tag() { let args = vec![ - TagArg { - name: "item".to_string(), + TagArg::Var { + name: "item".into(), required: true, - arg_type: ArgType::Simple(SimpleArgType::Variable), }, - TagArg { - name: "in".to_string(), + TagArg::Literal { + lit: "in".into(), required: true, - arg_type: ArgType::Simple(SimpleArgType::Literal), }, - TagArg { - name: "items".to_string(), + TagArg::Var { + name: "items".into(), required: true, - arg_type: ArgType::Simple(SimpleArgType::Variable), }, - TagArg { - name: "reversed".to_string(), + TagArg::Literal { + lit: "reversed".into(), required: false, - arg_type: ArgType::Simple(SimpleArgType::Literal), }, ]; @@ -156,10 +149,9 @@ mod tests { #[test] fn test_snippet_for_if_tag() { - let args = vec![TagArg { - name: "condition".to_string(), + let args = vec![TagArg::Expr { + name: "condition".into(), required: true, - arg_type: ArgType::Simple(SimpleArgType::Expression), }]; let snippet = generate_snippet_from_args(&args); @@ -168,12 +160,10 @@ mod tests { #[test] fn test_snippet_for_autoescape_tag() { - let args = vec![TagArg { - name: "mode".to_string(), + let args = vec![TagArg::Choice { + name: "mode".into(), required: true, - arg_type: ArgType::Choice { - choice: vec!["on".to_string(), "off".to_string()], - }, + choices: vec!["on".into(), "off".into()].into(), }]; let snippet = generate_snippet_from_args(&args); @@ -182,10 +172,9 @@ mod tests { #[test] fn test_snippet_for_extends_tag() { - let args = vec![TagArg { - name: "template".to_string(), + let args = vec![TagArg::String { + name: "template".into(), required: true, - arg_type: ArgType::Simple(SimpleArgType::String), }]; let snippet = generate_snippet_from_args(&args); @@ -202,23 +191,25 @@ mod tests { #[test] fn test_snippet_for_block_tag() { + use std::borrow::Cow; + let spec = TagSpec { - name: None, + module: "django.template.loader_tags".into(), end_tag: Some(EndTag { - name: "endblock".to_string(), + name: "endblock".into(), optional: false, - args: vec![TagArg { - name: "name".to_string(), + args: vec![TagArg::Var { + name: "name".into(), required: false, - arg_type: ArgType::Simple(SimpleArgType::Variable), - }], + }] + .into(), }), - intermediate_tags: None, - args: vec![TagArg { - name: "name".to_string(), + intermediate_tags: Cow::Borrowed(&[]), + args: vec![TagArg::Var { + name: "name".into(), required: true, - arg_type: ArgType::Simple(SimpleArgType::Variable), - }], + }] + .into(), }; let snippet = generate_snippet_for_tag_with_end("block", &spec); @@ -227,21 +218,22 @@ mod tests { #[test] fn test_snippet_with_end_tag() { + use std::borrow::Cow; + let spec = TagSpec { - name: None, + module: "django.template.defaulttags".into(), end_tag: Some(EndTag { - name: "endautoescape".to_string(), + name: "endautoescape".into(), optional: false, - args: vec![], + args: Cow::Borrowed(&[]), }), - intermediate_tags: None, - args: vec![TagArg { - name: "mode".to_string(), + intermediate_tags: Cow::Borrowed(&[]), + args: vec![TagArg::Choice { + name: "mode".into(), required: true, - arg_type: ArgType::Choice { - choice: vec!["on".to_string(), "off".to_string()], - }, - }], + choices: vec!["on".into(), "off".into()].into(), + }] + .into(), }; let snippet = generate_snippet_for_tag_with_end("autoescape", &spec); @@ -254,25 +246,21 @@ mod tests { #[test] fn test_snippet_for_url_tag() { let args = vec![ - TagArg { - name: "view_name".to_string(), + TagArg::String { + name: "view_name".into(), required: true, - arg_type: ArgType::Simple(SimpleArgType::String), }, - TagArg { - name: "args".to_string(), + TagArg::VarArgs { + name: "args".into(), required: false, - arg_type: ArgType::Simple(SimpleArgType::VarArgs), }, - TagArg { - name: "as".to_string(), + TagArg::Literal { + lit: "as".into(), required: false, - arg_type: ArgType::Simple(SimpleArgType::Literal), }, - TagArg { - name: "varname".to_string(), + TagArg::Var { + name: "varname".into(), required: false, - arg_type: ArgType::Simple(SimpleArgType::Variable), }, ]; diff --git a/crates/djls-semantic/src/builtins.rs b/crates/djls-semantic/src/builtins.rs deleted file mode 100644 index b55da1d5..00000000 --- a/crates/djls-semantic/src/builtins.rs +++ /dev/null @@ -1,556 +0,0 @@ -//! Built-in Django template tag specifications. -//! -//! This module defines all the standard Django template tags as compile-time -//! constants, avoiding the need for runtime TOML parsing. - -use std::sync::LazyLock; - -use rustc_hash::FxHashMap; - -use super::specs::EndTag; -use super::specs::IntermediateTag; -use super::specs::TagArg; -use super::specs::TagSpec; -use super::specs::TagSpecs; - -// Static storage for built-in specs - built only once on first access -static BUILTIN_SPECS: LazyLock = LazyLock::new(|| { - let mut specs = FxHashMap::default(); - - // Define all Django built-in tags using direct struct construction - let tags = vec![ - // Control flow tags - TagSpec { - name: Some("autoescape".to_string()), - end_tag: Some(EndTag { - name: "endautoescape".to_string(), - optional: false, - args: vec![], - }), - intermediate_tags: None, - args: vec![TagArg::choice( - "mode", - true, - vec!["on".to_string(), "off".to_string()], - )], - }, - TagSpec { - name: Some("if".to_string()), - end_tag: Some(EndTag { - name: "endif".to_string(), - optional: false, - args: vec![], - }), - intermediate_tags: Some(vec![ - IntermediateTag { - name: "elif".to_string(), - args: vec![TagArg::expr("condition", true)], - }, - IntermediateTag { - name: "else".to_string(), - args: vec![], - }, - ]), - args: vec![TagArg::expr("condition", true)], - }, - TagSpec { - name: Some("for".to_string()), - end_tag: Some(EndTag { - name: "endfor".to_string(), - optional: false, - args: vec![], - }), - intermediate_tags: Some(vec![IntermediateTag { - name: "empty".to_string(), - args: vec![], - }]), - args: vec![ - TagArg::var("item", true), - TagArg::literal("in", true), - TagArg::var("items", true), - TagArg::literal("reversed", false), - ], - }, - TagSpec { - name: Some("ifchanged".to_string()), - end_tag: Some(EndTag { - name: "endifchanged".to_string(), - optional: false, - args: vec![], - }), - intermediate_tags: Some(vec![IntermediateTag { - name: "else".to_string(), - args: vec![], - }]), - args: vec![TagArg::varargs("variables", false)], - }, - TagSpec { - name: Some("with".to_string()), - end_tag: Some(EndTag { - name: "endwith".to_string(), - optional: false, - args: vec![], - }), - intermediate_tags: None, - args: vec![TagArg::varargs("assignments", true)], - }, - // Block tags - TagSpec { - name: Some("block".to_string()), - end_tag: Some(EndTag { - name: "endblock".to_string(), - optional: false, - args: vec![TagArg::var("name", false)], - }), - intermediate_tags: None, - args: vec![TagArg::var("name", true)], - }, - TagSpec { - name: Some("extends".to_string()), - end_tag: None, - intermediate_tags: None, - args: vec![TagArg::string("template", true)], - }, - TagSpec { - name: Some("include".to_string()), - end_tag: None, - intermediate_tags: None, - args: vec![ - TagArg::string("template", true), - TagArg::literal("with", false), - TagArg::varargs("context", false), - TagArg::literal("only", false), - ], - }, - TagSpec { - name: Some("load".to_string()), - end_tag: None, - intermediate_tags: None, - args: vec![TagArg::varargs("libraries", true)], - }, - // Content manipulation tags - TagSpec { - name: Some("comment".to_string()), - end_tag: Some(EndTag { - name: "endcomment".to_string(), - optional: false, - args: vec![], - }), - intermediate_tags: None, - args: vec![TagArg::string("note", false)], - }, - TagSpec { - name: Some("filter".to_string()), - end_tag: Some(EndTag { - name: "endfilter".to_string(), - optional: false, - args: vec![], - }), - intermediate_tags: None, - args: vec![TagArg::varargs("filters", true)], - }, - TagSpec { - name: Some("spaceless".to_string()), - end_tag: Some(EndTag { - name: "endspaceless".to_string(), - optional: false, - args: vec![], - }), - intermediate_tags: None, - args: vec![], - }, - TagSpec { - name: Some("verbatim".to_string()), - end_tag: Some(EndTag { - name: "endverbatim".to_string(), - optional: false, - args: vec![], - }), - intermediate_tags: None, - args: vec![TagArg::string("name", false)], - }, - // Variables and expressions - TagSpec { - name: Some("cycle".to_string()), - end_tag: None, - intermediate_tags: None, - args: vec![ - TagArg::varargs("values", true), - TagArg::literal("as", false), - TagArg::var("varname", false), - TagArg::literal("silent", false), - ], - }, - TagSpec { - name: Some("firstof".to_string()), - end_tag: None, - intermediate_tags: None, - args: vec![ - TagArg::varargs("variables", true), - TagArg::string("fallback", false), - TagArg::literal("as", false), - TagArg::var("varname", false), - ], - }, - TagSpec { - name: Some("regroup".to_string()), - end_tag: None, - intermediate_tags: None, - args: vec![ - TagArg::var("target", true), - TagArg::literal("by", true), - TagArg::var("attribute", true), - TagArg::literal("as", true), - TagArg::var("grouped", true), - ], - }, - // Date and time - TagSpec { - name: Some("now".to_string()), - end_tag: None, - intermediate_tags: None, - args: vec![ - TagArg::string("format_string", true), - TagArg::literal("as", false), - TagArg::var("varname", false), - ], - }, - // URLs and static files - TagSpec { - name: Some("url".to_string()), - end_tag: None, - intermediate_tags: None, - args: vec![ - TagArg::string("view_name", true), - TagArg::varargs("args", false), - TagArg::literal("as", false), - TagArg::var("varname", false), - ], - }, - TagSpec { - name: Some("static".to_string()), - end_tag: None, - intermediate_tags: None, - args: vec![TagArg::string("path", true)], - }, - // Template tags - TagSpec { - name: Some("templatetag".to_string()), - end_tag: None, - intermediate_tags: None, - args: vec![TagArg::choice( - "tagbit", - true, - vec![ - "openblock".to_string(), - "closeblock".to_string(), - "openvariable".to_string(), - "closevariable".to_string(), - "openbrace".to_string(), - "closebrace".to_string(), - "opencomment".to_string(), - "closecomment".to_string(), - ], - )], - }, - // Security - TagSpec { - name: Some("csrf_token".to_string()), - end_tag: None, - intermediate_tags: None, - args: vec![], - }, - // Utilities - TagSpec { - name: Some("widthratio".to_string()), - end_tag: None, - intermediate_tags: None, - args: vec![ - TagArg::var("this_value", true), - TagArg::var("max_value", true), - TagArg::var("max_width", true), - TagArg::literal("as", false), - TagArg::var("varname", false), - ], - }, - TagSpec { - name: Some("lorem".to_string()), - end_tag: None, - intermediate_tags: None, - args: vec![ - TagArg::var("count", false), - TagArg::choice( - "method", - false, - vec!["w".to_string(), "p".to_string(), "b".to_string()], - ), - TagArg::literal("random", false), - ], - }, - TagSpec { - name: Some("debug".to_string()), - end_tag: None, - intermediate_tags: None, - args: vec![], - }, - // Cache tags - TagSpec { - name: Some("cache".to_string()), - end_tag: Some(EndTag { - name: "endcache".to_string(), - optional: false, - args: vec![], - }), - intermediate_tags: None, - args: vec![ - TagArg::var("timeout", true), - TagArg::var("cache_key", true), - TagArg::varargs("variables", false), - ], - }, - // Internationalization - TagSpec { - name: Some("localize".to_string()), - end_tag: Some(EndTag { - name: "endlocalize".to_string(), - optional: false, - args: vec![], - }), - intermediate_tags: None, - args: vec![TagArg::choice( - "mode", - false, - vec!["on".to_string(), "off".to_string()], - )], - }, - TagSpec { - name: Some("blocktranslate".to_string()), - end_tag: Some(EndTag { - name: "endblocktranslate".to_string(), - optional: false, - args: vec![], - }), - intermediate_tags: Some(vec![IntermediateTag { - name: "plural".to_string(), - args: vec![TagArg::var("count", false)], - }]), - args: vec![ - TagArg::string("context", false), - TagArg::literal("with", false), - TagArg::varargs("assignments", false), - TagArg::literal("asvar", false), - TagArg::var("varname", false), - ], - }, - TagSpec { - name: Some("trans".to_string()), - end_tag: None, - intermediate_tags: None, - args: vec![ - TagArg::string("message", true), - TagArg::string("context", false), - TagArg::literal("as", false), - TagArg::var("varname", false), - TagArg::literal("noop", false), - ], - }, - // Timezone tags - TagSpec { - name: Some("localtime".to_string()), - end_tag: Some(EndTag { - name: "endlocaltime".to_string(), - optional: false, - args: vec![], - }), - intermediate_tags: None, - args: vec![TagArg::choice( - "mode", - false, - vec!["on".to_string(), "off".to_string()], - )], - }, - TagSpec { - name: Some("timezone".to_string()), - end_tag: Some(EndTag { - name: "endtimezone".to_string(), - optional: false, - args: vec![], - }), - intermediate_tags: None, - args: vec![TagArg::var("timezone", true)], - }, - ]; - - // Insert all tags into the FxHashMap - for tag in tags { - if let Some(ref name) = tag.name { - specs.insert(name.clone(), tag); - } - } - - TagSpecs::new(specs) -}); - -/// Returns all built-in Django template tag specifications -/// -/// This function returns a clone of the statically initialized built-in specs. -/// The actual specs are only built once on first access and then cached. -#[must_use] -pub fn django_builtin_specs() -> TagSpecs { - BUILTIN_SPECS.clone() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_builtin_specs_non_empty() { - let specs = django_builtin_specs(); - - // Verify we have specs loaded - assert!( - specs.iter().count() > 0, - "Should have loaded at least one spec" - ); - - // Check a key tag is present as a smoke test - assert!(specs.get("if").is_some(), "'if' tag should be present"); - - // Verify all tag names are non-empty - for (name, _) in specs.iter() { - assert!(!name.is_empty(), "Tag name should not be empty"); - } - } - - #[test] - fn test_all_expected_tags_present() { - let specs = django_builtin_specs(); - - // Block tags that should be present - let expected_block_tags = [ - "autoescape", - "block", - "comment", - "filter", - "for", - "if", - "ifchanged", - "spaceless", - "verbatim", - "with", - "cache", - "localize", - "blocktranslate", - "localtime", - "timezone", - ]; - - // Single tags that should be present - let expected_single_tags = [ - "csrf_token", - "cycle", - "extends", - "include", - "load", - "now", - "templatetag", - "url", - "debug", - "firstof", - "lorem", - "regroup", - "widthratio", - "trans", - "static", - ]; - - for tag in expected_block_tags { - let spec = specs - .get(tag) - .unwrap_or_else(|| panic!("{tag} tag should be present")); - assert!(spec.end_tag.is_some(), "{tag} should have an end tag"); - } - - for tag in expected_single_tags { - assert!(specs.get(tag).is_some(), "{tag} tag should be present"); - } - - // Tags that should NOT be present yet (future Django versions) - let missing_tags = [ - "querystring", // Django 5.1+ - "resetcycle", - ]; - - for tag in missing_tags { - assert!( - specs.get(tag).is_none(), - "{tag} tag should not be present yet" - ); - } - } - - #[test] - fn test_if_tag_structure() { - let specs = django_builtin_specs(); - let if_tag = specs.get("if").expect("if tag should exist"); - - assert_eq!(if_tag.name, Some("if".to_string())); - assert!(if_tag.end_tag.is_some()); - assert_eq!(if_tag.end_tag.as_ref().unwrap().name, "endif"); - - let intermediates = if_tag.intermediate_tags.as_ref().unwrap(); - assert_eq!(intermediates.len(), 2); - assert_eq!(intermediates[0].name, "elif"); - assert_eq!(intermediates[1].name, "else"); - } - - #[test] - fn test_for_tag_structure() { - let specs = django_builtin_specs(); - let for_tag = specs.get("for").expect("for tag should exist"); - - assert_eq!(for_tag.name, Some("for".to_string())); - assert!(for_tag.end_tag.is_some()); - assert_eq!(for_tag.end_tag.as_ref().unwrap().name, "endfor"); - - let intermediates = for_tag.intermediate_tags.as_ref().unwrap(); - assert_eq!(intermediates.len(), 1); - assert_eq!(intermediates[0].name, "empty"); - - // Check args structure - assert!(!for_tag.args.is_empty(), "for tag should have arguments"); - } - - #[test] - fn test_block_tag_with_end_args() { - let specs = django_builtin_specs(); - let block_tag = specs.get("block").expect("block tag should exist"); - - let end_tag = block_tag.end_tag.as_ref().unwrap(); - assert_eq!(end_tag.name, "endblock"); - assert_eq!(end_tag.args.len(), 1); - assert_eq!(end_tag.args[0].name, "name"); - assert!(!end_tag.args[0].required); - } - - #[test] - fn test_single_tag_structure() { - let specs = django_builtin_specs(); - - // Test a single tag has no end tag or intermediates - let csrf_tag = specs - .get("csrf_token") - .expect("csrf_token tag should exist"); - assert!(csrf_tag.end_tag.is_none()); - assert!(csrf_tag.intermediate_tags.is_none()); - - // Test extends tag with args - let extends_tag = specs.get("extends").expect("extends tag should exist"); - assert!(extends_tag.end_tag.is_none()); - assert!( - !extends_tag.args.is_empty(), - "extends tag should have arguments" - ); - } -} diff --git a/crates/djls-semantic/src/db.rs b/crates/djls-semantic/src/db.rs index be4090e3..3c8005ed 100644 --- a/crates/djls-semantic/src/db.rs +++ b/crates/djls-semantic/src/db.rs @@ -4,7 +4,7 @@ use djls_templates::Db as TemplateDb; use djls_workspace::Db as WorkspaceDb; use crate::errors::ValidationError; -use crate::specs::TagSpecs; +use crate::templatetags::TagSpecs; #[salsa::db] pub trait Db: TemplateDb + WorkspaceDb { diff --git a/crates/djls-semantic/src/lib.rs b/crates/djls-semantic/src/lib.rs index 89688153..04b513f2 100644 --- a/crates/djls-semantic/src/lib.rs +++ b/crates/djls-semantic/src/lib.rs @@ -1,21 +1,16 @@ -pub mod builtins; -pub mod db; -pub mod errors; -pub mod specs; -pub mod validation; +mod db; +mod errors; +mod templatetags; +mod validation; -pub use builtins::django_builtin_specs; pub use db::Db; pub use db::ValidationErrorAccumulator; pub use errors::ValidationError; -pub use specs::ArgType; -pub use specs::EndTag; -pub use specs::IntermediateTag; -pub use specs::SimpleArgType; -pub use specs::TagArg; -pub use specs::TagSpec; -pub use specs::TagSpecs; -pub use validation::TagValidator; +pub use templatetags::django_builtin_specs; +pub use templatetags::EndTag; +pub use templatetags::TagArg; +pub use templatetags::TagSpec; +pub use templatetags::TagSpecs; /// Validate a Django template node list and return validation errors. /// @@ -31,5 +26,5 @@ pub fn validate_nodelist(db: &dyn Db, nodelist: djls_templates::NodeList<'_>) { return; } - TagValidator::new(db, nodelist).validate(); + validation::TagValidator::new(db, nodelist).validate(); } diff --git a/crates/djls-semantic/src/templatetags.rs b/crates/djls-semantic/src/templatetags.rs new file mode 100644 index 00000000..05bce44e --- /dev/null +++ b/crates/djls-semantic/src/templatetags.rs @@ -0,0 +1,9 @@ +mod builtins; +mod specs; + +pub use builtins::django_builtin_specs; +pub use specs::EndTag; +pub use specs::TagArg; +pub use specs::TagSpec; +pub use specs::TagSpecs; +pub use specs::TagType; diff --git a/crates/djls-semantic/src/templatetags/builtins.rs b/crates/djls-semantic/src/templatetags/builtins.rs new file mode 100644 index 00000000..26906957 --- /dev/null +++ b/crates/djls-semantic/src/templatetags/builtins.rs @@ -0,0 +1,871 @@ +//! Built-in Django template tag specifications. +//! +//! This module defines all the standard Django template tags as compile-time +//! constants, avoiding the need for runtime TOML parsing. + +use std::borrow::Cow::Borrowed as B; +use std::sync::LazyLock; + +use rustc_hash::FxHashMap; + +use super::specs::EndTag; +use super::specs::IntermediateTag; +use super::specs::TagArg; +use super::specs::TagSpec; +use super::specs::TagSpecs; + +const DEFAULTTAGS_MOD: &str = "django.template.defaulttags"; +static DEFAULTTAGS_PAIRS: &[(&str, &TagSpec)] = &[ + ( + "autoescape", + &TagSpec { + module: B(DEFAULTTAGS_MOD), + end_tag: Some(EndTag { + name: B("endautoescape"), + optional: false, + args: B(&[]), + }), + intermediate_tags: B(&[]), + args: B(&[TagArg::Choice { + name: B("mode"), + required: true, + choices: B(&[B("on"), B("off")]), + }]), + }, + ), + ( + "comment", + &TagSpec { + module: B(DEFAULTTAGS_MOD), + end_tag: Some(EndTag { + name: B("endcomment"), + optional: false, + args: B(&[]), + }), + intermediate_tags: B(&[]), + args: B(&[TagArg::String { + name: B("note"), + required: false, + }]), + }, + ), + ( + "csrf_token", + &TagSpec { + module: B(DEFAULTTAGS_MOD), + end_tag: None, + intermediate_tags: B(&[]), + args: B(&[]), + }, + ), + ( + "cycle", + &TagSpec { + module: B(DEFAULTTAGS_MOD), + end_tag: None, + intermediate_tags: B(&[]), + args: B(&[ + TagArg::VarArgs { + name: B("values"), + required: true, + }, + TagArg::Literal { + lit: B("as"), + required: false, + }, + TagArg::Var { + name: B("varname"), + required: false, + }, + TagArg::Literal { + lit: B("silent"), + required: false, + }, + ]), + }, + ), + ( + "debug", + &TagSpec { + module: B(DEFAULTTAGS_MOD), + end_tag: None, + intermediate_tags: B(&[]), + args: B(&[]), + }, + ), + ( + "filter", + &TagSpec { + module: B(DEFAULTTAGS_MOD), + end_tag: Some(EndTag { + name: B("endfilter"), + optional: false, + args: B(&[]), + }), + intermediate_tags: B(&[]), + args: B(&[TagArg::VarArgs { + name: B("filters"), + required: true, + }]), + }, + ), + ( + "firstof", + &TagSpec { + module: B(DEFAULTTAGS_MOD), + end_tag: None, + intermediate_tags: B(&[]), + args: B(&[ + TagArg::VarArgs { + name: B("variables"), + required: true, + }, + TagArg::String { + name: B("fallback"), + required: false, + }, + TagArg::Literal { + lit: B("as"), + required: false, + }, + TagArg::Var { + name: B("varname"), + required: false, + }, + ]), + }, + ), + ( + "for", + &TagSpec { + module: B(DEFAULTTAGS_MOD), + end_tag: Some(EndTag { + name: B("endfor"), + optional: false, + args: B(&[]), + }), + intermediate_tags: B(&[IntermediateTag { + name: B("empty"), + args: B(&[]), + }]), + args: B(&[ + TagArg::Var { + name: B("item"), + required: true, + }, + TagArg::Literal { + lit: B("in"), + required: true, + }, + TagArg::Var { + name: B("items"), + required: true, + }, + TagArg::Literal { + lit: B("reversed"), + required: false, + }, + ]), + }, + ), + ( + "if", + &TagSpec { + module: B(DEFAULTTAGS_MOD), + end_tag: Some(EndTag { + name: B("endif"), + optional: false, + args: B(&[]), + }), + intermediate_tags: B(&[ + IntermediateTag { + name: B("elif"), + args: B(&[TagArg::Expr { + name: B("condition"), + required: true, + }]), + }, + IntermediateTag { + name: B("else"), + args: B(&[]), + }, + ]), + args: B(&[TagArg::Expr { + name: B("condition"), + required: true, + }]), + }, + ), + ( + "ifchanged", + &TagSpec { + module: B(DEFAULTTAGS_MOD), + end_tag: Some(EndTag { + name: B("endifchanged"), + optional: false, + args: B(&[]), + }), + intermediate_tags: B(&[IntermediateTag { + name: B("else"), + args: B(&[]), + }]), + args: B(&[TagArg::VarArgs { + name: B("variables"), + required: false, + }]), + }, + ), + ( + "load", + &TagSpec { + module: B(DEFAULTTAGS_MOD), + end_tag: None, + intermediate_tags: B(&[]), + args: B(&[TagArg::VarArgs { + name: B("libraries"), + required: true, + }]), + }, + ), + ( + "lorem", + &TagSpec { + module: B(DEFAULTTAGS_MOD), + end_tag: None, + intermediate_tags: B(&[]), + args: B(&[ + TagArg::Var { + name: B("count"), + required: false, + }, + TagArg::Choice { + name: B("method"), + required: false, + choices: B(&[B("w"), B("p"), B("b")]), + }, + TagArg::Literal { + lit: B("random"), + required: false, + }, + ]), + }, + ), + ( + "now", + &TagSpec { + module: B(DEFAULTTAGS_MOD), + end_tag: None, + intermediate_tags: B(&[]), + args: B(&[ + TagArg::String { + name: B("format_string"), + required: true, + }, + TagArg::Literal { + lit: B("as"), + required: false, + }, + TagArg::Var { + name: B("varname"), + required: false, + }, + ]), + }, + ), + // TODO: PARTIALDEF_SPEC, 6.0+ + // TODO: PARTIAL_SPEC, 6.0+ + // TODO: QUERYSTRING_SPEC, 5.1+ + ( + "regroup", + &TagSpec { + module: B(DEFAULTTAGS_MOD), + end_tag: None, + intermediate_tags: B(&[]), + args: B(&[ + TagArg::Var { + name: B("target"), + required: true, + }, + TagArg::Literal { + lit: B("by"), + required: true, + }, + TagArg::Var { + name: B("attribute"), + required: true, + }, + TagArg::Literal { + lit: B("as"), + required: true, + }, + TagArg::Var { + name: B("grouped"), + required: true, + }, + ]), + }, + ), + // TODO: RESETCYCLE_SPEC? + ( + "spaceless", + &TagSpec { + module: B(DEFAULTTAGS_MOD), + end_tag: Some(EndTag { + name: B("endspaceless"), + optional: false, + args: B(&[]), + }), + intermediate_tags: B(&[]), + args: B(&[]), + }, + ), + ( + "templatetag", + &TagSpec { + module: B(DEFAULTTAGS_MOD), + end_tag: None, + intermediate_tags: B(&[]), + args: B(&[TagArg::Choice { + name: B("tagbit"), + required: true, + choices: B(&[ + B("openblock"), + B("closeblock"), + B("openvariable"), + B("closevariable"), + B("openbrace"), + B("closebrace"), + B("opencomment"), + B("closecomment"), + ]), + }]), + }, + ), + ( + "url", + &TagSpec { + module: B(DEFAULTTAGS_MOD), + end_tag: None, + intermediate_tags: B(&[]), + args: B(&[ + TagArg::String { + name: B("view_name"), + required: true, + }, + TagArg::VarArgs { + name: B("args"), + required: false, + }, + TagArg::Literal { + lit: B("as"), + required: false, + }, + TagArg::Var { + name: B("varname"), + required: false, + }, + ]), + }, + ), + ( + "verbatim", + &TagSpec { + module: B(DEFAULTTAGS_MOD), + end_tag: Some(EndTag { + name: B("endverbatim"), + optional: false, + args: B(&[]), + }), + intermediate_tags: B(&[]), + args: B(&[TagArg::String { + name: B("name"), + required: false, + }]), + }, + ), + ( + "widthratio", + &TagSpec { + module: B(DEFAULTTAGS_MOD), + end_tag: None, + intermediate_tags: B(&[]), + args: B(&[ + TagArg::Var { + name: B("this_value"), + required: true, + }, + TagArg::Var { + name: B("max_value"), + required: true, + }, + TagArg::Var { + name: B("max_width"), + required: true, + }, + TagArg::Literal { + lit: B("as"), + required: false, + }, + TagArg::Var { + name: B("varname"), + required: false, + }, + ]), + }, + ), + ( + "with", + &TagSpec { + module: B(DEFAULTTAGS_MOD), + end_tag: Some(EndTag { + name: B("endwith"), + optional: false, + args: B(&[]), + }), + intermediate_tags: B(&[]), + args: B(&[TagArg::VarArgs { + name: B("assignments"), + required: true, + }]), + }, + ), +]; + +const MOD_LOADER_TAGS: &str = "django.template.loader_tags"; +static LOADER_TAGS_PAIRS: &[(&str, &TagSpec)] = &[ + ( + "block", + &TagSpec { + module: B(MOD_LOADER_TAGS), + end_tag: Some(EndTag { + name: B("endblock"), + optional: false, + args: B(&[TagArg::Var { + name: B("name"), + required: false, + }]), + }), + intermediate_tags: B(&[]), + args: B(&[TagArg::Var { + name: B("name"), + required: true, + }]), + }, + ), + ( + "extends", + &TagSpec { + module: B(MOD_LOADER_TAGS), + end_tag: None, + intermediate_tags: B(&[]), + args: B(&[TagArg::String { + name: B("template"), + required: true, + }]), + }, + ), + ( + "include", + &TagSpec { + module: B(MOD_LOADER_TAGS), + end_tag: None, + intermediate_tags: B(&[]), + args: B(&[ + TagArg::String { + name: B("template"), + required: true, + }, + TagArg::Literal { + lit: B("with"), + required: false, + }, + TagArg::VarArgs { + name: B("context"), + required: false, + }, + TagArg::Literal { + lit: B("only"), + required: false, + }, + ]), + }, + ), +]; + +const CACHE_MOD: &str = "django.templatetags.cache"; +static CACHE_PAIRS: &[(&str, &TagSpec)] = &[( + "cache", + &TagSpec { + module: B(CACHE_MOD), + end_tag: Some(EndTag { + name: B("endcache"), + optional: false, + args: B(&[]), + }), + intermediate_tags: B(&[]), + args: B(&[ + TagArg::Var { + name: B("timeout"), + required: true, + }, + TagArg::Var { + name: B("cache_key"), + required: true, + }, + TagArg::VarArgs { + name: B("variables"), + required: false, + }, + ]), + }, +)]; + +const I18N_MOD: &str = "django.templatetags.i18n"; +static I18N_PAIRS: &[(&str, &TagSpec)] = &[ + ( + "blocktrans", + &TagSpec { + module: B(I18N_MOD), + end_tag: Some(EndTag { + name: B("endblocktrans"), + optional: false, + args: B(&[]), + }), + intermediate_tags: B(BLOCKTRANS_INTERMEDIATE_TAGS), + args: B(BLOCKTRANS_ARGS), + }, + ), + ( + "blocktranslate", + &TagSpec { + module: B(I18N_MOD), + end_tag: Some(EndTag { + name: B("endblocktranslate"), + optional: false, + args: B(&[]), + }), + intermediate_tags: B(BLOCKTRANS_INTERMEDIATE_TAGS), + args: B(BLOCKTRANS_ARGS), + }, + ), + // TODO: GET_AVAILABLE_LANGAUGES_SPEC + // TODO: GET_CURRENT_LANGUAGE_SPEC + // TODO: GET_CURRENT_LANGUAGE_BIDI_SPEC + // TODO: GET_LANGUAGE_INFO_SPEC + // TODO: GET_LANGUAGE_INFO_LIST_SPEC + // TODO: LANGUAGE_SPEC + ("trans", &TRANS_SPEC), + ("translate", &TRANS_SPEC), +]; +const BLOCKTRANS_INTERMEDIATE_TAGS: &[IntermediateTag] = &[IntermediateTag { + name: B("plural"), + args: B(&[TagArg::Var { + name: B("count"), + required: false, + }]), +}]; +const BLOCKTRANS_ARGS: &[TagArg] = &[ + TagArg::String { + name: B("context"), + required: false, + }, + TagArg::Literal { + lit: B("with"), + required: false, + }, + TagArg::VarArgs { + name: B("assignments"), + required: false, + }, + TagArg::Literal { + lit: B("asvar"), + required: false, + }, + TagArg::Var { + name: B("varname"), + required: false, + }, +]; +const TRANS_SPEC: TagSpec = TagSpec { + module: B(I18N_MOD), + end_tag: None, + intermediate_tags: B(&[]), + args: B(&[ + TagArg::String { + name: B("message"), + required: true, + }, + TagArg::String { + name: B("context"), + required: false, + }, + TagArg::Literal { + lit: B("as"), + required: false, + }, + TagArg::Var { + name: B("varname"), + required: false, + }, + TagArg::Literal { + lit: B("noop"), + required: false, + }, + ]), +}; + +const L10N_MOD: &str = "django.templatetags.l10n"; +static L10N_PAIRS: &[(&str, &TagSpec)] = &[( + "localize", + &TagSpec { + module: B(L10N_MOD), + end_tag: Some(EndTag { + name: B("endlocalize"), + optional: false, + args: B(&[]), + }), + intermediate_tags: B(&[]), + args: B(&[TagArg::Choice { + name: B("mode"), + required: false, + choices: B(&[B("on"), B("off")]), + }]), + }, +)]; + +const STATIC_MOD: &str = "django.templatetags.static"; +static STATIC_PAIRS: &[(&str, &TagSpec)] = &[ + // TODO: GET_MEDIA_PREFIX_SPEC + // TODO: GET_STATIC_PREFIX_SPEC + ( + "static", + &TagSpec { + module: B(STATIC_MOD), + end_tag: None, + intermediate_tags: B(&[]), + args: B(&[TagArg::String { + name: B("path"), + required: true, + }]), + }, + ), +]; + +const TZ_MOD: &str = "django.templatetags.tz"; +static TZ_PAIRS: &[(&str, &TagSpec)] = &[ + // TODO: GET_CURRENT_TIMEZONE_SPEC + ( + "localtime", + &TagSpec { + module: B(TZ_MOD), + end_tag: Some(EndTag { + name: B("endlocaltime"), + optional: false, + args: B(&[]), + }), + intermediate_tags: B(&[]), + args: B(&[TagArg::Choice { + name: B("mode"), + required: false, + choices: B(&[B("on"), B("off")]), + }]), + }, + ), + ( + "timezone", + &TagSpec { + module: B(TZ_MOD), + end_tag: Some(EndTag { + name: B("endtimezone"), + optional: false, + args: B(&[]), + }), + intermediate_tags: B(&[]), + args: B(&[TagArg::Var { + name: B("timezone"), + required: true, + }]), + }, + ), +]; + +static BUILTIN_SPECS: LazyLock = LazyLock::new(|| { + let mut specs = FxHashMap::default(); + + let all_pairs = DEFAULTTAGS_PAIRS + .iter() + .chain(LOADER_TAGS_PAIRS.iter()) + .chain(STATIC_PAIRS.iter()) + .chain(CACHE_PAIRS.iter()) + .chain(I18N_PAIRS.iter()) + .chain(L10N_PAIRS.iter()) + .chain(TZ_PAIRS.iter()); + + for (name, spec) in all_pairs { + specs.insert((*name).to_string(), (*spec).clone()); + } + + TagSpecs::new(specs) +}); + +/// Returns all built-in Django template tag specifications +/// +/// This function returns a clone of the statically initialized built-in specs. +/// The actual specs are only built once on first access and then cached. +#[must_use] +pub fn django_builtin_specs() -> TagSpecs { + BUILTIN_SPECS.clone() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_builtin_specs_non_empty() { + let specs = django_builtin_specs(); + + // Verify we have specs loaded + assert!( + specs.iter().count() > 0, + "Should have loaded at least one spec" + ); + + // Check a key tag is present as a smoke test + assert!(specs.get("if").is_some(), "'if' tag should be present"); + + // Verify all tag names are non-empty + for (name, _) in specs.iter() { + assert!(!name.is_empty(), "Tag name should not be empty"); + } + } + + #[test] + fn test_all_expected_tags_present() { + let specs = django_builtin_specs(); + + // Block tags that should be present + let expected_block_tags = [ + "autoescape", + "block", + "comment", + "filter", + "for", + "if", + "ifchanged", + "spaceless", + "verbatim", + "with", + "cache", + "localize", + "blocktranslate", + "localtime", + "timezone", + ]; + + // Single tags that should be present + let expected_single_tags = [ + "csrf_token", + "cycle", + "extends", + "include", + "load", + "now", + "templatetag", + "url", + "debug", + "firstof", + "lorem", + "regroup", + "widthratio", + "trans", + "static", + ]; + + for tag in expected_block_tags { + let spec = specs + .get(tag) + .unwrap_or_else(|| panic!("{tag} tag should be present")); + assert!(spec.end_tag.is_some(), "{tag} should have an end tag"); + } + + for tag in expected_single_tags { + assert!(specs.get(tag).is_some(), "{tag} tag should be present"); + } + + // Tags that should NOT be present yet (future Django versions) + let missing_tags = [ + "querystring", // Django 5.1+ + "resetcycle", + ]; + + for tag in missing_tags { + assert!( + specs.get(tag).is_none(), + "{tag} tag should not be present yet" + ); + } + } + + #[test] + fn test_if_tag_structure() { + let specs = django_builtin_specs(); + let if_tag = specs.get("if").expect("if tag should exist"); + + assert!(if_tag.end_tag.is_some()); + assert_eq!(if_tag.end_tag.as_ref().unwrap().name.as_ref(), "endif"); + + let intermediates = &if_tag.intermediate_tags; + assert_eq!(intermediates.len(), 2); + assert_eq!(intermediates[0].name.as_ref(), "elif"); + assert_eq!(intermediates[1].name.as_ref(), "else"); + } + + #[test] + fn test_for_tag_structure() { + let specs = django_builtin_specs(); + let for_tag = specs.get("for").expect("for tag should exist"); + + assert!(for_tag.end_tag.is_some()); + assert_eq!(for_tag.end_tag.as_ref().unwrap().name.as_ref(), "endfor"); + + let intermediates = &for_tag.intermediate_tags; + assert_eq!(intermediates.len(), 1); + assert_eq!(intermediates[0].name.as_ref(), "empty"); + + // Check args structure + assert!(!for_tag.args.is_empty(), "for tag should have arguments"); + } + + #[test] + fn test_block_tag_with_end_args() { + let specs = django_builtin_specs(); + let block_tag = specs.get("block").expect("block tag should exist"); + + let end_tag = block_tag.end_tag.as_ref().unwrap(); + assert_eq!(end_tag.name.as_ref(), "endblock"); + assert_eq!(end_tag.args.len(), 1); + assert_eq!(end_tag.args[0].name().as_ref(), "name"); + assert!(!end_tag.args[0].is_required()); + } + + #[test] + fn test_single_tag_structure() { + let specs = django_builtin_specs(); + + // Test a single tag has no end tag or intermediates + let csrf_tag = specs + .get("csrf_token") + .expect("csrf_token tag should exist"); + assert!(csrf_tag.end_tag.is_none()); + assert!(csrf_tag.intermediate_tags.is_empty()); + + // Test extends tag with args + let extends_tag = specs.get("extends").expect("extends tag should exist"); + assert!(extends_tag.end_tag.is_none()); + assert!( + !extends_tag.args.is_empty(), + "extends tag should have arguments" + ); + } +} diff --git a/crates/djls-semantic/src/specs.rs b/crates/djls-semantic/src/templatetags/specs.rs similarity index 64% rename from crates/djls-semantic/src/specs.rs rename to crates/djls-semantic/src/templatetags/specs.rs index 8bce230d..57c3e2c8 100644 --- a/crates/djls-semantic/src/specs.rs +++ b/crates/djls-semantic/src/templatetags/specs.rs @@ -1,5 +1,10 @@ +use std::borrow::Cow; + use rustc_hash::FxHashMap; +pub type S = Cow<'static, T>; +pub type L = Cow<'static, [T]>; + pub enum TagType { Opener, Intermediate, @@ -46,7 +51,7 @@ impl TagSpecs { pub fn find_opener_for_closer(&self, closer: &str) -> Option { for (tag_name, spec) in &self.0 { if let Some(end_spec) = &spec.end_tag { - if end_spec.name == closer { + if end_spec.name.as_ref() == closer { return Some(tag_name.clone()); } } @@ -59,7 +64,7 @@ impl TagSpecs { pub fn get_end_spec_for_closer(&self, closer: &str) -> Option<&EndTag> { for spec in self.0.values() { if let Some(end_spec) = &spec.end_tag { - if end_spec.name == closer { + if end_spec.name.as_ref() == closer { return Some(end_spec); } } @@ -79,10 +84,8 @@ impl TagSpecs { pub fn is_intermediate(&self, name: &str) -> bool { self.0.values().any(|spec| { spec.intermediate_tags - .as_ref() - .is_some_and(|intermediate_tags| { - intermediate_tags.iter().any(|tag| tag.name == name) - }) + .iter() + .any(|tag| tag.name.as_ref() == name) }) } @@ -91,7 +94,7 @@ impl TagSpecs { self.0.values().any(|spec| { spec.end_tag .as_ref() - .is_some_and(|end_tag| end_tag.name == name) + .is_some_and(|end_tag| end_tag.name.as_ref() == name) }) } @@ -100,10 +103,12 @@ impl TagSpecs { pub fn get_parent_tags_for_intermediate(&self, intermediate: &str) -> Vec { let mut parents = Vec::new(); for (opener_name, spec) in &self.0 { - if let Some(intermediate_tags) = &spec.intermediate_tags { - if intermediate_tags.iter().any(|tag| tag.name == intermediate) { - parents.push(opener_name.clone()); - } + if spec + .intermediate_tags + .iter() + .any(|tag| tag.name.as_ref() == intermediate) + { + parents.push(opener_name.clone()); } } parents @@ -119,16 +124,15 @@ impl TagSpecs { impl From<&djls_conf::Settings> for TagSpecs { fn from(settings: &djls_conf::Settings) -> Self { // Start with built-in specs - let mut specs = crate::builtins::django_builtin_specs(); + let mut specs = crate::templatetags::django_builtin_specs(); // Convert and merge user-defined tagspecs let mut user_specs = FxHashMap::default(); for tagspec_def in settings.tagspecs() { // Clone because we're consuming the tagspec_def in the conversion + let name = tagspec_def.name.clone(); let tagspec: TagSpec = tagspec_def.clone().into(); - if let Some(name) = &tagspec.name { - user_specs.insert(name.clone(), tagspec); - } + user_specs.insert(name, tagspec); } // Merge user specs into built-in specs (user specs override built-ins) @@ -142,166 +146,224 @@ impl From<&djls_conf::Settings> for TagSpecs { #[derive(Debug, Clone, PartialEq)] pub struct TagSpec { - pub name: Option, + pub module: S, pub end_tag: Option, - pub intermediate_tags: Option>, - pub args: Vec, + pub intermediate_tags: L, + pub args: L, } impl From for TagSpec { fn from(value: djls_conf::TagSpecDef) -> Self { TagSpec { - name: Some(value.name), + module: value.module.into(), end_tag: value.end_tag.map(Into::into), - intermediate_tags: if value.intermediate_tags.is_empty() { - None - } else { - Some( - value - .intermediate_tags - .into_iter() - .map(Into::into) - .collect(), - ) - }, - args: value.args.into_iter().map(Into::into).collect(), + intermediate_tags: value + .intermediate_tags + .into_iter() + .map(Into::into) + .collect::>() + .into(), + args: value + .args + .into_iter() + .map(Into::into) + .collect::>() + .into(), } } } #[derive(Debug, Clone, PartialEq)] -pub struct TagArg { - pub name: String, - pub required: bool, - pub arg_type: ArgType, +pub enum TagArg { + Var { + name: S, + required: bool, + }, + String { + name: S, + required: bool, + }, + Literal { + lit: S, + required: bool, + }, + Expr { + name: S, + required: bool, + }, + Assignment { + name: S, + required: bool, + }, + VarArgs { + name: S, + required: bool, + }, + Choice { + name: S, + required: bool, + choices: L, + }, } impl TagArg { - pub fn choice(name: impl Into, required: bool, choices: Vec) -> Self { - Self { - name: name.into(), - required, - arg_type: ArgType::Choice { choice: choices }, + #[must_use] + pub fn name(&self) -> &S { + match self { + Self::Var { name, .. } + | Self::String { name, .. } + | Self::Expr { name, .. } + | Self::Assignment { name, .. } + | Self::VarArgs { name, .. } + | Self::Choice { name, .. } => name, + Self::Literal { lit, .. } => lit, + } + } + + #[must_use] + pub fn is_required(&self) -> bool { + match self { + Self::Var { required, .. } + | Self::String { required, .. } + | Self::Literal { required, .. } + | Self::Expr { required, .. } + | Self::Assignment { required, .. } + | Self::VarArgs { required, .. } + | Self::Choice { required, .. } => *required, } } - pub fn expr(name: impl Into, required: bool) -> Self { - Self { + pub fn choice(name: impl Into, required: bool, choices: impl Into>) -> Self { + Self::Choice { name: name.into(), required, - arg_type: ArgType::Simple(SimpleArgType::Expression), + choices: choices.into(), } } - pub fn literal(name: impl Into, required: bool) -> Self { - Self { + pub fn expr(name: impl Into, required: bool) -> Self { + Self::Expr { name: name.into(), required, - arg_type: ArgType::Simple(SimpleArgType::Literal), } } - pub fn string(name: impl Into, required: bool) -> Self { - Self { - name: name.into(), + pub fn literal(lit: impl Into, required: bool) -> Self { + Self::Literal { + lit: lit.into(), required, - arg_type: ArgType::Simple(SimpleArgType::String), } } - pub fn var(name: impl Into, required: bool) -> Self { - Self { + pub fn string(name: impl Into, required: bool) -> Self { + Self::String { name: name.into(), required, - arg_type: ArgType::Simple(SimpleArgType::Variable), } } - pub fn varargs(name: impl Into, required: bool) -> Self { - Self { + pub fn var(name: impl Into, required: bool) -> Self { + Self::Var { name: name.into(), required, - arg_type: ArgType::Simple(SimpleArgType::VarArgs), } } -} -impl From for TagArg { - fn from(value: djls_conf::TagArgDef) -> Self { - TagArg { - name: value.name, - required: value.required, - arg_type: value.arg_type.into(), + pub fn varargs(name: impl Into, required: bool) -> Self { + Self::VarArgs { + name: name.into(), + required, } } -} - -#[derive(Debug, Clone, PartialEq)] -pub enum ArgType { - Simple(SimpleArgType), - Choice { choice: Vec }, -} -impl From for ArgType { - fn from(value: djls_conf::ArgTypeDef) -> Self { - match value { - djls_conf::ArgTypeDef::Simple(simple) => ArgType::Simple(simple.into()), - djls_conf::ArgTypeDef::Choice { choice } => ArgType::Choice { choice }, + pub fn assignment(name: impl Into, required: bool) -> Self { + Self::Assignment { + name: name.into(), + required, } } } -#[derive(Debug, Clone, PartialEq)] -pub enum SimpleArgType { - Literal, - Variable, - String, - Expression, - Assignment, - VarArgs, -} - -impl From for SimpleArgType { - fn from(value: djls_conf::SimpleArgTypeDef) -> Self { - match value { - djls_conf::SimpleArgTypeDef::Literal => SimpleArgType::Literal, - djls_conf::SimpleArgTypeDef::Variable => SimpleArgType::Variable, - djls_conf::SimpleArgTypeDef::String => SimpleArgType::String, - djls_conf::SimpleArgTypeDef::Expression => SimpleArgType::Expression, - djls_conf::SimpleArgTypeDef::Assignment => SimpleArgType::Assignment, - djls_conf::SimpleArgTypeDef::VarArgs => SimpleArgType::VarArgs, +impl From for TagArg { + fn from(value: djls_conf::TagArgDef) -> Self { + match value.arg_type { + djls_conf::ArgTypeDef::Simple(simple) => match simple { + djls_conf::SimpleArgTypeDef::Literal => TagArg::Literal { + lit: value.name.into(), + required: value.required, + }, + djls_conf::SimpleArgTypeDef::Variable => TagArg::Var { + name: value.name.into(), + required: value.required, + }, + djls_conf::SimpleArgTypeDef::String => TagArg::String { + name: value.name.into(), + required: value.required, + }, + djls_conf::SimpleArgTypeDef::Expression => TagArg::Expr { + name: value.name.into(), + required: value.required, + }, + djls_conf::SimpleArgTypeDef::Assignment => TagArg::Assignment { + name: value.name.into(), + required: value.required, + }, + djls_conf::SimpleArgTypeDef::VarArgs => TagArg::VarArgs { + name: value.name.into(), + required: value.required, + }, + }, + djls_conf::ArgTypeDef::Choice { choice } => TagArg::Choice { + name: value.name.into(), + required: value.required, + choices: choice + .into_iter() + .map(Into::into) + .collect::>() + .into(), + }, } } } #[derive(Debug, Clone, PartialEq)] pub struct EndTag { - pub name: String, + pub name: S, pub optional: bool, - pub args: Vec, + pub args: L, } impl From for EndTag { fn from(value: djls_conf::EndTagDef) -> Self { EndTag { - name: value.name, + name: value.name.into(), optional: value.optional, - args: value.args.into_iter().map(Into::into).collect(), + args: value + .args + .into_iter() + .map(Into::into) + .collect::>() + .into(), } } } #[derive(Debug, Clone, PartialEq)] pub struct IntermediateTag { - pub name: String, - pub args: Vec, + pub name: S, + pub args: L, } impl From for IntermediateTag { fn from(value: djls_conf::IntermediateTagDef) -> Self { IntermediateTag { - name: value.name, - args: value.args.into_iter().map(Into::into).collect(), + name: value.name.into(), + args: value + .args + .into_iter() + .map(Into::into) + .collect::>() + .into(), } } } @@ -318,10 +380,10 @@ mod tests { specs.insert( "csrf_token".to_string(), TagSpec { - name: Some("csrf_token".to_string()), + module: "django.template.defaulttags".into(), end_tag: None, - intermediate_tags: None, - args: vec![], + intermediate_tags: Cow::Borrowed(&[]), + args: Cow::Borrowed(&[]), }, ); @@ -329,23 +391,23 @@ mod tests { specs.insert( "if".to_string(), TagSpec { - name: Some("if".to_string()), + module: "django.template.defaulttags".into(), end_tag: Some(EndTag { - name: "endif".to_string(), + name: "endif".into(), optional: false, - args: vec![], + args: Cow::Borrowed(&[]), }), - intermediate_tags: Some(vec![ + intermediate_tags: Cow::Owned(vec![ IntermediateTag { - name: "elif".to_string(), - args: vec![TagArg::expr("condition", true)], + name: "elif".into(), + args: Cow::Owned(vec![TagArg::expr("condition", true)]), }, IntermediateTag { - name: "else".to_string(), - args: vec![], + name: "else".into(), + args: Cow::Borrowed(&[]), }, ]), - args: vec![], + args: Cow::Borrowed(&[]), }, ); @@ -353,23 +415,23 @@ mod tests { specs.insert( "for".to_string(), TagSpec { - name: Some("for".to_string()), + module: "django.template.defaulttags".into(), end_tag: Some(EndTag { - name: "endfor".to_string(), + name: "endfor".into(), optional: false, - args: vec![], + args: Cow::Borrowed(&[]), }), - intermediate_tags: Some(vec![ + intermediate_tags: Cow::Owned(vec![ IntermediateTag { - name: "empty".to_string(), - args: vec![], + name: "empty".into(), + args: Cow::Borrowed(&[]), }, IntermediateTag { - name: "else".to_string(), - args: vec![], + name: "else".into(), + args: Cow::Borrowed(&[]), }, // Note: else is shared ]), - args: vec![], + args: Cow::Borrowed(&[]), }, ); @@ -377,18 +439,17 @@ mod tests { specs.insert( "block".to_string(), TagSpec { - name: Some("block".to_string()), + module: "django.template.loader_tags".into(), end_tag: Some(EndTag { - name: "endblock".to_string(), + name: "endblock".into(), optional: false, - args: vec![TagArg { - name: "name".to_string(), + args: Cow::Owned(vec![TagArg::Var { + name: "name".into(), required: false, - arg_type: ArgType::Simple(SimpleArgType::Variable), - }], + }]), }), - intermediate_tags: None, - args: vec![], + intermediate_tags: Cow::Borrowed(&[]), + args: Cow::Borrowed(&[]), }, ); @@ -408,9 +469,9 @@ mod tests { // Test get with non-existing key assert!(specs.get("nonexistent").is_none()); - // Verify the content is correct + // Verify the content is correct - if tag should have an end tag let if_spec = specs.get("if").unwrap(); - assert_eq!(if_spec.name, Some("if".to_string())); + assert!(if_spec.end_tag.is_some()); } #[test] @@ -462,14 +523,14 @@ mod tests { let specs = create_test_specs(); let endif_spec = specs.get_end_spec_for_closer("endif").unwrap(); - assert_eq!(endif_spec.name, "endif"); + assert_eq!(endif_spec.name.as_ref(), "endif"); assert!(!endif_spec.optional); assert_eq!(endif_spec.args.len(), 0); let endblock_spec = specs.get_end_spec_for_closer("endblock").unwrap(); - assert_eq!(endblock_spec.name, "endblock"); + assert_eq!(endblock_spec.name.as_ref(), "endblock"); assert_eq!(endblock_spec.args.len(), 1); - assert_eq!(endblock_spec.args[0].name, "name"); + assert_eq!(endblock_spec.args[0].name().as_ref(), "name"); assert!(specs.get_end_spec_for_closer("endnonexistent").is_none()); } @@ -573,10 +634,10 @@ mod tests { specs2_map.insert( "custom".to_string(), TagSpec { - name: Some("custom".to_string()), + module: "custom.module".into(), end_tag: None, - intermediate_tags: None, - args: vec![], + intermediate_tags: Cow::Borrowed(&[]), + args: Cow::Borrowed(&[]), }, ); @@ -584,14 +645,14 @@ mod tests { specs2_map.insert( "if".to_string(), TagSpec { - name: Some("if".to_string()), + module: "django.template.defaulttags".into(), end_tag: Some(EndTag { - name: "endif".to_string(), + name: "endif".into(), optional: true, // Changed to optional - args: vec![], + args: Cow::Borrowed(&[]), }), - intermediate_tags: None, // Removed intermediates - args: vec![], + intermediate_tags: Cow::Borrowed(&[]), // Removed intermediates + args: Cow::Borrowed(&[]), }, ); @@ -609,7 +670,7 @@ mod tests { // Check that existing tag was overwritten let if_spec = specs1.get("if").unwrap(); assert!(if_spec.end_tag.as_ref().unwrap().optional); // Should be optional now - assert!(if_spec.intermediate_tags.is_none()); // Should have no intermediates + assert!(if_spec.intermediate_tags.is_empty()); // Should have no intermediates // Check that unaffected tags remain assert!(specs1.get("for").is_some()); @@ -634,45 +695,44 @@ mod tests { #[test] fn test_conversion_from_conf_types() { - // Test SimpleArgTypeDef -> SimpleArgType conversion - assert_eq!( - SimpleArgType::from(djls_conf::SimpleArgTypeDef::Variable), - SimpleArgType::Variable - ); - assert_eq!( - SimpleArgType::from(djls_conf::SimpleArgTypeDef::Literal), - SimpleArgType::Literal - ); - - // Test ArgTypeDef -> ArgType conversion - let simple_arg = djls_conf::ArgTypeDef::Simple(djls_conf::SimpleArgTypeDef::String); + // Test TagArgDef -> TagArg conversion for different arg types + let string_arg_def = djls_conf::TagArgDef { + name: "test".to_string(), + required: true, + arg_type: djls_conf::ArgTypeDef::Simple(djls_conf::SimpleArgTypeDef::String), + }; assert!(matches!( - ArgType::from(simple_arg), - ArgType::Simple(SimpleArgType::String) + TagArg::from(string_arg_def), + TagArg::String { .. } )); - let choice_arg = djls_conf::ArgTypeDef::Choice { - choice: vec!["on".to_string(), "off".to_string()], + let choice_arg_def = djls_conf::TagArgDef { + name: "mode".to_string(), + required: false, + arg_type: djls_conf::ArgTypeDef::Choice { + choice: vec!["on".to_string(), "off".to_string()], + }, }; - if let ArgType::Choice { choice } = ArgType::from(choice_arg) { - assert_eq!(choice, vec!["on".to_string(), "off".to_string()]); + if let TagArg::Choice { choices, .. } = TagArg::from(choice_arg_def) { + assert_eq!(choices.len(), 2); + assert_eq!(choices[0].as_ref(), "on"); + assert_eq!(choices[1].as_ref(), "off"); } else { panic!("Expected Choice variant"); } - // Test TagArgDef -> Arg conversion + // Test TagArgDef -> TagArg conversion for Variable type let tag_arg_def = djls_conf::TagArgDef { name: "test_arg".to_string(), required: true, arg_type: djls_conf::ArgTypeDef::Simple(djls_conf::SimpleArgTypeDef::Variable), }; let arg = TagArg::from(tag_arg_def); - assert_eq!(arg.name, "test_arg"); - assert!(arg.required); - assert!(matches!( - arg.arg_type, - ArgType::Simple(SimpleArgType::Variable) - )); + assert!(matches!(arg, TagArg::Var { .. })); + if let TagArg::Var { name, required } = arg { + assert_eq!(name.as_ref(), "test_arg"); + assert!(required); + } // Test EndTagDef -> EndTag conversion let end_tag_def = djls_conf::EndTagDef { @@ -681,7 +741,7 @@ mod tests { args: vec![], }; let end_tag = EndTag::from(end_tag_def); - assert_eq!(end_tag.name, "endtest"); + assert_eq!(end_tag.name.as_ref(), "endtest"); assert!(end_tag.optional); assert_eq!(end_tag.args.len(), 0); @@ -695,9 +755,9 @@ mod tests { }], }; let intermediate = IntermediateTag::from(intermediate_def); - assert_eq!(intermediate.name, "elif"); + assert_eq!(intermediate.name.as_ref(), "elif"); assert_eq!(intermediate.args.len(), 1); - assert_eq!(intermediate.args[0].name, "condition"); + assert_eq!(intermediate.args[0].name().as_ref(), "condition"); // Test full TagSpecDef -> TagSpec conversion let tagspec_def = djls_conf::TagSpecDef { @@ -715,16 +775,12 @@ mod tests { args: vec![], }; let tagspec = TagSpec::from(tagspec_def); - assert_eq!(tagspec.name, Some("custom".to_string())); + // Name field was removed from TagSpec assert!(tagspec.end_tag.is_some()); - assert_eq!(tagspec.end_tag.as_ref().unwrap().name, "endcustom"); - assert!(tagspec.intermediate_tags.is_some()); - assert_eq!(tagspec.intermediate_tags.as_ref().unwrap().len(), 1); - assert_eq!( - tagspec.intermediate_tags.as_ref().unwrap()[0].name, - "branch" - ); - assert_eq!(tagspec.intermediate_tags.as_ref().unwrap()[0].args.len(), 0); + assert_eq!(tagspec.end_tag.as_ref().unwrap().name.as_ref(), "endcustom"); + assert_eq!(tagspec.intermediate_tags.len(), 1); + assert_eq!(tagspec.intermediate_tags[0].name.as_ref(), "branch"); + assert_eq!(tagspec.intermediate_tags[0].args.len(), 0); } #[test] @@ -770,28 +826,22 @@ end_tag = { name = "endif", optional = true } // Should have user-defined custom tag let mytag = specs.get("mytag").expect("mytag should be present"); - assert_eq!(mytag.name, Some("mytag".to_string())); - assert_eq!(mytag.end_tag.as_ref().unwrap().name, "endmytag"); + // Name field was removed from TagSpec + assert_eq!(mytag.end_tag.as_ref().unwrap().name.as_ref(), "endmytag"); assert!(!mytag.end_tag.as_ref().unwrap().optional); - assert_eq!(mytag.intermediate_tags.as_ref().unwrap().len(), 1); - assert_eq!( - mytag.intermediate_tags.as_ref().unwrap()[0].name, - "mybranch" - ); + assert_eq!(mytag.intermediate_tags.len(), 1); + assert_eq!(mytag.intermediate_tags[0].name.as_ref(), "mybranch"); assert_eq!(mytag.args.len(), 2); - assert_eq!(mytag.args[0].name, "arg1"); - assert!(mytag.args[0].required); - assert_eq!(mytag.args[1].name, "arg2"); - assert!(!mytag.args[1].required); + assert_eq!(mytag.args[0].name().as_ref(), "arg1"); + assert!(mytag.args[0].is_required()); + assert_eq!(mytag.args[1].name().as_ref(), "arg2"); + assert!(!mytag.args[1].is_required()); // Should have overridden built-in "if" tag let if_tag = specs.get("if").expect("if tag should be present"); assert!(if_tag.end_tag.as_ref().unwrap().optional); // Changed to optional // Note: The built-in if tag has intermediate tags, but the override doesn't specify them // The override completely replaces the built-in - assert!( - if_tag.intermediate_tags.is_none() - || if_tag.intermediate_tags.as_ref().unwrap().is_empty() - ); + assert!(if_tag.intermediate_tags.is_empty()); } } diff --git a/crates/djls-semantic/src/validation.rs b/crates/djls-semantic/src/validation.rs index b06f6e6c..06853042 100644 --- a/crates/djls-semantic/src/validation.rs +++ b/crates/djls-semantic/src/validation.rs @@ -27,10 +27,8 @@ use salsa::Accumulator; use crate::db::Db as SemanticDb; use crate::db::ValidationErrorAccumulator; use crate::errors::ValidationError; -use crate::specs::ArgType; -use crate::specs::SimpleArgType; -use crate::specs::TagArg; -use crate::specs::TagType; +use crate::templatetags::TagArg; +use crate::templatetags::TagType; pub struct TagValidator<'db> { db: &'db dyn SemanticDb, @@ -67,7 +65,12 @@ impl<'db> TagValidator<'db> { }; // Pass full_span for error reporting - self.check_arguments(&name_str, bits, node.full_span(), args); + self.check_arguments( + &name_str, + bits, + node.full_span(), + args.map(std::convert::AsRef::as_ref), + ); match tag_type { TagType::Opener => { @@ -104,14 +107,14 @@ impl<'db> TagValidator<'db> { name: &str, bits: &[TagBit<'db>], span: Span, - args: Option<&Vec>, + args: Option<&[TagArg]>, ) { let Some(args) = args else { return; }; // Count required arguments - let required_count = args.iter().filter(|arg| arg.required).count(); + let required_count = args.iter().filter(|arg| arg.is_required()).count(); if bits.len() < required_count { self.report_error(ValidationError::MissingRequiredArguments { @@ -122,9 +125,7 @@ impl<'db> TagValidator<'db> { } // If there are more bits than defined args, that might be okay for varargs - let has_varargs = args - .iter() - .any(|arg| matches!(arg.arg_type, ArgType::Simple(SimpleArgType::VarArgs))); + let has_varargs = args.iter().any(|arg| matches!(arg, TagArg::VarArgs { .. })); if !has_varargs && bits.len() > args.len() { self.report_error(ValidationError::TooManyArguments { diff --git a/crates/djls-server/src/db.rs b/crates/djls-server/src/db.rs index 30081dbf..f1faae20 100644 --- a/crates/djls-server/src/db.rs +++ b/crates/djls-server/src/db.rs @@ -14,7 +14,7 @@ use djls_project::Db as ProjectDb; use djls_project::InspectorPool; use djls_project::Interpreter; use djls_project::Project; -use djls_semantic::db::Db as SemanticDb; +use djls_semantic::Db as SemanticDb; use djls_semantic::TagSpecs; use djls_templates::db::Db as TemplateDb; use djls_workspace::db::Db as WorkspaceDb; diff --git a/crates/djls-server/src/server.rs b/crates/djls-server/src/server.rs index 1a638069..4f00252c 100644 --- a/crates/djls-server/src/server.rs +++ b/crates/djls-server/src/server.rs @@ -2,6 +2,7 @@ use std::future::Future; use std::sync::Arc; use djls_project::Db as ProjectDb; +use djls_semantic::Db as SemanticDb; use djls_workspace::paths; use djls_workspace::FileKind; use tokio::sync::Mutex; @@ -339,7 +340,7 @@ impl LanguageServer for DjangoLanguageServer { None } }); - let tag_specs = session.with_db(djls_semantic::db::Db::tag_specs); + let tag_specs = session.with_db(SemanticDb::tag_specs); let supports_snippets = session.supports_snippets(); let completions = djls_ide::handle_completion(