Merge pull request #584 from GuillaumeGomez/extends-first

Emit an error if an extends block doesn't come first in a template
This commit is contained in:
Guillaume Gomez 2025-08-25 12:28:31 +02:00 committed by GitHub
commit 5c4b52ad62
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 82 additions and 18 deletions

View File

@ -44,7 +44,9 @@ fn compare_ex(
) {
let generated = jinja_to_rust(jinja, fields, prefix);
let expected: TokenStream = expected.parse().unwrap();
let expected: TokenStream = expected
.parse()
.expect("`TokenStream` failed to parse input");
let expected: syn::File = syn::parse_quote! {
#[automatically_derived]
impl askama::Template for Foo {
@ -159,7 +161,10 @@ struct Foo {{ {} }}"##,
.join(","),
);
let generated = build_template(&syn::parse_str::<syn::DeriveInput>(&jinja).unwrap()).unwrap();
let generated = build_template(
&syn::parse_str::<syn::DeriveInput>(&jinja).expect("`syn` failed to parse code"),
)
.expect("`build_template` failed");
match syn::parse2(generated.clone()) {
Ok(generated) => generated,
Err(err) => panic!(
@ -1150,11 +1155,11 @@ fn test_concat() {
fn extends_with_whitespace_control() {
const CONTROL: &[&str] = &["", "\t", "-", "+", "~"];
let expected = jinja_to_rust(r#"front {% extends "a.html" %} back"#, &[], "");
let expected = jinja_to_rust(r#"{% extends "a.html" %} back"#, &[], "");
let expected = unparse(&expected);
for front in CONTROL {
for back in CONTROL {
let src = format!(r#"front {{%{front} extends "a.html" {back}%}} back"#);
let src = format!(r#"{{%{front} extends "a.html" {back}%}} back"#);
let actual = jinja_to_rust(&src, &[], "");
let actual = unparse(&actual);
assert_eq!(expected, actual, "source: {src:?}");

View File

@ -38,8 +38,27 @@ pub enum Node<'a> {
impl<'a: 'l, 'l> Node<'a> {
pub(super) fn parse_template(i: &mut InputStream<'a, 'l>) -> ParseResult<'a, Vec<Box<Self>>> {
let mut p = parse_with_unexpected_fallback(Self::many, unexpected_tag);
let nodes = p.parse_next(i)?;
let mut nodes = vec![];
let mut allow_extends = true;
while let Some(node) = parse_with_unexpected_fallback(
opt(move |i: &mut _| Self::one(i, allow_extends)),
unexpected_tag,
)
.parse_next(i)?
{
if allow_extends {
match &*node {
// Since comments don't impact generated code, we allow them before `extends`.
Node::Comment(_) => {}
// If it only contains whitespace characters, it's fine too.
Node::Lit(lit) if lit.val.is_empty() => {}
// Everything else must not come before an `extends` block.
_ => allow_extends = false,
}
}
nodes.push(node);
}
if !i.is_empty() {
opt(unexpected_tag).parse_next(i)?;
return cut_error!(
@ -53,12 +72,15 @@ impl<'a: 'l, 'l> Node<'a> {
}
fn many(i: &mut InputStream<'a, 'l>) -> ParseResult<'a, Vec<Box<Self>>> {
repeat(
0..,
alt((Lit::parse, Comment::parse, Self::expr, Self::parse)),
)
.map(|v: Vec<_>| v)
.parse_next(i)
repeat(0.., |i: &mut _| Self::one(i, false)).parse_next(i)
}
fn one(i: &mut InputStream<'a, 'l>, allow_extends: bool) -> ParseResult<'a, Box<Self>> {
let node = alt((Lit::parse, Comment::parse, Self::expr, Self::parse)).parse_next(i)?;
if !allow_extends && let Node::Extends(node) = &*node {
return cut_error!("`extends` block must come first in a template", node.span());
}
Ok(node)
}
fn parse(i: &mut InputStream<'a, 'l>) -> ParseResult<'a, Box<Self>> {

View File

@ -1160,10 +1160,10 @@ fn extends_with_whitespace_control() {
const CONTROL: &[&str] = &["", "\t", "-", "+", "~"];
let syntax = Syntax::default();
let expected = Ast::from_str(r#"front {% extends "nothing" %} back"#, None, &syntax).unwrap();
let expected = Ast::from_str(r#"{% extends "nothing" %} back"#, None, &syntax).unwrap();
for front in CONTROL {
for back in CONTROL {
let src = format!(r#"front {{%{front} extends "nothing" {back}%}} back"#);
let src = format!(r#"{{%{front} extends "nothing" {back}%}} back"#);
let actual = Ast::from_str(&src, None, &syntax).unwrap();
assert_eq!(expected.nodes(), actual.nodes(), "source: {src:?}");
}

View File

@ -25,3 +25,17 @@ fn test_macro_in_block_inheritance() {
assert_eq!(A.render().unwrap(), "\n\n1 1\n2 2\n--> 3");
}
// This test ensures that comments are allowed before `extends` block.
#[test]
fn test_comment_before_extend() {
#[derive(Template)]
#[template(
source = r##"{# comment #}{% extends "base.html" %}"##,
ext = "txt",
print = "ast"
)]
pub struct X {
title: &'static str,
}
}

View File

@ -1,5 +1,5 @@
error: `extends` blocks are not allowed below top level
--> MyTemplate1.txt:3:2
error: `extends` block must come first in a template
--> <source attribute>:3:2
" extends \"bla.txt\" %}\n{% endblock %}\n"
--> tests/ui/blocks_below_top_level.rs:4:21
|

View File

@ -0,0 +1,13 @@
use askama::Template;
#[derive(Template)]
#[template(
source = r##"bla
{% extends "base.html" %}
"##,
ext = "txt",
print = "ast"
)]
pub struct X;
fn main() {}

View File

@ -0,0 +1,10 @@
error: `extends` block must come first in a template
--> <source attribute>:2:2
" extends \"base.html\" %}\n"
--> tests/ui/extend.rs:5:14
|
5 | source = r##"bla
| ______________^
6 | | {% extends "base.html" %}
7 | | "##,
| |___^

View File

@ -1,5 +1,5 @@
error: multiple extend blocks found
--> MyTemplate4.txt:3:2
error: `extends` block must come first in a template
--> <source attribute>:3:2
" extends \"foo.html\" %}\n"
--> tests/ui/multiple_extends.rs:4:21
|