Add support for `..` in let pattern matching for structs (alternative take)
This commit is contained in:
Guillaume Gomez 2024-06-29 00:02:08 +02:00 committed by GitHub
commit eb419049bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 480 additions and 160 deletions

View File

@ -12,10 +12,9 @@ use crate::input::{Source, TemplateInput};
use crate::{CompileError, CRATE}; use crate::{CompileError, CRATE};
use parser::node::{ use parser::node::{
Call, Comment, CondTest, FilterBlock, If, Include, Let, Lit, Loop, Match, Target, Whitespace, Call, Comment, CondTest, FilterBlock, If, Include, Let, Lit, Loop, Match, Whitespace, Ws,
Ws,
}; };
use parser::{Expr, Filter, Node, WithSpan}; use parser::{Expr, Filter, Node, Target, WithSpan};
use quote::quote; use quote::quote;
pub(crate) struct Generator<'a> { pub(crate) struct Generator<'a> {
@ -1788,8 +1787,8 @@ impl<'a> Generator<'a> {
target: &Target<'a>, target: &Target<'a>,
) { ) {
match target { match target {
Target::Name("_") => { Target::Placeholder(s) | Target::Rest(s) => {
buf.write("_"); buf.write(s);
} }
Target::Name(name) => { Target::Name(name) => {
let name = normalize_identifier(name); let name = normalize_identifier(name);
@ -1824,6 +1823,11 @@ impl<'a> Generator<'a> {
buf.write(SeparatedPath(path)); buf.write(SeparatedPath(path));
buf.write(" { "); buf.write(" { ");
for (name, target) in targets { for (name, target) in targets {
if let Target::Rest(s) = target {
buf.write(s);
continue;
}
buf.write(normalize_identifier(name)); buf.write(normalize_identifier(name));
buf.write(": "); buf.write(": ");
self.visit_target(buf, initialized, false, target); self.visit_target(buf, initialized, false, target);

View File

@ -22,6 +22,9 @@ pub mod expr;
pub use expr::{Expr, Filter}; pub use expr::{Expr, Filter};
pub mod node; pub mod node;
pub use node::Node; pub use node::Node;
mod target;
pub use target::Target;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;

View File

@ -3,19 +3,15 @@ use std::str;
use nom::branch::alt; use nom::branch::alt;
use nom::bytes::complete::{tag, take_till}; use nom::bytes::complete::{tag, take_till};
use nom::character::complete::char; use nom::character::complete::char;
use nom::combinator::{ use nom::combinator::{complete, consumed, cut, eof, map, not, opt, peek, recognize, value};
complete, consumed, cut, eof, map, map_res, not, opt, peek, recognize, value,
};
use nom::error::ErrorKind; use nom::error::ErrorKind;
use nom::error_position; use nom::error_position;
use nom::multi::{fold_many0, many0, many1, separated_list0, separated_list1}; use nom::multi::{many0, many1, separated_list0};
use nom::sequence::{delimited, pair, preceded, terminated, tuple}; use nom::sequence::{delimited, pair, preceded, tuple};
use crate::{not_ws, ErrorContext, ParseResult, WithSpan}; use crate::{
filter, identifier, is_ws, keyword, not_ws, skip_till, str_lit, ws, ErrorContext, Expr, Filter,
use super::{ ParseResult, State, Target, WithSpan,
bool_lit, char_lit, filter, identifier, is_ws, keyword, num_lit, path_or_identifier, skip_till,
str_lit, ws, Expr, Filter, PathOrIdentifier, State,
}; };
#[derive(Debug, PartialEq)] #[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<Target<'a>>),
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<Target<'a>>),
}
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<Self, nom::Err<ErrorContext<'a>>> {
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)] #[derive(Debug, PartialEq)]
pub struct When<'a> { pub struct When<'a> {
pub ws: Ws, pub ws: Ws,
@ -347,7 +199,7 @@ impl<'a> When<'a> {
WithSpan::new( WithSpan::new(
Self { Self {
ws: Ws(pws, nws), ws: Ws(pws, nws),
target: Target::Name("_"), target: Target::Placeholder("_"),
nodes, nodes,
}, },
start, start,

213
rinja_parser/src/target.rs Normal file
View File

@ -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<Target<'a>>),
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<Target<'a>>),
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<Target<'a>, nom::Err<ErrorContext<'a>>> {
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<T>)> {
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<Target<'_>>) -> Result<Vec<Target<'_>>, 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)
}

View File

@ -119,3 +119,73 @@ fn test_let_destruct_with_path_and_with_keyword() {
}; };
assert_eq!(t.render().unwrap(), "hello"); 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");
}

View File

@ -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-");
}

View File

@ -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() {}

View File

@ -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)