Skip to content

Conversation

@joshuadavidthomas
Copy link
Owner

@joshuadavidthomas joshuadavidthomas commented Nov 4, 2025

Summary by CodeRabbit

Release Notes

  • Refactor
    • Restructured tag argument validation for improved performance and consistency.
    • Enhanced error detection for edge cases including missing required arguments and invalid argument configurations.

@coderabbitai
Copy link

coderabbitai bot commented Nov 4, 2025

Walkthrough

The PR refactors tag argument validation from a multi-pass, block-structure-dependent approach to a unified single-pass validation over a flat NodeList. This involves relocating validation logic to a new module structure, introducing helper methods on TagSpecs, and simplifying the overall validation pathway.

Changes

Cohort / File(s) Summary
Validation Function Restructuring
crates/djls-semantic/src/arguments.rs
Introduced validate_all_tag_arguments for single-pass flat NodeList validation; removed validate_block_tags and validate_non_block_tags; centralized argument validation logic with new helper functions and routing through validate_tag_arguments and validate_args_against_spec.
Module Reorganization & Exports
crates/djls-semantic/src/lib.rs
Updated module structure to add mod arguments and import validate_all_tag_arguments; expanded public exports (ValidationErrorAccumulator, ValidationError, Tag, Template, TemplateName, TemplateReference, TagSpec, TagSpecs); removed exports for old validation functions; updated validate_nodelist to call new validation function.
Semantic Module Cleanup
crates/djls-semantic/src/semantic.rs
Removed mod args declaration and associated re-exports (validate_block_tags, validate_non_block_tags) following relocation of validation logic.
TagSpec API Extensions
crates/djls-semantic/src/templatetags/specs.rs
Added get_intermediate_spec() method to TagSpecs; introduced new TagArgSliceExt trait with find_next_literal() method for locating Literal arguments in slices.
Re-export Updates
crates/djls-semantic/src/templatetags.rs
Removed re-export of IntermediateTag; added re-export of TagArgSliceExt.

Sequence Diagram

sequenceDiagram
    actor Caller
    participant lib as lib.rs<br/>validate_nodelist
    participant old as OLD: Multi-Pass
    participant new as NEW: Single-Pass
    participant specs as TagSpecs
    
    Caller->>lib: validate_nodelist()
    
    rect rgb(220, 240, 255)
    Note over old: Previous Flow (Removed)
    lib->>old: validate_block_tags()
    lib->>old: validate_non_block_tags()
    old->>old: Multi-pass hierarchical<br/>tree traversal
    end
    
    rect rgb(240, 255, 220)
    Note over new: New Flow (Added)
    lib->>new: validate_all_tag_arguments()
    new->>new: Single-pass flat<br/>NodeList iteration
    new->>specs: get_intermediate_spec()
    new->>specs: validate_tag_arguments()
    specs-->>new: Validation result
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • arguments.rs: Requires careful review of the new single-pass validation logic, argument handling pathways, and error semantics to ensure parity with previous multi-pass approach
  • lib.rs: Verify that updated validate_nodelist correctly integrates new validation and that expanded exports align with refactoring goals
  • Integration points: Confirm that removal of old functions from semantic.rs and adjustments to templatetags.rs re-exports don't break existing dependencies or change behavior unintentionally
  • New trait and methods: Review TagArgSliceExt::find_next_literal() and TagSpecs::get_intermediate_spec() for correctness and performance implications

Possibly related PRs

  • PR #351: Modifies tag-argument validation logic including literal-finding and varargs handling patterns
  • PR #350: Adjusts TagSpecs construction, intermediate spec handling, and introduces similar trait-based argument manipulation on TagArg slices

Poem

🐰 From trees of blocks to flat and fleet,
Arguments validate in one neat meet,
New traits and specs now light the way,
Single-pass validation saves the day! ✨

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 'refactor tag validation to not depend on semantic forest' accurately describes the main architectural change in the pull request—migrating from forest-based validation to a centralized, flat NodeList-based validation approach.
✨ 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 refactor-arg-validation-v2

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

@codspeed-hq
Copy link

codspeed-hq bot commented Nov 4, 2025

CodSpeed Performance Report

Merging #354 will not alter performance

Comparing refactor-arg-validation-v2 (213734b) with main (26847c2)

Summary

✅ 20 untouched

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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 26847c2 and 213734b.

📒 Files selected for processing (5)
  • crates/djls-semantic/src/arguments.rs (7 hunks)
  • crates/djls-semantic/src/lib.rs (3 hunks)
  • crates/djls-semantic/src/semantic.rs (0 hunks)
  • crates/djls-semantic/src/templatetags.rs (1 hunks)
  • crates/djls-semantic/src/templatetags/specs.rs (2 hunks)
💤 Files with no reviewable changes (1)
  • crates/djls-semantic/src/semantic.rs
🧰 Additional context used
🧬 Code graph analysis (2)
crates/djls-semantic/src/lib.rs (3)
crates/djls-semantic/src/arguments.rs (1)
  • validate_all_tag_arguments (21-28)
crates/djls-semantic/src/semantic.rs (1)
  • build_semantic_forest (11-27)
crates/djls-bench/benches/semantic.rs (1)
  • build_semantic_forest (26-41)
crates/djls-semantic/src/arguments.rs (2)
crates/djls-semantic/src/templatetags/specs.rs (2)
  • name (274-284)
  • choice (299-305)
crates/djls-semantic/src/templatetags/builtins.rs (1)
  • django_builtin_specs (716-718)

Comment on lines +260 to 285
// Check for unconsumed tokens (too many arguments)
if bit_index < bits.len() {
// We have unconsumed tokens - this is a too-many-arguments error
// Note: VarArgs sets bit_index = bits.len(), so we never reach here for VarArgs tags
ValidationErrorAccumulator(ValidationError::TooManyArguments {
tag: tag_name.to_string(),
max: args.len(),
max: args.len(), // Approximate - imperfect for Expr args but close enough
span,
})
.accumulate(db);
return;
}

for arg in args.iter().skip(args_consumed) {
// Check for missing required arguments that weren't satisfied
// (Only matters if we consumed all tokens but didn't satisfy all required args)
for arg in args.iter().skip(bit_index) {
if arg.is_required() {
ValidationErrorAccumulator(ValidationError::MissingArgument {
tag: tag_name.to_string(),
argument: argument_name(arg),
argument: arg.name().to_string(),
span,
})
.accumulate(db);
}
}
}
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

Fix missing required-argument detection for multi-token expressions.

When an Expr spec consumes multiple tokens (e.g. {% custom user.id > 0 %}) and the spec requires a keyword afterwards, bit_index may end up at 3 while we only declared two arguments (Expr, required literal). The final pass currently does args.iter().skip(bit_index), so it skips three entries and never flags the missing literal. Any TagSpec that expects a keyword after an expression can now silently pass validation even though the keyword is absent.

Please track the number of spec entries that were actually satisfied and use that when checking for missing required arguments:

-    let mut bit_index = 0usize;
+    let mut bit_index = 0usize;
+    let mut consumed_args = 0usize;
@@
-    for (arg_index, arg) in args.iter().enumerate() {
+    'arg_loop: for (arg_index, arg) in args.iter().enumerate() {
         if bit_index >= bits.len() {
             break; // Ran out of tokens to consume
         }
 
         match arg {
             TagArg::Literal { lit, required } => {
                 let matches_literal = bits[bit_index] == lit.as_ref();
                 if *required {
                     if matches_literal {
                         bit_index += 1;
+                        consumed_args = arg_index + 1;
                     } else {
                         ValidationErrorAccumulator(ValidationError::InvalidLiteralArgument {
                             tag: tag_name.to_string(),
                             expected: lit.to_string(),
                             span,
                         })
                         .accumulate(db);
-                        break; // Can't continue if required literal is wrong
+                        consumed_args = arg_index;
+                        break 'arg_loop; // Can't continue if required literal is wrong
                     }
                 } else if matches_literal {
                     bit_index += 1; // Optional literal matched, consume it
+                    consumed_args = arg_index + 1;
                 } else {
+                    consumed_args = consumed_args.max(arg_index + 1);
                 }
             }
             …
         }
+
+        consumed_args = consumed_args.max(arg_index + 1);
     }
@@
-    for arg in args.iter().skip(bit_index) {
+    for arg in args.iter().skip(consumed_args) {
         if arg.is_required() {
             ValidationErrorAccumulator(ValidationError::MissingArgument {
                 tag: tag_name.to_string(),
                 argument: arg.name().to_string(),
                 span,
             })
             .accumulate(db);
         }
     }

Remember to bump consumed_args appropriately in the remaining match arms whenever they succeed so we only skip the portion of the spec we truly matched.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In crates/djls-semantic/src/arguments.rs around lines 260 to 285, the final
missing-argument check uses args.iter().skip(bit_index) which is wrong when Expr
specs consume multiple input tokens; instead maintain and use a consumed_args
counter that represents how many spec entries were actually satisfied (increment
consumed_args in each successful match arm, including when Expr consumes
multiple tokens), then replace the args.iter().skip(bit_index) usage with
args.iter().skip(consumed_args) so required arguments after multi-token Exprs
are correctly detected and reported.

@joshuadavidthomas joshuadavidthomas merged commit 8cf05a3 into main Nov 5, 2025
34 checks passed
@joshuadavidthomas joshuadavidthomas deleted the refactor-arg-validation-v2 branch November 5, 2025 06:42
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