Skip to content

Commit 846a63d

Browse files
handle variable length args in tag validation
1 parent 4e2e5f1 commit 846a63d

File tree

2 files changed

+469
-22
lines changed

2 files changed

+469
-22
lines changed

crates/djls-semantic/src/semantic/args.rs

Lines changed: 329 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)