@@ -122,14 +122,10 @@ fn validate_args(db: &dyn Db, tag_name: &str, bits: &[String], span: Span, args:
122122 . accumulate ( db) ;
123123 }
124124
125- if !has_varargs && bits. len ( ) > args. len ( ) {
126- ValidationErrorAccumulator ( ValidationError :: TooManyArguments {
127- tag : tag_name. to_string ( ) ,
128- max : args. len ( ) ,
129- span,
130- } )
131- . accumulate ( db) ;
132- }
125+ // NOTE: We cannot check bits.len() > args.len() because:
126+ // - Expression arguments can consume multiple tokens (e.g., "x > 0" is 3 tokens, 1 arg)
127+ // - String arguments are now single tokens even if they contain spaces
128+ // The validate_choices_and_order function will catch actual too-many-arguments cases
133129
134130 validate_literals ( db, tag_name, bits, span, args) ;
135131
@@ -162,7 +158,7 @@ fn validate_choices_and_order(
162158) {
163159 let mut bit_index = 0usize ;
164160
165- for arg in args {
161+ for ( arg_index , arg) in args. iter ( ) . enumerate ( ) {
166162 if bit_index >= bits. len ( ) {
167163 break ;
168164 }
@@ -209,13 +205,69 @@ fn validate_choices_and_order(
209205 break ;
210206 }
211207 }
212- TagArg :: Var { .. }
213- | TagArg :: String { .. }
214- | TagArg :: Expr { .. }
215- | TagArg :: Assignment { .. }
216- | TagArg :: VarArgs { .. } => {
208+ TagArg :: Var { .. } | TagArg :: String { .. } => {
209+ // Consume exactly 1 token
217210 bit_index += 1 ;
218211 }
212+ TagArg :: Expr { .. } => {
213+ // Expression arguments consume tokens until:
214+ // - We hit the next literal keyword
215+ // - We hit the end of bits
216+ // - We've consumed at least one token
217+
218+ let start_index = bit_index;
219+ let next_literal = find_next_literal ( & args[ arg_index + 1 ..] ) ;
220+
221+ // Consume tokens greedily until we hit a known literal
222+ while bit_index < bits. len ( ) {
223+ if let Some ( ref lit) = next_literal {
224+ if bits[ bit_index] == * lit {
225+ break ; // Stop before the literal
226+ }
227+ }
228+ bit_index += 1 ;
229+ }
230+
231+ // Must consume at least one token for expression
232+ if bit_index == start_index {
233+ bit_index += 1 ;
234+ }
235+ }
236+ TagArg :: Assignment { .. } => {
237+ // Assignment can be:
238+ // 1. Single token with = (e.g., "total=value")
239+ // 2. Multiple tokens with "as" keyword (e.g., "url 'name' as varname")
240+ // For now, consume until we find pattern or reach next literal
241+
242+ let next_literal = find_next_literal ( & args[ arg_index + 1 ..] ) ;
243+
244+ while bit_index < bits. len ( ) {
245+ let token = & bits[ bit_index] ;
246+ bit_index += 1 ;
247+
248+ // If token contains =, we're done with this assignment
249+ if token. contains ( '=' ) {
250+ break ;
251+ }
252+
253+ // If we hit "as", consume one more token (the variable name)
254+ if token == "as" && bit_index < bits. len ( ) {
255+ bit_index += 1 ;
256+ break ;
257+ }
258+
259+ // Stop if we hit next literal
260+ if let Some ( ref lit) = next_literal {
261+ if token == lit {
262+ break ;
263+ }
264+ }
265+ }
266+ }
267+ TagArg :: VarArgs { .. } => {
268+ // Consume all remaining tokens
269+ bit_index = bits. len ( ) ;
270+ }
219271 }
220272 }
221273
@@ -248,3 +300,266 @@ fn argument_name(arg: &TagArg) -> String {
248300 | TagArg :: VarArgs { name, .. } => name. to_string ( ) ,
249301 }
250302}
303+
304+ /// Find the next literal keyword in the argument list.
305+ /// This helps expression arguments know when to stop consuming tokens.
306+ fn find_next_literal ( remaining_args : & [ TagArg ] ) -> Option < String > {
307+ for arg in remaining_args {
308+ if let TagArg :: Literal { lit, .. } = arg {
309+ return Some ( lit. to_string ( ) ) ;
310+ }
311+ }
312+ None
313+ }
314+
315+ #[ cfg( test) ]
316+ mod tests {
317+ use super :: * ;
318+
319+ // Helper to manually validate arguments without full database setup
320+ fn validate_args_simple ( bits : Vec < String > , args : Vec < TagArg > ) -> Vec < String > {
321+ let mut errors = Vec :: new ( ) ;
322+
323+ let has_varargs = args. iter ( ) . any ( |arg| matches ! ( arg, TagArg :: VarArgs { .. } ) ) ;
324+ let required_count = args. iter ( ) . filter ( |arg| arg. is_required ( ) ) . count ( ) ;
325+
326+ if bits. len ( ) < required_count {
327+ errors. push ( format ! ( "Missing required arguments: expected at least {required_count}, got {}" , bits. len( ) ) ) ;
328+ }
329+
330+ // Validate using the actual logic
331+ let mut bit_index = 0usize ;
332+
333+ for ( arg_index, arg) in args. iter ( ) . enumerate ( ) {
334+ if bit_index >= bits. len ( ) {
335+ break ;
336+ }
337+
338+ match arg {
339+ TagArg :: Literal { lit, required } => {
340+ let matches_literal = bits[ bit_index] == lit. as_ref ( ) ;
341+ if * required {
342+ if matches_literal {
343+ bit_index += 1 ;
344+ } else {
345+ errors. push ( format ! ( "Expected literal '{}', got '{}'" , lit, bits[ bit_index] ) ) ;
346+ break ;
347+ }
348+ } else if matches_literal {
349+ bit_index += 1 ;
350+ }
351+ }
352+ TagArg :: Choice { name : _, required : _, choices } => {
353+ let value = & bits[ bit_index] ;
354+ if choices. iter ( ) . any ( |choice| choice. as_ref ( ) == value) {
355+ bit_index += 1 ;
356+ }
357+ }
358+ TagArg :: Var { .. } | TagArg :: String { .. } => {
359+ bit_index += 1 ;
360+ }
361+ TagArg :: Expr { .. } => {
362+ let start_index = bit_index;
363+ let next_literal = find_next_literal ( & args[ arg_index + 1 ..] ) ;
364+
365+ while bit_index < bits. len ( ) {
366+ if let Some ( ref lit) = next_literal {
367+ if bits[ bit_index] == * lit {
368+ break ;
369+ }
370+ }
371+ bit_index += 1 ;
372+ }
373+
374+ if bit_index == start_index {
375+ bit_index += 1 ;
376+ }
377+ }
378+ TagArg :: Assignment { .. } => {
379+ let next_literal = find_next_literal ( & args[ arg_index + 1 ..] ) ;
380+
381+ while bit_index < bits. len ( ) {
382+ let token = & bits[ bit_index] ;
383+ bit_index += 1 ;
384+
385+ if token. contains ( '=' ) {
386+ break ;
387+ }
388+
389+ if token == "as" && bit_index < bits. len ( ) {
390+ bit_index += 1 ;
391+ break ;
392+ }
393+
394+ if let Some ( ref lit) = next_literal {
395+ if token == lit {
396+ break ;
397+ }
398+ }
399+ }
400+ }
401+ TagArg :: VarArgs { .. } => {
402+ bit_index = bits. len ( ) ;
403+ }
404+ }
405+ }
406+
407+ // Check if we have leftover bits (too many arguments)
408+ if !has_varargs && bit_index < bits. len ( ) {
409+ errors. push ( format ! ( "Too many arguments: consumed {bit_index} tokens but got {}" , bits. len( ) ) ) ;
410+ }
411+
412+ errors
413+ }
414+
415+ #[ test]
416+ fn test_if_tag_with_comparison_operator ( ) {
417+ // Issue #1: {% if message.input_tokens > 0 %}
418+ // Parser tokenizes as: ["message.input_tokens", ">", "0"]
419+ // Spec expects: [Expr{name="condition"}]
420+ let bits = vec ! [
421+ "message.input_tokens" . to_string( ) ,
422+ ">" . to_string( ) ,
423+ "0" . to_string( ) ,
424+ ] ;
425+ let args = vec ! [ TagArg :: Expr {
426+ name: "condition" . into( ) ,
427+ required: true ,
428+ } ] ;
429+
430+ let errors = validate_args_simple ( bits, args) ;
431+ assert ! ( errors. is_empty( ) , "Should not error on expression with multiple tokens: {:?}" , errors) ;
432+ }
433+
434+ #[ test]
435+ fn test_translate_with_quoted_string ( ) {
436+ // Issue #2: {% translate "Contact the owner of the site" %}
437+ // Parser tokenizes as: ['"Contact the owner of the site"'] (single token now!)
438+ let bits = vec ! [ r#""Contact the owner of the site""# . to_string( ) ] ;
439+ let args = vec ! [ TagArg :: String {
440+ name: "message" . into( ) ,
441+ required: true ,
442+ } ] ;
443+
444+ let errors = validate_args_simple ( bits, args) ;
445+ assert ! ( errors. is_empty( ) , "Should not error on quoted string: {:?}" , errors) ;
446+ }
447+
448+ #[ test]
449+ fn test_for_tag_with_reversed ( ) {
450+ // {% for item in items reversed %}
451+ let bits = vec ! [
452+ "item" . to_string( ) ,
453+ "in" . to_string( ) ,
454+ "items" . to_string( ) ,
455+ "reversed" . to_string( ) ,
456+ ] ;
457+ let args = vec ! [
458+ TagArg :: Var {
459+ name: "item" . into( ) ,
460+ required: true ,
461+ } ,
462+ TagArg :: Literal {
463+ lit: "in" . into( ) ,
464+ required: true ,
465+ } ,
466+ TagArg :: Var {
467+ name: "items" . into( ) ,
468+ required: true ,
469+ } ,
470+ TagArg :: Literal {
471+ lit: "reversed" . into( ) ,
472+ required: false ,
473+ } ,
474+ ] ;
475+
476+ let errors = validate_args_simple ( bits, args) ;
477+ assert ! ( errors. is_empty( ) , "Should handle optional literal 'reversed': {:?}" , errors) ;
478+ }
479+
480+ #[ test]
481+ fn test_if_complex_expression ( ) {
482+ // {% if user.is_authenticated and user.is_staff %}
483+ let bits = vec ! [
484+ "user.is_authenticated" . to_string( ) ,
485+ "and" . to_string( ) ,
486+ "user.is_staff" . to_string( ) ,
487+ ] ;
488+ let args = vec ! [ TagArg :: Expr {
489+ name: "condition" . into( ) ,
490+ required: true ,
491+ } ] ;
492+
493+ let errors = validate_args_simple ( bits, args) ;
494+ assert ! ( errors. is_empty( ) , "Should handle complex boolean expression: {:?}" , errors) ;
495+ }
496+
497+ #[ test]
498+ fn test_url_with_multiple_args ( ) {
499+ // {% url 'view_name' arg1 arg2 arg3 %}
500+ let bits = vec ! [
501+ "'view_name'" . to_string( ) ,
502+ "arg1" . to_string( ) ,
503+ "arg2" . to_string( ) ,
504+ "arg3" . to_string( ) ,
505+ ] ;
506+ let args = vec ! [
507+ TagArg :: String {
508+ name: "view_name" . into( ) ,
509+ required: true ,
510+ } ,
511+ TagArg :: VarArgs {
512+ name: "args" . into( ) ,
513+ required: false ,
514+ } ,
515+ ] ;
516+
517+ let errors = validate_args_simple ( bits, args) ;
518+ assert ! ( errors. is_empty( ) , "Should handle varargs: {:?}" , errors) ;
519+ }
520+
521+ #[ test]
522+ fn test_with_assignment ( ) {
523+ // {% with total=items|length %}
524+ let bits = vec ! [ "total=items|length" . to_string( ) ] ;
525+ let args = vec ! [ TagArg :: Assignment {
526+ name: "bindings" . into( ) ,
527+ required: true ,
528+ } ] ;
529+
530+ let errors = validate_args_simple ( bits, args) ;
531+ assert ! ( errors. is_empty( ) , "Should handle assignment with filter: {:?}" , errors) ;
532+ }
533+
534+ #[ test]
535+ fn test_include_with_quoted_path ( ) {
536+ // {% include "partials/header.html" %}
537+ let bits = vec ! [ r#""partials/header.html""# . to_string( ) ] ;
538+ let args = vec ! [ TagArg :: String {
539+ name: "template" . into( ) ,
540+ required: true ,
541+ } ] ;
542+
543+ let errors = validate_args_simple ( bits, args) ;
544+ assert ! ( errors. is_empty( ) , "Should handle quoted path: {:?}" , errors) ;
545+ }
546+
547+ #[ test]
548+ fn test_expr_stops_at_literal ( ) {
549+ // {% if condition reversed %} - "reversed" should not be consumed by expr
550+ let bits = vec ! [ "x" . to_string( ) , ">" . to_string( ) , "0" . to_string( ) , "reversed" . to_string( ) ] ;
551+ let args = vec ! [
552+ TagArg :: Expr {
553+ name: "condition" . into( ) ,
554+ required: true ,
555+ } ,
556+ TagArg :: Literal {
557+ lit: "reversed" . into( ) ,
558+ required: false ,
559+ } ,
560+ ] ;
561+
562+ let errors = validate_args_simple ( bits, args) ;
563+ assert ! ( errors. is_empty( ) , "Expr should stop before literal keyword: {:?}" , errors) ;
564+ }
565+ }
0 commit comments