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 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! { let expected: syn::File = syn::parse_quote! {
#[automatically_derived] #[automatically_derived]
impl askama::Template for Foo { impl askama::Template for Foo {
@ -159,7 +161,10 @@ struct Foo {{ {} }}"##,
.join(","), .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()) { match syn::parse2(generated.clone()) {
Ok(generated) => generated, Ok(generated) => generated,
Err(err) => panic!( Err(err) => panic!(
@ -1150,11 +1155,11 @@ fn test_concat() {
fn extends_with_whitespace_control() { fn extends_with_whitespace_control() {
const CONTROL: &[&str] = &["", "\t", "-", "+", "~"]; 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); let expected = unparse(&expected);
for front in CONTROL { for front in CONTROL {
for back 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 = jinja_to_rust(&src, &[], "");
let actual = unparse(&actual); let actual = unparse(&actual);
assert_eq!(expected, actual, "source: {src:?}"); assert_eq!(expected, actual, "source: {src:?}");

View File

@ -38,8 +38,27 @@ pub enum Node<'a> {
impl<'a: 'l, 'l> Node<'a> { impl<'a: 'l, 'l> Node<'a> {
pub(super) fn parse_template(i: &mut InputStream<'a, 'l>) -> ParseResult<'a, Vec<Box<Self>>> { 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 mut nodes = vec![];
let nodes = p.parse_next(i)?; 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() { if !i.is_empty() {
opt(unexpected_tag).parse_next(i)?; opt(unexpected_tag).parse_next(i)?;
return cut_error!( 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>>> { fn many(i: &mut InputStream<'a, 'l>) -> ParseResult<'a, Vec<Box<Self>>> {
repeat( repeat(0.., |i: &mut _| Self::one(i, false)).parse_next(i)
0.., }
alt((Lit::parse, Comment::parse, Self::expr, Self::parse)),
) fn one(i: &mut InputStream<'a, 'l>, allow_extends: bool) -> ParseResult<'a, Box<Self>> {
.map(|v: Vec<_>| v) let node = alt((Lit::parse, Comment::parse, Self::expr, Self::parse)).parse_next(i)?;
.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>> { 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", "-", "+", "~"]; const CONTROL: &[&str] = &["", "\t", "-", "+", "~"];
let syntax = Syntax::default(); 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 front in CONTROL {
for back 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(); let actual = Ast::from_str(&src, None, &syntax).unwrap();
assert_eq!(expected.nodes(), actual.nodes(), "source: {src:?}"); 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"); 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 error: `extends` block must come first in a template
--> MyTemplate1.txt:3:2 --> <source attribute>:3:2
" extends \"bla.txt\" %}\n{% endblock %}\n" " extends \"bla.txt\" %}\n{% endblock %}\n"
--> tests/ui/blocks_below_top_level.rs:4:21 --> 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 error: `extends` block must come first in a template
--> MyTemplate4.txt:3:2 --> <source attribute>:3:2
" extends \"foo.html\" %}\n" " extends \"foo.html\" %}\n"
--> tests/ui/multiple_extends.rs:4:21 --> tests/ui/multiple_extends.rs:4:21
| |