Skip to content

Conversation

@joshuadavidthomas
Copy link
Owner

@joshuadavidthomas joshuadavidthomas commented Nov 5, 2025

Summary by CodeRabbit

  • Improvements

    • Enhanced semantic validation of template tag arguments with refined token count and literal argument classification.
    • Improved IDE completions and snippet generation for more accurate template argument handling.
  • Tools

    • Added new Django template tag analysis tool for examining tag specifications and generating recommendations.

@coderabbitai
Copy link

coderabbitai bot commented Nov 5, 2025

Walkthrough

This PR refactors the TagArg enum to enhance semantic clarity and validation: renames Var/Expr to Variable/Any, introduces TokenCount and LiteralKind enums, augments variants with metadata, and adds new constructor functions. Updates span completions, snippets, semantic validation, and built-in tag definitions. Adds a nox session for Django tag analysis.

Changes

Cohort / File(s) Summary
Enum Variant Renames
crates/djls-ide/src/completions.rs, crates/djls-ide/src/snippets.rs
Renames TagArg::Var to TagArg::Variable and TagArg::Expr to TagArg::Any in completions and snippet generation. Updates pattern matching and test constructors to use new variant names.
Core Semantic Type Definitions
crates/djls-semantic/src/templatetags/specs.rs
Introduces TokenCount enum (Exact/Greedy) and LiteralKind enum (Literal/Syntax/Modifier). Refactors TagArg variants: renames VarVariable, ExprAny; adds count field to Variable/Any/Assignment; adds kind field to Literal; introduces Choice variant. Adds public constructors: expr(), var(), var_greedy(), literal(), syntax(), modifier(), assignment(). Updates From<djls_conf::TagArgDef> to map to new variants with computed TokenCount.
Validation Logic Adaptations
crates/djls-semantic/src/arguments.rs
Updates argument validation to handle renamed variants and new TokenCount-driven consumption rules. Expression-like arguments (Variable/Any) support greedy consumption with minimum one-token enforcement. Assignment supports Exact/Greedy modes. Adds constructors and updates test expectations.
Public API Re-exports
crates/djls-semantic/src/lib.rs, crates/djls-semantic/src/templatetags.rs
Re-exports LiteralKind and TokenCount from specs to expand public API surface.
Built-in Tag Specifications
crates/djls-semantic/src/templatetags/builtins.rs
Updates Django built-in tag definitions across all tags to use new TagArg variants with enhanced metadata: replaces VarVariable, ExprAny, augments literals with LiteralKind. Imports LiteralKind and TokenCount.
Development Infrastructure
noxfile.py
Adds new analyze_tags nox session with Django version parameterization. Installs specified Django version and executes tag analysis script, enabling automated Django tag specification generation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Key areas requiring extra attention during review:

  • crates/djls-semantic/src/templatetags/specs.rs: Core refactoring with new enum types and From<djls_conf::TagArgDef> implementation; verify token count mapping logic and constructor behavior.
  • crates/djls-semantic/src/templatetags/builtins.rs: Extensive changes across all tag definitions; spot-check several tag specifications to ensure TokenCount and LiteralKind assignments are semantically correct.
  • crates/djls-semantic/src/arguments.rs: Validation logic with greedy vs. exact consumption rules; verify boundary conditions and token counting behavior matches intent.
  • Cross-file consistency: Ensure enum variant renames are applied consistently across all files and no stale references remain.

Poem

🐰 Var hops to Variable, Expr leaps to Any,
TokenCount and LiteralKind make semantics quite fancy,
With Greedy and Exact, our parsing runs true,
Built-in tags now gleam with metadata new! ✨

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: completing a migration to tagspec v0.6.0, which involves API updates across multiple files including enum variant renames and new constructors.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch tagspec-v0.6.0-v2

Comment @coderabbitai help to get the list of available commands and usage tips.

@codspeed-hq
Copy link

codspeed-hq bot commented Nov 5, 2025

CodSpeed Performance Report

Merging #356 will not alter performance

Comparing tagspec-v0.6.0-v2 (e265ed1) with main (8cf05a3)

Summary

✅ 20 untouched

Base automatically changed from refactor-arg-validation-v2 to main November 5, 2025 06:42
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/djls-semantic/src/arguments.rs (1)

225-263: Stop assignment args from eating required literals and add Exact bounds checks.

The assignment branch has the same Exact-token gap as above, and its greedy loop consumes the “next literal” sentinel before checking it. For inputs like {% with total=items|length only %} the current code swallows only, so the required literal never validates and we falsely succeed. Please (1) add the availability check for TokenCount::Exact, and (2) inspect the next literal before incrementing bit_index so we bail out without consuming it.

-            TagArg::Assignment { count, .. } => {
-                match count {
-                    crate::templatetags::TokenCount::Exact(n) => {
-                        // Consume exactly N tokens
-                        bit_index += n;
-                    }
-                    crate::templatetags::TokenCount::Greedy => {
+            TagArg::Assignment {
+                name,
+                required,
+                count,
+            } => {
+                match count {
+                    crate::templatetags::TokenCount::Exact(n) => {
+                        let available = bits.len().saturating_sub(bit_index);
+                        if available < n {
+                            if *required {
+                                ValidationErrorAccumulator(ValidationError::MissingArgument {
+                                    tag: tag_name.to_string(),
+                                    argument: name.to_string(),
+                                    span,
+                                })
+                                .accumulate(db);
+                                return;
+                            }
+                            continue;
+                        }
+
+                        // Consume exactly N tokens
+                        bit_index += n;
+                    }
+                    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;
-                                }
-                            }
-                        }
+                        while bit_index < bits.len() {
+                            if let Some(ref lit) = next_literal {
+                                if bits[bit_index] == *lit {
+                                    break;
+                                }
+                            }
+
+                            let token = &bits[bit_index];
+
+                            // If token contains =, we've found the assignment
+                            if token.contains('=') {
+                                bit_index += 1;
+                                break;
+                            }
+
+                            // If we hit "as", consume the variable name after it
+                            if token == "as" {
+                                bit_index += 1;
+                                if bit_index < bits.len() {
+                                    bit_index += 1; // Consume the variable name
+                                }
+                                break;
+                            }
+
+                            bit_index += 1;
+                        }
                     }
                 }
             }
🧹 Nitpick comments (1)
crates/djls-ide/src/completions.rs (1)

610-613: Refresh the comment to match the renamed variant

The comment still references the old Expr variant; updating it to Any will keep the guidance aligned with the current TagArg naming.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8cf05a3 and e265ed1.

📒 Files selected for processing (8)
  • crates/djls-ide/src/completions.rs (1 hunks)
  • crates/djls-ide/src/snippets.rs (5 hunks)
  • crates/djls-semantic/src/arguments.rs (8 hunks)
  • crates/djls-semantic/src/lib.rs (1 hunks)
  • crates/djls-semantic/src/templatetags.rs (1 hunks)
  • crates/djls-semantic/src/templatetags/builtins.rs (20 hunks)
  • crates/djls-semantic/src/templatetags/specs.rs (10 hunks)
  • noxfile.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
crates/djls-ide/src/snippets.rs (1)
crates/djls-semantic/src/templatetags/specs.rs (4)
  • var (382-388)
  • syntax (357-363)
  • modifier (366-372)
  • expr (339-345)
crates/djls-semantic/src/arguments.rs (1)
crates/djls-semantic/src/templatetags/specs.rs (6)
  • expr (339-345)
  • var (382-388)
  • syntax (357-363)
  • modifier (366-372)
  • assignment (407-413)
  • varargs (399-404)

Comment on lines +197 to 223
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;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Prevent silently accepting too few tokens for Exact arguments.

TokenCount::Exact(n) blindly bumps bit_index even when there are < n tokens left. In that case we step past bits.len() and the later “missing argument” scan never fires, so a required argument that only received a partial token sequence is treated as valid (e.g. {% with value as %} slips through). Please gate consumption on the available token count and surface a MissingArgument for required args; optional args should simply leave the remaining tokens untouched.

-            TagArg::Variable { count, .. } | TagArg::Any { count, .. } => {
-                match count {
-                    crate::templatetags::TokenCount::Exact(n) => {
-                        // Consume exactly N tokens
-                        bit_index += n;
-                    }
+            TagArg::Variable {
+                name,
+                required,
+                count,
+            }
+            | TagArg::Any {
+                name,
+                required,
+                count,
+            } => {
+                match count {
+                    crate::templatetags::TokenCount::Exact(n) => {
+                        let available = bits.len().saturating_sub(bit_index);
+                        if available < n {
+                            if *required {
+                                ValidationErrorAccumulator(ValidationError::MissingArgument {
+                                    tag: tag_name.to_string(),
+                                    argument: name.to_string(),
+                                    span,
+                                })
+                                .accumulate(db);
+                                return;
+                            }
+                            continue;
+                        }
+
+                        // Consume exactly N tokens
+                        bit_index += n;
+                    }
🤖 Prompt for AI Agents
In crates/djls-semantic/src/arguments.rs around lines 197–223, the
TokenCount::Exact(n) branch unconditionally advances bit_index by n and can run
past bits.len(), letting required args silently accept too few tokens; change
this to first compute available = bits.len() - bit_index, and if available < n
then: for required TagArg variants return/surface a MissingArgument error (do
not advance bit_index), and for optional variants leave bit_index unchanged (do
not consume any tokens). Otherwise (available >= n) advance bit_index by n but
never beyond bits.len(). Ensure no panics from out-of-bounds indexing and that
the greedy branch behavior is unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants