diff --git a/crates/djls-ide/src/completions.rs b/crates/djls-ide/src/completions.rs index 8c33cf66..d923fcf6 100644 --- a/crates/djls-ide/src/completions.rs +++ b/crates/djls-ide/src/completions.rs @@ -579,7 +579,7 @@ fn generate_argument_completions( } } } - TagArg::Var { name, .. } => { + TagArg::Variable { name, .. } => { // For variables, we could offer variable completions from context // For now, just provide a hint if partial.is_empty() { diff --git a/crates/djls-ide/src/snippets.rs b/crates/djls-ide/src/snippets.rs index 5662bde6..f5393929 100644 --- a/crates/djls-ide/src/snippets.rs +++ b/crates/djls-ide/src/snippets.rs @@ -25,7 +25,7 @@ pub fn generate_snippet_from_args(args: &[TagArg]) -> String { // At this point, we know it's required (optional literals were skipped above) lit.to_string() } - TagArg::Var { name, .. } | TagArg::Expr { name, .. } => { + TagArg::Variable { name, .. } | TagArg::Any { name, .. } => { // Variables and expressions become placeholders let result = format!("${{{}:{}}}", placeholder_index, name.as_ref()); placeholder_index += 1; @@ -125,22 +125,10 @@ mod tests { #[test] fn test_snippet_for_for_tag() { let args = vec![ - TagArg::Var { - name: "item".into(), - required: true, - }, - TagArg::Literal { - lit: "in".into(), - required: true, - }, - TagArg::Var { - name: "items".into(), - required: true, - }, - TagArg::Literal { - lit: "reversed".into(), - required: false, - }, + TagArg::var("item", true), + TagArg::syntax("in", true), + TagArg::var("items", true), + TagArg::modifier("reversed", false), ]; let snippet = generate_snippet_from_args(&args); @@ -149,10 +137,7 @@ mod tests { #[test] fn test_snippet_for_if_tag() { - let args = vec![TagArg::Expr { - name: "condition".into(), - required: true, - }]; + let args = vec![TagArg::expr("condition", true)]; let snippet = generate_snippet_from_args(&args); assert_eq!(snippet, "${1:condition}"); @@ -198,18 +183,10 @@ mod tests { end_tag: Some(EndTag { name: "endblock".into(), required: true, - args: vec![TagArg::Var { - name: "name".into(), - required: false, - }] - .into(), + args: vec![TagArg::var("name", false)].into(), }), intermediate_tags: Cow::Borrowed(&[]), - args: vec![TagArg::Var { - name: "name".into(), - required: true, - }] - .into(), + args: vec![TagArg::var("name", true)].into(), }; let snippet = generate_snippet_for_tag_with_end("block", &spec); @@ -254,14 +231,8 @@ mod tests { name: "args".into(), required: false, }, - TagArg::Literal { - lit: "as".into(), - required: false, - }, - TagArg::Var { - name: "varname".into(), - required: false, - }, + TagArg::syntax("as", false), + TagArg::var("varname", false), ]; let snippet = generate_snippet_from_args(&args); diff --git a/crates/djls-semantic/src/arguments.rs b/crates/djls-semantic/src/arguments.rs index 1bc61449..a22aaba3 100644 --- a/crates/djls-semantic/src/arguments.rs +++ b/crates/djls-semantic/src/arguments.rs @@ -143,7 +143,8 @@ fn validate_argument_order( } match arg { - TagArg::Literal { lit, required } => { + TagArg::Literal { lit, required, .. } => { + // kind field is ignored for validation - it's only for semantic hints let matches_literal = bits[bit_index] == lit.as_ref(); if *required { if matches_literal { @@ -188,63 +189,74 @@ fn validate_argument_order( // Optional choice didn't match - don't consume, continue } - TagArg::Var { .. } | TagArg::String { .. } => { - // Simple arguments consume exactly one token + TagArg::String { .. } => { + // String arguments consume exactly one token bit_index += 1; } - TagArg::Expr { .. } => { - // Expression arguments consume tokens greedily until: - // - We hit the next literal keyword (if any) - // - We run out of tokens - // Must consume at least one token - - let start_index = bit_index; - let next_literal = args[arg_index + 1..].find_next_literal(); + TagArg::Variable { count, .. } | TagArg::Any { count, .. } => { + match count { + crate::templatetags::TokenCount::Exact(n) => { + // Consume exactly N tokens + bit_index += n; + } + crate::templatetags::TokenCount::Greedy => { + // Greedy: consume tokens until next literal or end + let start_index = bit_index; + let next_literal = args[arg_index + 1..].find_next_literal(); + + while bit_index < bits.len() { + if let Some(ref lit) = next_literal { + if bits[bit_index] == *lit { + break; // Stop before the literal + } + } + bit_index += 1; + } - // Consume tokens greedily until we hit a known literal - while bit_index < bits.len() { - if let Some(ref lit) = next_literal { - if bits[bit_index] == *lit { - break; // Stop before the literal + // Ensure we consumed at least one token + if bit_index == start_index { + bit_index += 1; } } - bit_index += 1; - } - - // Ensure we consumed at least one token for the expression - if bit_index == start_index { - bit_index += 1; } } - TagArg::Assignment { .. } => { - // Assignment arguments can appear as: - // 1. Single token: var=value - // 2. Multi-token: expr as varname - // Consume until we find = or "as", or hit next literal - - let next_literal = args[arg_index + 1..].find_next_literal(); - - while bit_index < bits.len() { - let token = &bits[bit_index]; - bit_index += 1; - - // If token contains =, we've found the assignment - if token.contains('=') { - break; - } - - // If we hit "as", consume the variable name after it - if token == "as" && bit_index < bits.len() { - bit_index += 1; // Consume the variable name - break; + TagArg::Assignment { count, .. } => { + match count { + crate::templatetags::TokenCount::Exact(n) => { + // Consume exactly N tokens + bit_index += n; } - - // Stop if we hit the next literal argument - if let Some(ref lit) = next_literal { - if token == lit { - break; + crate::templatetags::TokenCount::Greedy => { + // Assignment arguments can appear as: + // 1. Single token: var=value + // 2. Multi-token: expr as varname + // Consume until we find = or "as", or hit next literal + + let next_literal = args[arg_index + 1..].find_next_literal(); + + while bit_index < bits.len() { + let token = &bits[bit_index]; + bit_index += 1; + + // If token contains =, we've found the assignment + if token.contains('=') { + break; + } + + // If we hit "as", consume the variable name after it + if token == "as" && bit_index < bits.len() { + bit_index += 1; // Consume the variable name + break; + } + + // Stop if we hit the next literal argument + if let Some(ref lit) = next_literal { + if token == lit { + break; + } + } } } } @@ -403,16 +415,13 @@ mod tests { fn test_if_tag_with_comparison_operator() { // Issue #1: {% if message.input_tokens > 0 %} // Parser tokenizes as: ["message.input_tokens", ">", "0"] - // Spec expects: [Expr{name="condition"}] + // Spec expects: [Any{name="condition", count=Greedy}] let bits = vec![ "message.input_tokens".to_string(), ">".to_string(), "0".to_string(), ]; - let args = vec![TagArg::Expr { - name: "condition".into(), - required: true, - }]; + let args = vec![TagArg::expr("condition", true)]; let errors = check_validation_errors("if", &bits, &args); assert!( @@ -448,22 +457,10 @@ mod tests { "reversed".to_string(), ]; let args = vec![ - TagArg::Var { - name: "item".into(), - required: true, - }, - TagArg::Literal { - lit: "in".into(), - required: true, - }, - TagArg::Var { - name: "items".into(), - required: true, - }, - TagArg::Literal { - lit: "reversed".into(), - required: false, - }, + TagArg::var("item", true), + TagArg::syntax("in", true), + TagArg::var("items", true), + TagArg::modifier("reversed", false), ]; let errors = check_validation_errors("for", &bits, &args); @@ -481,10 +478,7 @@ mod tests { "and".to_string(), "user.is_staff".to_string(), ]; - let args = vec![TagArg::Expr { - name: "condition".into(), - required: true, - }]; + let args = vec![TagArg::expr("condition", true)]; let errors = check_validation_errors("if", &bits, &args); assert!( @@ -521,10 +515,7 @@ mod tests { fn test_with_assignment() { // {% with total=items|length %} let bits = vec!["total=items|length".to_string()]; - let args = vec![TagArg::Assignment { - name: "bindings".into(), - required: true, - }]; + let args = vec![TagArg::assignment("bindings", true)]; let errors = check_validation_errors("with", &bits, &args); assert!( @@ -556,14 +547,8 @@ mod tests { "reversed".to_string(), ]; let args = vec![ - TagArg::Expr { - name: "condition".into(), - required: true, - }, - TagArg::Literal { - lit: "reversed".into(), - required: false, - }, + TagArg::expr("condition", true), + TagArg::modifier("reversed", false), ]; let errors = check_validation_errors("if", &bits, &args); @@ -675,18 +660,9 @@ mod tests { "library".to_string(), ]; let args = vec![ - TagArg::VarArgs { - name: "tags".into(), - required: false, - }, - TagArg::Literal { - lit: "from".into(), - required: false, - }, - TagArg::Var { - name: "library".into(), - required: false, - }, + TagArg::varargs("tags", false), + TagArg::syntax("from", false), + TagArg::var("library", false), ]; let errors = check_validation_errors("load", &bits, &args); diff --git a/crates/djls-semantic/src/lib.rs b/crates/djls-semantic/src/lib.rs index 90e582a5..6218ef12 100644 --- a/crates/djls-semantic/src/lib.rs +++ b/crates/djls-semantic/src/lib.rs @@ -24,9 +24,11 @@ pub use resolution::TemplateReference; pub use semantic::build_semantic_forest; pub use templatetags::django_builtin_specs; pub use templatetags::EndTag; +pub use templatetags::LiteralKind; pub use templatetags::TagArg; pub use templatetags::TagSpec; pub use templatetags::TagSpecs; +pub use templatetags::TokenCount; /// Validate a Django template node list and return validation errors. /// diff --git a/crates/djls-semantic/src/templatetags.rs b/crates/djls-semantic/src/templatetags.rs index 3d45e51b..73419aea 100644 --- a/crates/djls-semantic/src/templatetags.rs +++ b/crates/djls-semantic/src/templatetags.rs @@ -3,7 +3,9 @@ mod specs; pub use builtins::django_builtin_specs; pub use specs::EndTag; +pub use specs::LiteralKind; pub use specs::TagArg; pub(crate) use specs::TagArgSliceExt; pub use specs::TagSpec; pub use specs::TagSpecs; +pub use specs::TokenCount; diff --git a/crates/djls-semantic/src/templatetags/builtins.rs b/crates/djls-semantic/src/templatetags/builtins.rs index 3774b2bb..b4f19d6e 100644 --- a/crates/djls-semantic/src/templatetags/builtins.rs +++ b/crates/djls-semantic/src/templatetags/builtins.rs @@ -10,9 +10,11 @@ use rustc_hash::FxHashMap; use super::specs::EndTag; use super::specs::IntermediateTag; +use super::specs::LiteralKind; use super::specs::TagArg; use super::specs::TagSpec; use super::specs::TagSpecs; +use super::specs::TokenCount; const DEFAULTTAGS_MOD: &str = "django.template.defaulttags"; static DEFAULTTAGS_PAIRS: &[(&str, &TagSpec)] = &[ @@ -72,14 +74,17 @@ static DEFAULTTAGS_PAIRS: &[(&str, &TagSpec)] = &[ TagArg::Literal { lit: B("as"), required: false, + kind: LiteralKind::Syntax, }, - TagArg::Var { + TagArg::Variable { name: B("varname"), required: false, + count: TokenCount::Exact(1), }, TagArg::Literal { lit: B("silent"), required: false, + kind: LiteralKind::Modifier, }, ]), }, @@ -127,10 +132,12 @@ static DEFAULTTAGS_PAIRS: &[(&str, &TagSpec)] = &[ TagArg::Literal { lit: B("as"), required: false, + kind: LiteralKind::Syntax, }, - TagArg::Var { + TagArg::Variable { name: B("varname"), required: false, + count: TokenCount::Exact(1), }, ]), }, @@ -149,21 +156,25 @@ static DEFAULTTAGS_PAIRS: &[(&str, &TagSpec)] = &[ args: B(&[]), }]), args: B(&[ - TagArg::Var { + TagArg::Variable { name: B("item"), required: true, + count: TokenCount::Exact(1), }, TagArg::Literal { lit: B("in"), required: true, + kind: LiteralKind::Syntax, }, - TagArg::Var { + TagArg::Variable { name: B("items"), required: true, + count: TokenCount::Exact(1), }, TagArg::Literal { lit: B("reversed"), required: false, + kind: LiteralKind::Modifier, }, ]), }, @@ -180,9 +191,10 @@ static DEFAULTTAGS_PAIRS: &[(&str, &TagSpec)] = &[ intermediate_tags: B(&[ IntermediateTag { name: B("elif"), - args: B(&[TagArg::Expr { + args: B(&[TagArg::Any { name: B("condition"), required: true, + count: TokenCount::Greedy, }]), }, IntermediateTag { @@ -190,9 +202,10 @@ static DEFAULTTAGS_PAIRS: &[(&str, &TagSpec)] = &[ args: B(&[]), }, ]), - args: B(&[TagArg::Expr { + args: B(&[TagArg::Any { name: B("condition"), required: true, + count: TokenCount::Greedy, }]), }, ), @@ -234,9 +247,10 @@ static DEFAULTTAGS_PAIRS: &[(&str, &TagSpec)] = &[ end_tag: None, intermediate_tags: B(&[]), args: B(&[ - TagArg::Var { + TagArg::Variable { name: B("count"), required: false, + count: TokenCount::Exact(1), }, TagArg::Choice { name: B("method"), @@ -246,6 +260,7 @@ static DEFAULTTAGS_PAIRS: &[(&str, &TagSpec)] = &[ TagArg::Literal { lit: B("random"), required: false, + kind: LiteralKind::Literal, }, ]), }, @@ -264,10 +279,12 @@ static DEFAULTTAGS_PAIRS: &[(&str, &TagSpec)] = &[ TagArg::Literal { lit: B("as"), required: false, + kind: LiteralKind::Syntax, }, - TagArg::Var { + TagArg::Variable { name: B("varname"), required: false, + count: TokenCount::Exact(1), }, ]), }, @@ -282,25 +299,30 @@ static DEFAULTTAGS_PAIRS: &[(&str, &TagSpec)] = &[ end_tag: None, intermediate_tags: B(&[]), args: B(&[ - TagArg::Var { + TagArg::Variable { name: B("target"), required: true, + count: TokenCount::Exact(1), }, TagArg::Literal { lit: B("by"), required: true, + kind: LiteralKind::Syntax, }, - TagArg::Var { + TagArg::Variable { name: B("attribute"), required: true, + count: TokenCount::Exact(1), }, TagArg::Literal { lit: B("as"), required: true, + kind: LiteralKind::Syntax, }, - TagArg::Var { + TagArg::Variable { name: B("grouped"), required: true, + count: TokenCount::Exact(1), }, ]), }, @@ -359,10 +381,12 @@ static DEFAULTTAGS_PAIRS: &[(&str, &TagSpec)] = &[ TagArg::Literal { lit: B("as"), required: false, + kind: LiteralKind::Syntax, }, - TagArg::Var { + TagArg::Variable { name: B("varname"), required: false, + count: TokenCount::Exact(1), }, ]), }, @@ -390,25 +414,30 @@ static DEFAULTTAGS_PAIRS: &[(&str, &TagSpec)] = &[ end_tag: None, intermediate_tags: B(&[]), args: B(&[ - TagArg::Var { + TagArg::Variable { name: B("this_value"), required: true, + count: TokenCount::Exact(1), }, - TagArg::Var { + TagArg::Variable { name: B("max_value"), required: true, + count: TokenCount::Exact(1), }, - TagArg::Var { + TagArg::Variable { name: B("max_width"), required: true, + count: TokenCount::Exact(1), }, TagArg::Literal { lit: B("as"), required: false, + kind: LiteralKind::Syntax, }, - TagArg::Var { + TagArg::Variable { name: B("varname"), required: false, + count: TokenCount::Exact(1), }, ]), }, @@ -440,15 +469,17 @@ static LOADER_TAGS_PAIRS: &[(&str, &TagSpec)] = &[ end_tag: Some(EndTag { name: B("endblock"), required: true, - args: B(&[TagArg::Var { + args: B(&[TagArg::Variable { name: B("name"), required: false, + count: TokenCount::Exact(1), }]), }), intermediate_tags: B(&[]), - args: B(&[TagArg::Var { + args: B(&[TagArg::Variable { name: B("name"), required: true, + count: TokenCount::Exact(1), }]), }, ), @@ -478,6 +509,7 @@ static LOADER_TAGS_PAIRS: &[(&str, &TagSpec)] = &[ TagArg::Literal { lit: B("with"), required: false, + kind: LiteralKind::Syntax, }, TagArg::VarArgs { name: B("context"), @@ -486,6 +518,7 @@ static LOADER_TAGS_PAIRS: &[(&str, &TagSpec)] = &[ TagArg::Literal { lit: B("only"), required: false, + kind: LiteralKind::Modifier, }, ]), }, @@ -504,13 +537,15 @@ static CACHE_PAIRS: &[(&str, &TagSpec)] = &[( }), intermediate_tags: B(&[]), args: B(&[ - TagArg::Var { + TagArg::Variable { name: B("timeout"), required: true, + count: TokenCount::Exact(1), }, - TagArg::Var { + TagArg::Variable { name: B("cache_key"), required: true, + count: TokenCount::Exact(1), }, TagArg::VarArgs { name: B("variables"), @@ -559,9 +594,10 @@ static I18N_PAIRS: &[(&str, &TagSpec)] = &[ ]; const BLOCKTRANS_INTERMEDIATE_TAGS: &[IntermediateTag] = &[IntermediateTag { name: B("plural"), - args: B(&[TagArg::Var { + args: B(&[TagArg::Variable { name: B("count"), required: false, + count: TokenCount::Exact(1), }]), }]; const BLOCKTRANS_ARGS: &[TagArg] = &[ @@ -572,6 +608,7 @@ const BLOCKTRANS_ARGS: &[TagArg] = &[ TagArg::Literal { lit: B("with"), required: false, + kind: LiteralKind::Syntax, }, TagArg::VarArgs { name: B("assignments"), @@ -580,10 +617,12 @@ const BLOCKTRANS_ARGS: &[TagArg] = &[ TagArg::Literal { lit: B("asvar"), required: false, + kind: LiteralKind::Literal, }, - TagArg::Var { + TagArg::Variable { name: B("varname"), required: false, + count: TokenCount::Exact(1), }, ]; const TRANS_SPEC: TagSpec = TagSpec { @@ -602,14 +641,17 @@ const TRANS_SPEC: TagSpec = TagSpec { TagArg::Literal { lit: B("as"), required: false, + kind: LiteralKind::Syntax, }, - TagArg::Var { + TagArg::Variable { name: B("varname"), required: false, + count: TokenCount::Exact(1), }, TagArg::Literal { lit: B("noop"), required: false, + kind: LiteralKind::Literal, }, ]), }; @@ -681,9 +723,10 @@ static TZ_PAIRS: &[(&str, &TagSpec)] = &[ args: B(&[]), }), intermediate_tags: B(&[]), - args: B(&[TagArg::Var { + args: B(&[TagArg::Variable { name: B("timezone"), required: true, + count: TokenCount::Exact(1), }]), }, ), diff --git a/crates/djls-semantic/src/templatetags/specs.rs b/crates/djls-semantic/src/templatetags/specs.rs index 34fa2afd..8674520d 100644 --- a/crates/djls-semantic/src/templatetags/specs.rs +++ b/crates/djls-semantic/src/templatetags/specs.rs @@ -10,6 +10,32 @@ use rustc_hash::FxHashMap; pub type S = Cow<'static, T>; pub type L = Cow<'static, [T]>; +/// Token consumption strategy for tag arguments. +/// +/// This separates "how many tokens" from "what the tokens mean", +/// allowing v0.6.0 spec semantics to be properly represented. +#[derive(Debug, Clone, PartialEq)] +pub enum TokenCount { + /// Exactly N tokens (count = N in spec) + Exact(usize), + /// Variable/greedy consumption until next literal or end (count = null in spec) + Greedy, +} + +/// Semantic classification for literal arguments. +/// +/// All three map to `TagArg::Literal` but provide semantic hints +/// for better diagnostics and IDE features. +#[derive(Debug, Clone, PartialEq)] +pub enum LiteralKind { + /// Plain literal token (kind = "literal" in spec) + Literal, + /// Mandatory syntactic token like "in", "as" (kind = "syntax" in spec) + Syntax, + /// Boolean modifier like "reversed", "silent" (kind = "modifier" in spec) + Modifier, +} + #[allow(dead_code)] pub enum TagType { Opener, @@ -238,30 +264,35 @@ impl From<(djls_conf::TagDef, String)> for TagSpec { #[derive(Debug, Clone, PartialEq)] pub enum TagArg { - Var { - name: S, - required: bool, - }, - String { + /// Template variable or filter expression (kind = "variable") + Variable { name: S, required: bool, + count: TokenCount, }, + /// String literal argument (no direct v0.6.0 equivalent, implementation detail) + String { name: S, required: bool }, + /// Literal token with semantic classification (kind = "literal", "syntax", or "modifier") Literal { lit: S, required: bool, + kind: LiteralKind, }, - Expr { + /// Any template expression or literal (kind = "any") + Any { name: S, required: bool, + count: TokenCount, }, + /// Variable assignment (kind = "assignment") Assignment { name: S, required: bool, + count: TokenCount, }, - VarArgs { - name: S, - required: bool, - }, + /// Consumes all remaining tokens (implementation detail) + VarArgs { name: S, required: bool }, + /// Choice from specific literals (kind = "choice") Choice { name: S, required: bool, @@ -273,9 +304,9 @@ impl TagArg { #[must_use] pub fn name(&self) -> &S { match self { - Self::Var { name, .. } + Self::Variable { name, .. } | Self::String { name, .. } - | Self::Expr { name, .. } + | Self::Any { name, .. } | Self::Assignment { name, .. } | Self::VarArgs { name, .. } | Self::Choice { name, .. } => name, @@ -286,10 +317,10 @@ impl TagArg { #[must_use] pub fn is_required(&self) -> bool { match self { - Self::Var { required, .. } + Self::Variable { required, .. } | Self::String { required, .. } | Self::Literal { required, .. } - | Self::Expr { required, .. } + | Self::Any { required, .. } | Self::Assignment { required, .. } | Self::VarArgs { required, .. } | Self::Choice { required, .. } => *required, @@ -304,17 +335,39 @@ impl TagArg { } } + /// Create an Any argument with greedy token consumption (replaces old `expr()`) pub fn expr(name: impl Into, required: bool) -> Self { - Self::Expr { + Self::Any { name: name.into(), required, + count: TokenCount::Greedy, } } + /// Create a literal with Literal kind (plain literal) pub fn literal(lit: impl Into, required: bool) -> Self { Self::Literal { lit: lit.into(), required, + kind: LiteralKind::Literal, + } + } + + /// Create a literal with Syntax kind (keywords like "in", "as") + pub fn syntax(lit: impl Into, required: bool) -> Self { + Self::Literal { + lit: lit.into(), + required, + kind: LiteralKind::Syntax, + } + } + + /// Create a literal with Modifier kind (modifiers like "reversed") + pub fn modifier(lit: impl Into, required: bool) -> Self { + Self::Literal { + lit: lit.into(), + required, + kind: LiteralKind::Modifier, } } @@ -325,10 +378,21 @@ impl TagArg { } } + /// Create a Variable argument with single token consumption (replaces old `var()`) pub fn var(name: impl Into, required: bool) -> Self { - Self::Var { + Self::Variable { + name: name.into(), + required, + count: TokenCount::Exact(1), + } + } + + /// Create a Variable argument with greedy token consumption + pub fn var_greedy(name: impl Into, required: bool) -> Self { + Self::Variable { name: name.into(), required, + count: TokenCount::Greedy, } } @@ -339,10 +403,12 @@ impl TagArg { } } + /// Create an Assignment argument with greedy token consumption pub fn assignment(name: impl Into, required: bool) -> Self { Self::Assignment { name: name.into(), required, + count: TokenCount::Greedy, } } } @@ -370,24 +436,42 @@ impl TagArgSliceExt for [TagArg] { impl From for TagArg { fn from(value: djls_conf::TagArgDef) -> Self { + // Convert count: Option to TokenCount + let token_count = match value.count { + Some(n) => TokenCount::Exact(n), + None => TokenCount::Greedy, + }; + match value.kind { - djls_conf::ArgKindDef::Literal - | djls_conf::ArgKindDef::Syntax - | djls_conf::ArgKindDef::Modifier => TagArg::Literal { + djls_conf::ArgKindDef::Literal => TagArg::Literal { lit: value.name.into(), required: value.required, + kind: LiteralKind::Literal, }, - djls_conf::ArgKindDef::Variable => TagArg::Var { + djls_conf::ArgKindDef::Syntax => TagArg::Literal { + lit: value.name.into(), + required: value.required, + kind: LiteralKind::Syntax, + }, + djls_conf::ArgKindDef::Modifier => TagArg::Literal { + lit: value.name.into(), + required: value.required, + kind: LiteralKind::Modifier, + }, + djls_conf::ArgKindDef::Variable => TagArg::Variable { name: value.name.into(), required: value.required, + count: token_count, }, - djls_conf::ArgKindDef::Any => TagArg::Expr { + djls_conf::ArgKindDef::Any => TagArg::Any { name: value.name.into(), required: value.required, + count: token_count, }, djls_conf::ArgKindDef::Assignment => TagArg::Assignment { name: value.name.into(), required: value.required, + count: token_count, }, djls_conf::ArgKindDef::Choice => { // Extract choices from extra metadata @@ -534,9 +618,10 @@ mod tests { end_tag: Some(EndTag { name: "endblock".into(), required: true, - args: Cow::Owned(vec![TagArg::Var { + args: Cow::Owned(vec![TagArg::Variable { name: "name".into(), required: false, + count: TokenCount::Exact(1), }]), }), intermediate_tags: Cow::Borrowed(&[]), @@ -798,10 +883,17 @@ mod tests { extra: None, }; let arg = TagArg::from(var_arg_def); - assert!(matches!(arg, TagArg::Var { .. })); - if let TagArg::Var { name, required } = arg { + assert!(matches!(arg, TagArg::Variable { .. })); + if let TagArg::Variable { + name, + required, + count, + } = arg + { assert_eq!(name.as_ref(), "test_arg"); assert!(required); + // count is None in spec, so should be Greedy + assert_eq!(count, TokenCount::Greedy); } // Test choice argument with extra metadata diff --git a/noxfile.py b/noxfile.py index 8bc23a15..4d48275c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -163,6 +163,27 @@ def lint(session): session.error("Linting failed with all Python versions") +@nox.session +@nox.parametrize("django", [DJ42, DJ51, DJ52, DJ60, DJMAIN]) +def analyze_tags(session, django): + """Analyze Django template tags and generate TagSpec suggestions.""" + if django == DJMAIN: + session.install( + "django @ https://github.com/django/django/archive/refs/heads/main.zip" + ) + else: + session.install(f"django=={django}") + + session.run( + "python", + "scripts/analyze_django_tags.py", + "--version", + django, + "--output-dir", + "analysis", + ) + + @nox.session def copy_bench_fixtures(session): django_version = (