@@ -11,11 +11,23 @@ pub(crate) mod statements;
1111pub ( crate ) use calls:: extract_return_value;
1212pub ( crate ) use calls:: AbstractValueKey ;
1313use 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 ;
1423use ruff_python_ast:: Stmt ;
24+ use ruff_python_ast:: StmtAssign ;
1525use ruff_python_ast:: StmtFunctionDef ;
26+ use ruff_python_ast:: UnaryOp ;
1627
1728use crate :: python:: analysis:: constraints:: ExtractedTagConstraints ;
1829use crate :: python:: analysis:: guards:: ExtractedRuleFragment ;
30+ use crate :: python:: ext:: ExprExt ;
1931use crate :: python:: types:: ArgumentCountConstraint ;
2032use crate :: python:: types:: AsVar ;
2133use 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 (
0 commit comments