Skip to content

Commit d6c0cac

Browse files
Handle manual as-var tag parsing
Refs #401
1 parent 32ad486 commit d6c0cac

6 files changed

Lines changed: 141 additions & 9 deletions

crates/djls-semantic/src/python/analysis.rs

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,23 @@ pub(crate) mod statements;
1111
pub(crate) use calls::extract_return_value;
1212
pub(crate) use calls::AbstractValueKey;
1313
use djls_source::File;
14+
use ruff_python_ast::BoolOp;
15+
use ruff_python_ast::CmpOp;
16+
use ruff_python_ast::Expr;
17+
use ruff_python_ast::ExprBoolOp;
18+
use ruff_python_ast::ExprCompare;
19+
use ruff_python_ast::ExprName;
20+
use ruff_python_ast::ExprSlice;
21+
use ruff_python_ast::ExprSubscript;
22+
use ruff_python_ast::ExprUnaryOp;
1423
use ruff_python_ast::Stmt;
24+
use ruff_python_ast::StmtAssign;
1525
use ruff_python_ast::StmtFunctionDef;
26+
use ruff_python_ast::UnaryOp;
1627

1728
use crate::python::analysis::constraints::ExtractedTagConstraints;
1829
use crate::python::analysis::guards::ExtractedRuleFragment;
30+
use crate::python::ext::ExprExt;
1931
use crate::python::types::ArgumentCountConstraint;
2032
use crate::python::types::AsVar;
2133
use crate::python::types::ExtractedArg;
@@ -155,10 +167,105 @@ pub(crate) fn analyze_compile_function(func: &StmtFunctionDef) -> TagRule {
155167
Some(result.diagnostic_messages)
156168
},
157169
extracted_args,
158-
as_var: AsVar::Keep,
170+
as_var: if supports_manual_as_var_strip(compile_fn.body) {
171+
AsVar::Strip
172+
} else {
173+
AsVar::Keep
174+
},
159175
}
160176
}
161177

178+
fn supports_manual_as_var_strip(stmts: &[Stmt]) -> bool {
179+
stmts.iter().any(|stmt| {
180+
let Stmt::If(stmt_if) = stmt else {
181+
return false;
182+
};
183+
let Some(name) = body_strips_trailing_as_var(&stmt_if.body) else {
184+
return false;
185+
};
186+
condition_checks_trailing_as_var(stmt_if.test.as_ref(), &name)
187+
})
188+
}
189+
190+
fn body_strips_trailing_as_var(stmts: &[Stmt]) -> Option<String> {
191+
stmts.iter().find_map(|stmt| {
192+
let Stmt::Assign(StmtAssign { targets, value, .. }) = stmt else {
193+
return None;
194+
};
195+
if targets.len() != 1 {
196+
return None;
197+
}
198+
let Expr::Name(ExprName { id: target, .. }) = &targets[0] else {
199+
return None;
200+
};
201+
let Expr::Subscript(ExprSubscript { value, slice, .. }) = value.as_ref() else {
202+
return None;
203+
};
204+
let Expr::Name(ExprName { id: source, .. }) = value.as_ref() else {
205+
return None;
206+
};
207+
if target.as_str() != source.as_str() {
208+
return None;
209+
}
210+
let Expr::Slice(ExprSlice {
211+
lower: None,
212+
upper: Some(upper),
213+
step: None,
214+
..
215+
}) = slice.as_ref()
216+
else {
217+
return None;
218+
};
219+
(negative_integer(upper) == Some(2)).then(|| target.to_string())
220+
})
221+
}
222+
223+
fn condition_checks_trailing_as_var(expr: &Expr, name: &str) -> bool {
224+
match expr {
225+
Expr::BoolOp(ExprBoolOp {
226+
op: BoolOp::And,
227+
values,
228+
..
229+
}) => values
230+
.iter()
231+
.any(|value| condition_checks_trailing_as_var(value, name)),
232+
Expr::Compare(compare) => compare_checks_trailing_as_var(compare, name),
233+
_ => false,
234+
}
235+
}
236+
237+
fn compare_checks_trailing_as_var(compare: &ExprCompare, name: &str) -> bool {
238+
compare.ops.len() == 1
239+
&& compare.comparators.len() == 1
240+
&& matches!(compare.ops[0], CmpOp::Eq)
241+
&& ((subscript_is_negative_index(compare.left.as_ref(), name, 2)
242+
&& compare.comparators[0].string_literal().as_deref() == Some("as"))
243+
|| (compare.left.string_literal().as_deref() == Some("as")
244+
&& subscript_is_negative_index(&compare.comparators[0], name, 2)))
245+
}
246+
247+
fn subscript_is_negative_index(expr: &Expr, name: &str, index: usize) -> bool {
248+
let Expr::Subscript(ExprSubscript { value, slice, .. }) = expr else {
249+
return false;
250+
};
251+
let Expr::Name(ExprName { id, .. }) = value.as_ref() else {
252+
return false;
253+
};
254+
id == name && negative_integer(slice) == Some(index)
255+
}
256+
257+
fn negative_integer(expr: &Expr) -> Option<usize> {
258+
let Expr::UnaryOp(ExprUnaryOp {
259+
op: UnaryOp::USub,
260+
operand,
261+
..
262+
}) = expr
263+
else {
264+
return None;
265+
};
266+
operand.non_negative_integer()
267+
}
268+
162269
/// Extract argument names from the environment after analysis.
163270
///
164271
/// Scans env bindings for `SplitElement` values to reconstruct positional
@@ -305,6 +412,29 @@ mod tests {
305412
analyze_compile_function(&func)
306413
}
307414

415+
#[test]
416+
fn manual_as_var_suffix_pattern_strips_before_count_validation() {
417+
let rule = analyze_source(
418+
r#"
419+
def now(parser, token):
420+
bits = token.split_contents()
421+
asvar = None
422+
if len(bits) == 4 and bits[-2] == "as":
423+
asvar = bits[-1]
424+
bits = bits[:-2]
425+
if len(bits) != 2:
426+
raise TemplateSyntaxError("'now' statement takes one argument")
427+
format_string = bits[1][1:-1]
428+
"#,
429+
);
430+
431+
assert_eq!(rule.as_var, AsVar::Strip);
432+
assert_eq!(
433+
rule.arg_constraints,
434+
vec![ArgumentCountConstraint::Exact(2)]
435+
);
436+
}
437+
308438
#[test]
309439
fn arg_names_from_tuple_unpack() {
310440
let rule = analyze_source(

crates/djls-semantic/src/python/registry.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,9 @@ impl RegistrationKind {
105105
}
106106
Self::Tag | Self::SimpleBlockTag => {
107107
let mut rule = analysis::analyze_compile_function(func);
108-
rule.as_var = self.as_var();
108+
if self.as_var().strips_suffix() {
109+
rule.as_var = self.as_var();
110+
}
109111
rule.has_content().then(|| Box::new(rule))
110112
}
111113
}

crates/djls-semantic/src/snapshots/djls_semantic__python__tests__golden_defaulttags.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ tag_rules:
131131
required: true
132132
kind: Variable
133133
position: 0
134-
as_var: keep
134+
as_var: strip
135135
"django.template.defaulttags::tag::partial":
136136
arg_constraints:
137137
- Exact: 2
@@ -294,7 +294,7 @@ tag_rules:
294294
required: true
295295
kind: Variable
296296
position: 0
297-
as_var: keep
297+
as_var: strip
298298
"django.template.defaulttags::tag::widthratio":
299299
arg_constraints: []
300300
required_keywords:

crates/djls-semantic/tests/snapshots/corpus__repos__django-5.2__django__template__defaulttags.py.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ tag_rules:
131131
required: true
132132
kind: Variable
133133
position: 0
134-
as_var: keep
134+
as_var: strip
135135
"django.template.defaulttags::tag::querystring":
136136
arg_constraints: []
137137
required_keywords: []
@@ -260,7 +260,7 @@ tag_rules:
260260
required: true
261261
kind: Variable
262262
position: 0
263-
as_var: keep
263+
as_var: strip
264264
"django.template.defaulttags::tag::widthratio":
265265
arg_constraints: []
266266
required_keywords:

crates/djls-semantic/tests/snapshots/corpus__repos__django-6.0__django__template__defaulttags.py.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ tag_rules:
131131
required: true
132132
kind: Variable
133133
position: 0
134-
as_var: keep
134+
as_var: strip
135135
"django.template.defaulttags::tag::partial":
136136
arg_constraints:
137137
- Exact: 2
@@ -294,7 +294,7 @@ tag_rules:
294294
required: true
295295
kind: Variable
296296
position: 0
297-
as_var: keep
297+
as_var: strip
298298
"django.template.defaulttags::tag::widthratio":
299299
arg_constraints: []
300300
required_keywords:

crates/djls-semantic/tests/snapshots/corpus__repos__netbox__netbox__utilities__templatetags__helpers.py.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ tag_rules:
1818
required: true
1919
kind: Variable
2020
position: 1
21-
as_var: keep
21+
as_var: strip
2222
"netbox.utilities.templatetags.helpers::tag::applied_filters":
2323
arg_constraints:
2424
- Min: 4

0 commit comments

Comments
 (0)