diff --git a/rinja_derive/src/generator.rs b/rinja_derive/src/generator.rs index 9e7ed9fd..e63cb5c9 100644 --- a/rinja_derive/src/generator.rs +++ b/rinja_derive/src/generator.rs @@ -12,10 +12,9 @@ use crate::input::{Source, TemplateInput}; use crate::{CompileError, CRATE}; use parser::node::{ - Call, Comment, CondTest, FilterBlock, If, Include, Let, Lit, Loop, Match, Target, Whitespace, - Ws, + Call, Comment, CondTest, FilterBlock, If, Include, Let, Lit, Loop, Match, Whitespace, Ws, }; -use parser::{Expr, Filter, Node, WithSpan}; +use parser::{Expr, Filter, Node, Target, WithSpan}; use quote::quote; pub(crate) struct Generator<'a> { @@ -1788,8 +1787,8 @@ impl<'a> Generator<'a> { target: &Target<'a>, ) { match target { - Target::Name("_") => { - buf.write("_"); + Target::Placeholder(s) | Target::Rest(s) => { + buf.write(s); } Target::Name(name) => { let name = normalize_identifier(name); @@ -1824,6 +1823,11 @@ impl<'a> Generator<'a> { buf.write(SeparatedPath(path)); buf.write(" { "); for (name, target) in targets { + if let Target::Rest(s) = target { + buf.write(s); + continue; + } + buf.write(normalize_identifier(name)); buf.write(": "); self.visit_target(buf, initialized, false, target); diff --git a/rinja_parser/src/lib.rs b/rinja_parser/src/lib.rs index ae88a544..a7ffb195 100644 --- a/rinja_parser/src/lib.rs +++ b/rinja_parser/src/lib.rs @@ -22,6 +22,9 @@ pub mod expr; pub use expr::{Expr, Filter}; pub mod node; pub use node::Node; + +mod target; +pub use target::Target; #[cfg(test)] mod tests; diff --git a/rinja_parser/src/node.rs b/rinja_parser/src/node.rs index c8ddbf4c..de209921 100644 --- a/rinja_parser/src/node.rs +++ b/rinja_parser/src/node.rs @@ -3,19 +3,15 @@ use std::str; use nom::branch::alt; use nom::bytes::complete::{tag, take_till}; use nom::character::complete::char; -use nom::combinator::{ - complete, consumed, cut, eof, map, map_res, not, opt, peek, recognize, value, -}; +use nom::combinator::{complete, consumed, cut, eof, map, not, opt, peek, recognize, value}; use nom::error::ErrorKind; use nom::error_position; -use nom::multi::{fold_many0, many0, many1, separated_list0, separated_list1}; -use nom::sequence::{delimited, pair, preceded, terminated, tuple}; +use nom::multi::{many0, many1, separated_list0}; +use nom::sequence::{delimited, pair, preceded, tuple}; -use crate::{not_ws, ErrorContext, ParseResult, WithSpan}; - -use super::{ - bool_lit, char_lit, filter, identifier, is_ws, keyword, num_lit, path_or_identifier, skip_till, - str_lit, ws, Expr, Filter, PathOrIdentifier, State, +use crate::{ + filter, identifier, is_ws, keyword, not_ws, skip_till, str_lit, ws, ErrorContext, Expr, Filter, + ParseResult, State, Target, WithSpan, }; #[derive(Debug, PartialEq)] @@ -177,150 +173,6 @@ impl<'a> Node<'a> { } } -#[derive(Clone, Debug, PartialEq)] -pub enum Target<'a> { - Name(&'a str), - Tuple(Vec<&'a str>, Vec>), - Struct(Vec<&'a str>, Vec<(&'a str, Target<'a>)>), - NumLit(&'a str), - StrLit(&'a str), - CharLit(&'a str), - BoolLit(&'a str), - Path(Vec<&'a str>), - OrChain(Vec>), -} - -impl<'a> Target<'a> { - /// Parses multiple targets with `or` separating them - pub(super) fn parse(i: &'a str, s: &State<'_>) -> ParseResult<'a, Self> { - map( - separated_list1(ws(tag("or")), |i| s.nest(i, |i| Self::parse_one(i, s))), - |mut opts| match opts.len() { - 1 => opts.pop().unwrap(), - _ => Self::OrChain(opts), - }, - )(i) - } - - /// Parses a single target without an `or`, unless it is wrapped in parentheses. - fn parse_one(i: &'a str, s: &State<'_>) -> ParseResult<'a, Self> { - let mut opt_opening_paren = map(opt(ws(char('('))), |o| o.is_some()); - let mut opt_closing_paren = map(opt(ws(char(')'))), |o| o.is_some()); - let mut opt_opening_brace = map(opt(ws(char('{'))), |o| o.is_some()); - - let (i, lit) = opt(Self::lit)(i)?; - if let Some(lit) = lit { - return Ok((i, lit)); - } - - // match tuples and unused parentheses - let (i, target_is_tuple) = opt_opening_paren(i)?; - if target_is_tuple { - let (i, is_empty_tuple) = opt_closing_paren(i)?; - if is_empty_tuple { - return Ok((i, Self::Tuple(Vec::new(), Vec::new()))); - } - - let (i, first_target) = Self::parse(i, s)?; - let (i, is_unused_paren) = opt_closing_paren(i)?; - if is_unused_paren { - return Ok((i, first_target)); - } - - let mut targets = vec![first_target]; - let (i, _) = cut(tuple(( - fold_many0( - preceded(ws(char(',')), |i| Self::parse(i, s)), - || (), - |_, target| { - targets.push(target); - }, - ), - opt(ws(char(','))), - ws(cut(char(')'))), - )))(i)?; - return Ok((i, Self::Tuple(Vec::new(), targets))); - } - - let path = |i| { - map_res(path_or_identifier, |v| match v { - PathOrIdentifier::Path(v) => Ok(v), - PathOrIdentifier::Identifier(v) => Err(v), - })(i) - }; - - // match structs - let (i, path) = opt(path)(i)?; - if let Some(path) = path { - let i_before_matching_with = i; - let (i, _) = opt(ws(keyword("with")))(i)?; - - let (i, is_unnamed_struct) = opt_opening_paren(i)?; - if is_unnamed_struct { - let (i, targets) = alt(( - map(char(')'), |_| Vec::new()), - terminated( - cut(separated_list1(ws(char(',')), |i| Self::parse(i, s))), - pair(opt(ws(char(','))), ws(cut(char(')')))), - ), - ))(i)?; - return Ok((i, Self::Tuple(path, targets))); - } - - let (i, is_named_struct) = opt_opening_brace(i)?; - if is_named_struct { - let (i, targets) = alt(( - map(char('}'), |_| Vec::new()), - terminated( - cut(separated_list1(ws(char(',')), |i| Self::named(i, s))), - pair(opt(ws(char(','))), ws(cut(char('}')))), - ), - ))(i)?; - return Ok((i, Self::Struct(path, targets))); - } - - return Ok((i_before_matching_with, Self::Path(path))); - } - - // neither literal nor struct nor path - let (new_i, name) = identifier(i)?; - Ok((new_i, Self::verify_name(i, name)?)) - } - - fn lit(i: &'a str) -> ParseResult<'a, Self> { - alt(( - map(str_lit, Self::StrLit), - map(char_lit, Self::CharLit), - map(num_lit, Self::NumLit), - map(bool_lit, Self::BoolLit), - ))(i) - } - - fn named(init_i: &'a str, s: &State<'_>) -> ParseResult<'a, (&'a str, Self)> { - let (i, (src, target)) = pair( - identifier, - opt(preceded(ws(char(':')), |i| Self::parse(i, s))), - )(init_i)?; - - let target = match target { - Some(target) => target, - None => Self::verify_name(init_i, src)?, - }; - - Ok((i, (src, target))) - } - - fn verify_name(input: &'a str, name: &'a str) -> Result>> { - match name { - "self" | "writer" => Err(nom::Err::Failure(ErrorContext::new( - format!("cannot use `{name}` as a name"), - input, - ))), - _ => Ok(Self::Name(name)), - } - } -} - #[derive(Debug, PartialEq)] pub struct When<'a> { pub ws: Ws, @@ -347,7 +199,7 @@ impl<'a> When<'a> { WithSpan::new( Self { ws: Ws(pws, nws), - target: Target::Name("_"), + target: Target::Placeholder("_"), nodes, }, start, diff --git a/rinja_parser/src/target.rs b/rinja_parser/src/target.rs new file mode 100644 index 00000000..5f6a7c5e --- /dev/null +++ b/rinja_parser/src/target.rs @@ -0,0 +1,213 @@ +use nom::branch::alt; +use nom::bytes::complete::tag; +use nom::character::complete::{char, one_of}; +use nom::combinator::{consumed, map, map_res, opt}; +use nom::multi::separated_list1; +use nom::sequence::{pair, preceded}; + +use crate::{ + bool_lit, char_lit, identifier, keyword, num_lit, path_or_identifier, str_lit, ws, + ErrorContext, ParseErr, ParseResult, PathOrIdentifier, State, +}; + +#[derive(Clone, Debug, PartialEq)] +pub enum Target<'a> { + Name(&'a str), + Tuple(Vec<&'a str>, Vec>), + Struct(Vec<&'a str>, Vec<(&'a str, Target<'a>)>), + NumLit(&'a str), + StrLit(&'a str), + CharLit(&'a str), + BoolLit(&'a str), + Path(Vec<&'a str>), + OrChain(Vec>), + Placeholder(&'a str), + Rest(&'a str), +} + +impl<'a> Target<'a> { + /// Parses multiple targets with `or` separating them + pub(super) fn parse(i: &'a str, s: &State<'_>) -> ParseResult<'a, Self> { + map( + separated_list1(ws(tag("or")), |i| s.nest(i, |i| Self::parse_one(i, s))), + |mut opts| match opts.len() { + 1 => opts.pop().unwrap(), + _ => Self::OrChain(opts), + }, + )(i) + } + + /// Parses a single target without an `or`, unless it is wrapped in parentheses. + fn parse_one(i: &'a str, s: &State<'_>) -> ParseResult<'a, Self> { + let mut opt_opening_paren = map(opt(ws(char('('))), |o| o.is_some()); + let mut opt_opening_brace = map(opt(ws(char('{'))), |o| o.is_some()); + + let (i, lit) = opt(Self::lit)(i)?; + if let Some(lit) = lit { + return Ok((i, lit)); + } + + // match tuples and unused parentheses + let (i, target_is_tuple) = opt_opening_paren(i)?; + if target_is_tuple { + let (i, (singleton, mut targets)) = collect_targets(i, s, ')', Self::unnamed)?; + if singleton { + return Ok((i, targets.pop().unwrap())); + } + return Ok((i, Self::Tuple(Vec::new(), only_one_rest_pattern(targets)?))); + } + + let path = |i| { + map_res(path_or_identifier, |v| match v { + PathOrIdentifier::Path(v) => Ok(v), + PathOrIdentifier::Identifier(v) => Err(v), + })(i) + }; + + // match structs + let (i, path) = opt(path)(i)?; + if let Some(path) = path { + let i_before_matching_with = i; + let (i, _) = opt(ws(keyword("with")))(i)?; + + let (i, is_unnamed_struct) = opt_opening_paren(i)?; + if is_unnamed_struct { + let (i, (_, targets)) = collect_targets(i, s, ')', Self::unnamed)?; + return Ok((i, Self::Tuple(path, only_one_rest_pattern(targets)?))); + } + + let (i, is_named_struct) = opt_opening_brace(i)?; + if is_named_struct { + let (i, (_, targets)) = collect_targets(i, s, '}', Self::named)?; + return Ok((i, Self::Struct(path, targets))); + } + + return Ok((i_before_matching_with, Self::Path(path))); + } + + // neither literal nor struct nor path + let (new_i, name) = identifier(i)?; + let target = match name { + "_" => Self::Placeholder(name), + _ => verify_name(i, name)?, + }; + Ok((new_i, target)) + } + + fn lit(i: &'a str) -> ParseResult<'a, Self> { + alt(( + map(str_lit, Self::StrLit), + map(char_lit, Self::CharLit), + map(num_lit, Self::NumLit), + map(bool_lit, Self::BoolLit), + ))(i) + } + + fn unnamed(i: &'a str, s: &State<'_>) -> ParseResult<'a, Self> { + alt((Self::rest, |i| Self::parse(i, s)))(i) + } + + fn named(init_i: &'a str, s: &State<'_>) -> ParseResult<'a, (&'a str, Self)> { + let (i, rest) = opt(consumed(Self::rest))(init_i)?; + if let Some(rest) = rest { + let (_, chr) = ws(opt(one_of(",:")))(i)?; + if let Some(chr) = chr { + return Err(nom::Err::Failure(ErrorContext::new( + format!( + "unexpected `{chr}` character after `..`\n\ + note that in a named struct, `..` must come last to ignore other members" + ), + i, + ))); + } + return Ok((i, rest)); + } + + let (i, (src, target)) = pair( + identifier, + opt(preceded(ws(char(':')), |i| Self::parse(i, s))), + )(init_i)?; + + if src == "_" { + return Err(nom::Err::Failure(ErrorContext::new( + "cannot use placeholder `_` as source in named struct", + init_i, + ))); + } + + let target = match target { + Some(target) => target, + None => verify_name(init_i, src)?, + }; + Ok((i, (src, target))) + } + + fn rest(i: &'a str) -> ParseResult<'a, Self> { + map(tag(".."), Self::Rest)(i) + } +} + +fn verify_name<'a>( + input: &'a str, + name: &'a str, +) -> Result, nom::Err>> { + match name { + "self" | "writer" => Err(nom::Err::Failure(ErrorContext::new( + format!("cannot use `{name}` as a name"), + input, + ))), + _ => Ok(Target::Name(name)), + } +} + +fn collect_targets<'a, T>( + i: &'a str, + s: &State<'_>, + delim: char, + mut one: impl FnMut(&'a str, &State<'_>) -> ParseResult<'a, T>, +) -> ParseResult<'a, (bool, Vec)> { + let opt_comma = |i| map(ws(opt(char(','))), |o| o.is_some())(i); + let opt_end = |i| map(ws(opt(char(delim))), |o| o.is_some())(i); + + let (i, has_end) = opt_end(i)?; + if has_end { + return Ok((i, (false, Vec::new()))); + } + + let (i, targets) = opt(separated_list1(ws(char(',')), |i| one(i, s)))(i)?; + let Some(targets) = targets else { + return Err(nom::Err::Failure(ErrorContext::new( + "expected comma separated list of members", + i, + ))); + }; + + let (i, (has_comma, has_end)) = pair(opt_comma, opt_end)(i)?; + if !has_end { + let msg = match has_comma { + true => format!("expected member, or `{delim}` as terminator"), + false => format!("expected `,` for more members, or `{delim}` as terminator"), + }; + return Err(nom::Err::Failure(ErrorContext::new(msg, i))); + } + + let singleton = !has_comma && targets.len() == 1; + Ok((i, (singleton, targets))) +} + +fn only_one_rest_pattern(targets: Vec>) -> Result>, ParseErr<'_>> { + let snd_wildcard = targets + .iter() + .filter_map(|t| match t { + Target::Rest(s) => Some(s), + _ => None, + }) + .nth(1); + if let Some(snd_wildcard) = snd_wildcard { + return Err(nom::Err::Failure(ErrorContext::new( + "`..` can only be used once per tuple pattern", + snd_wildcard, + ))); + } + Ok(targets) +} diff --git a/testing/tests/let_destructoring.rs b/testing/tests/let_destructoring.rs index 5d433028..383d56d5 100644 --- a/testing/tests/let_destructoring.rs +++ b/testing/tests/let_destructoring.rs @@ -119,3 +119,73 @@ fn test_let_destruct_with_path_and_with_keyword() { }; assert_eq!(t.render().unwrap(), "hello"); } + +#[derive(Template)] +#[template( + source = " +{%- if let RestPattern2 { a, b } = x -%}hello {{ a }}{%- endif -%} +{%- if let RestPattern2 { a, b, } = x -%}hello {{ b }}{%- endif -%} +{%- if let RestPattern2 { a, .. } = x -%}hello {{ a }}{%- endif -%} +", + ext = "html" +)] +struct RestPattern { + x: RestPattern2, +} + +struct RestPattern2 { + a: u32, + b: u32, +} + +#[test] +fn test_has_rest_pattern() { + let t = RestPattern { + x: RestPattern2 { a: 0, b: 1 }, + }; + assert_eq!(t.render().unwrap(), "hello 0hello 1hello 0"); +} + +#[allow(dead_code)] +struct X { + a: u32, + b: u32, +} + +#[derive(Template)] +#[template( + source = " +{%- if let X { a, .. } = x -%}hello {{ a }}{%- endif -%} +", + ext = "html" +)] +struct T1 { + x: X, +} + +#[test] +fn test_t1() { + let t = T1 { + x: X { a: 1, b: 2 }, + }; + assert_eq!(t.render().unwrap(), "hello 1"); +} + +#[derive(Template)] +#[template( + source = " +{%- if let X { .. } = x -%}hello{%- endif -%} +", + ext = "html" +)] +struct T2 { + x: X, +} + +#[test] +fn test_t2() { + let t = T2 { + x: X { a: 1, b: 2 }, + }; + assert_eq!(t.render().unwrap(), "hello"); +} diff --git a/testing/tests/rest_pattern.rs b/testing/tests/rest_pattern.rs new file mode 100644 index 00000000..28e4d933 --- /dev/null +++ b/testing/tests/rest_pattern.rs @@ -0,0 +1,78 @@ +use rinja::Template; + +#[test] +fn a() { + #[derive(Template)] + #[template(source = "{% if let (a, ..) = abc %}-{{a}}-{% endif %}", ext = "txt")] + struct Tmpl { + abc: (u32, u32, u32), + } + + assert_eq!(Tmpl { abc: (1, 2, 3) }.to_string(), "-1-"); +} + +#[test] +fn ab() { + #[derive(Template)] + #[template( + source = "{% if let (a, b, ..) = abc %}-{{a}}{{b}}-{% endif %}", + ext = "txt" + )] + struct Tmpl { + abc: (u32, u32, u32), + } + + assert_eq!(Tmpl { abc: (1, 2, 3) }.to_string(), "-12-"); +} + +#[test] +fn abc() { + #[derive(Template)] + #[template( + source = "{% if let (a, b, c, ..) = abc %}-{{a}}{{b}}{{c}}-{% endif %}", + ext = "txt" + )] + struct Tmpl1 { + abc: (u32, u32, u32), + } + + assert_eq!(Tmpl1 { abc: (1, 2, 3) }.to_string(), "-123-"); + + assert_eq!(Tmpl2 { abc: (1, 2, 3) }.to_string(), "-123-"); + + #[derive(Template)] + #[template( + source = "{% if let (a, b, c, ..) = abc %}-{{a}}{{b}}{{c}}-{% endif %}", + ext = "txt" + )] + struct Tmpl2 { + abc: (u32, u32, u32), + } + + assert_eq!(Tmpl2 { abc: (1, 2, 3) }.to_string(), "-123-"); +} + +#[test] +fn bc() { + #[derive(Template)] + #[template( + source = "{% if let (.., b, c) = abc %}-{{b}}{{c}}-{% endif %}", + ext = "txt" + )] + struct Tmpl { + abc: (u32, u32, u32), + } + + assert_eq!(Tmpl { abc: (1, 2, 3) }.to_string(), "-23-"); +} + +#[test] +fn c() { + #[derive(Template)] + #[template(source = "{% if let (.., c) = abc %}-{{c}}-{% endif %}", ext = "txt")] + struct Tmpl { + abc: (u32, u32, u32), + } + + assert_eq!(Tmpl { abc: (1, 2, 3) }.to_string(), "-3-"); +} diff --git a/testing/tests/ui/let_destructuring_has_rest.rs b/testing/tests/ui/let_destructuring_has_rest.rs new file mode 100644 index 00000000..fe002138 --- /dev/null +++ b/testing/tests/ui/let_destructuring_has_rest.rs @@ -0,0 +1,48 @@ +use rinja::Template; + +struct X { + a: u32, + b: u32, +} + +#[derive(Template)] +#[template(source = " +{%- if let X { a, .., } = x -%}hello {{ a }}{%- endif -%} +", ext = "html")] +struct T1 { + x: X, +} + +#[derive(Template)] +#[template(source = " +{%- if let X { a .. } = x -%}hello {{ a }}{%- endif -%} +", ext = "html")] +struct T2 { + x: X, +} + +#[derive(Template)] +#[template(source = " +{%- if let X { a, 1 } = x -%}hello {{ a }}{%- endif -%} +", ext = "html")] +struct T3 { + x: X, +} + +#[derive(Template)] +#[template(source = " +{%- if let X { a, .., b } = x -%}hello {{ a }}{%- endif -%} +", ext = "html")] +struct T4 { + x: X, +} + +#[derive(Template)] +#[template(source = " +{%- if let X { .., b } = x -%}hello {{ a }}{%- endif -%} +", ext = "html")] +struct T5 { + x: X, +} + +fn main() {} diff --git a/testing/tests/ui/let_destructuring_has_rest.stderr b/testing/tests/ui/let_destructuring_has_rest.stderr new file mode 100644 index 00000000..7be69205 --- /dev/null +++ b/testing/tests/ui/let_destructuring_has_rest.stderr @@ -0,0 +1,52 @@ +error: unexpected `,` character after `..` + note that in a named struct, `..` must come last to ignore other members + failed to parse template source at row 2, column 20 near: + ", } = x -%}hello {{ a }}{%- endif -%}\n" + --> tests/ui/let_destructuring_has_rest.rs:8:10 + | +8 | #[derive(Template)] + | ^^^^^^^^ + | + = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: expected `,` for more members, or `}` as terminator + failed to parse template source at row 2, column 17 near: + ".. } = x -%}hello {{ a }}{%- endif -%}\n" + --> tests/ui/let_destructuring_has_rest.rs:16:10 + | +16 | #[derive(Template)] + | ^^^^^^^^ + | + = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: expected member, or `}` as terminator + failed to parse template source at row 2, column 18 near: + "1 } = x -%}hello {{ a }}{%- endif -%}\n" + --> tests/ui/let_destructuring_has_rest.rs:24:10 + | +24 | #[derive(Template)] + | ^^^^^^^^ + | + = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: unexpected `,` character after `..` + note that in a named struct, `..` must come last to ignore other members + failed to parse template source at row 2, column 20 near: + ", b } = x -%}hello {{ a }}{%- endif -%}\n" + --> tests/ui/let_destructuring_has_rest.rs:32:10 + | +32 | #[derive(Template)] + | ^^^^^^^^ + | + = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: unexpected `,` character after `..` + note that in a named struct, `..` must come last to ignore other members + failed to parse template source at row 2, column 17 near: + ", b } = x -%}hello {{ a }}{%- endif -%}\n" + --> tests/ui/let_destructuring_has_rest.rs:40:10 + | +40 | #[derive(Template)] + | ^^^^^^^^ + | + = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)