Merge pull request #476 from Kijewski/issue-475

parser: recognize/reject prefixed ids and lits in macro calls
This commit is contained in:
Guillaume Gomez 2025-06-05 21:23:09 +02:00 committed by GitHub
commit c8acf8b270
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 122 additions and 10 deletions

View File

@ -1199,8 +1199,7 @@ impl<'a> Generator<'a, '_> {
// locals in the normalized manner
let normalized_arg = normalize_identifier(arg);
buf.write(format_args!(
"let {} = {before}{value}{after};",
normalized_arg
"let {normalized_arg} = {before}{value}{after};"
));
this.locals
.insert_with_default(Cow::Borrowed(normalized_arg));

View File

@ -9,8 +9,8 @@ use proc_macro2::TokenStream;
use quote::quote;
use similar::{Algorithm, ChangeTag, TextDiffConfig};
use crate::AnyTemplateArgs;
use crate::integration::Buffer;
use crate::{AnyTemplateArgs, derive_template};
#[track_caller]
fn build_template(ast: &syn::DeriveInput) -> Result<String, crate::CompileError> {
@ -1186,7 +1186,7 @@ fn test_generated_with_error() {
#[template(ext = "txt", source = "test {#")]
struct HelloWorld;
};
let ts = crate::derive_template(ts, import_askama);
let ts = derive_template(ts, import_askama);
let _: syn::File = syn::parse2(ts).unwrap();
}
@ -1221,7 +1221,7 @@ fn fuzzed_0b85() -> Result<(), syn::Error> {
)]
struct a {}
};
let output = crate::derive_template(input, import_askama);
let output = derive_template(input, import_askama);
let _: syn::File = syn::parse2(output)?;
Ok(())
}
@ -1235,7 +1235,7 @@ fn fuzzed_comparator_chain() -> Result<(), syn::Error> {
)]
enum fff {}
};
let output = crate::derive_template(input, import_askama);
let output = derive_template(input, import_askama);
let _: syn::File = syn::parse2(output)?;
Ok(())
}
@ -1293,8 +1293,43 @@ fn test_macro_calls_need_proper_tokens() -> Result<(), syn::Error> {
)]
struct f {}
};
let output = crate::derive_template(input, import_askama);
let output = derive_template(input, import_askama);
assert!(output.to_string().contains("expected valid tokens in macro call"));
let _: syn::File = syn::parse2(output)?;
Ok(())
}
#[test]
fn test_macro_call_raw_prefix_without_data() -> Result<(), syn::Error> {
// Regression test for <https://github.com/askama-rs/askama/issues/475>.
// The parser must reject wrong usage of raw prefixes.
let input = quote! {
#[template(ext = "", source = "{{ z!{r#} }}")]
enum q {}
};
let output = derive_template(input, import_askama);
assert!(
output
.to_string()
.contains("raw prefix `r#` is only allowed with raw identifiers and raw strings")
);
let _: syn::File = syn::parse2(output)?;
Ok(())
}
#[test]
fn test_macro_call_reserved_prefix() -> Result<(), syn::Error> {
// The parser must reject reserved prefixes.
let input = quote! {
#[template(ext = "", source = "{{ z!{hello#world} }}")]
enum q {}
};
let output = derive_template(input, import_askama);
assert!(
output
.to_string()
.contains("reserved prefix `hello#`, only `r#` is allowed")
);
let _: syn::File = syn::parse2(output)?;
Ok(())
}

View File

@ -773,8 +773,8 @@ impl<'a> Suffix<'a> {
fn token<'a>(i: &mut &'a str) -> ParseResult<'a, Token> {
// <https://doc.rust-lang.org/reference/tokens.html>
let some_other = alt((
// keywords + identifiers
identifier.value(Token::SomeOther),
// keywords + (raw) identifiers + raw strings
identifier_or_prefixed_string,
// literals
Expr::char.value(Token::SomeOther),
Expr::str.value(Token::SomeOther),
@ -783,15 +783,76 @@ impl<'a> Suffix<'a> {
('\'', identifier, not(peek('\''))).value(Token::SomeOther),
// punctuations
punctuation.value(Token::SomeOther),
hash,
));
alt((open.map(Token::Open), close.map(Token::Close), some_other)).parse_next(i)
}
fn identifier_or_prefixed_string<'a>(i: &mut &'a str) -> ParseResult<'a, Token> {
let prefix = identifier.parse_next(i)?;
if opt('#').parse_next(i)?.is_none() {
// a simple identifier
return Ok(Token::SomeOther);
}
match prefix {
// raw cstring or byte slice
"cr" | "br" => {}
// raw string string or identifier
"r" => {
if opt(identifier).parse_next(i)?.is_some() {
return Ok(Token::SomeOther);
}
}
// reserved prefix: reject
_ => {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new(
format!(
"reserved prefix `{}#`, only `r#` is allowed",
prefix.escape_debug(),
),
prefix,
)));
}
}
let hashes: usize = repeat(.., '#').parse_next(i)?;
if opt('"').parse_next(i)?.is_none() {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new(
"raw prefix `r#` is only allowed with raw identifiers and raw strings",
prefix,
)));
}
let Some((_, j)) = i.split_once(&format!("\"#{:#<hashes$}", "")) else {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new(
"unterminated raw string",
prefix,
)));
};
*i = j;
Ok(Token::SomeOther)
}
fn hash<'a>(i: &mut &'a str) -> ParseResult<'a, Token> {
let start = *i;
'#'.parse_next(i)?;
if opt('"').parse_next(i)?.is_some() {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new(
"unprefixed guarded string literals are reserved for future use",
start,
)));
}
Ok(Token::SomeOther)
}
fn punctuation<'a>(i: &mut &'a str) -> ParseResult<'a, ()> {
// <https://doc.rust-lang.org/reference/tokens.html#punctuation>
// hash '#' omitted
let one = one_of([
'+', '-', '*', '/', '%', '^', '!', '&', '|', '=', '>', '<', '@', '_', '.', ',',
';', ':', '#', '$', '?', '~',
';', ':', '$', '?', '~',
]);
let two = alt((
"&&", "||", "<<", ">>", "+=", "-=", "*=", "/=", "%=", "^=", "&=", "|=", "==", "!=",

View File

@ -1417,3 +1417,19 @@ fn comparison_operators_cannot_be_chained() {
}
}
}
#[test]
fn macro_calls_can_have_raw_prefixes() {
// Related to issue <https://github.com/askama-rs/askama/issues/475>.
let syntax = Syntax::default();
let inner = r####"r#"test"# r##"test"## r###"test"### r#loop"####;
assert_eq!(
Ast::from_str(&format!("{{{{ z!{{{inner}}} }}}}"), None, &syntax)
.unwrap()
.nodes,
vec![Node::Expr(
Ws(None, None),
WithSpan::no_span(Expr::RustMacro(vec!["z"], inner)),
)],
);
}

View File

@ -0,0 +1 @@
˙˙˙{{z!{r#}}} ˙q˙ř