use proc_macro2::Span;
use quote::quote;
use syn::{
token::{self},
Ident, LitInt, Token, Type,
};
use crate::ast::{keyword::Keyword, node::Pattern};
#[derive(Clone)]
#[cfg_attr(any(feature = "extra-traits", test), derive(Debug))]
pub struct Capture {
pub _hash_tag: Token![#],
pub _paren: token::Paren,
pub binder: Binder, pub matcher: Matcher, pub quantity: Quantity,
pub edge: Option<Keyword>,
pub span: Span,
}
#[derive(Clone)]
#[cfg_attr(any(feature = "extra-traits", test), derive(Debug, PartialEq))]
pub enum Binder {
Named(syn::Ident), Inline(usize), Anonymous, }
#[derive(Clone)]
#[cfg_attr(any(feature = "extra-traits", test), derive(Debug))]
pub struct Matcher {
pub kind: MatcherKind,
pub span: Span,
}
#[derive(Clone)]
#[cfg_attr(any(feature = "extra-traits", test), derive(Debug))]
pub enum MatcherKind {
SynType(syn::Type),
Nested(Vec<Pattern>),
Enum {
enum_name: Type,
variants: Vec<(EnumVariant, Matcher)>,
},
}
#[derive(Clone)]
#[cfg_attr(any(feature = "extra-traits", test), derive(Debug))]
#[allow(dead_code)]
pub enum EnumVariant {
Type {
ident: Type,
ty: Type,
},
Capture {
ident: Type,
named: bool,
fields: Vec<FieldDef>,
pattern: Box<Pattern>,
},
}
#[derive(Clone)]
#[cfg_attr(any(feature = "extra-traits", test), derive(Debug, PartialEq))]
pub enum Quantity {
One, Optional, Many(Option<Keyword>), }
#[derive(Clone)]
#[cfg_attr(any(feature = "extra-traits", test), derive(Debug, PartialEq))]
pub struct FieldDef {
pub name: Ident,
pub ty: Type,
pub is_optional: bool, pub is_inline: bool,
}
impl Capture {
pub fn collect_captures(&self) -> Vec<FieldDef> {
let mut fields = self.matcher.collect_captures(&self.binder);
self.apply_quantity_wrapping(&mut fields);
fields
}
fn apply_quantity_wrapping(&self, fields: &mut Vec<FieldDef>) {
if fields.is_empty() {
return;
}
match &self.quantity {
Quantity::One => {
}
Quantity::Optional => {
for field in fields {
if !field.is_optional {
let ty = &field.ty;
field.ty = syn::parse_quote!(::std::option::Option<#ty>);
field.is_optional = true;
}
}
}
Quantity::Many(sep) => {
for field in fields {
let ty = &field.ty;
if let Some(s) = sep {
field.ty = syn::parse_quote!(::syn::punctuated::Punctuated<#ty, #s>);
} else {
field.ty = syn::parse_quote!(::std::vec::Vec<#ty>);
}
field.is_optional = false;
}
}
}
}
}
impl Matcher {
fn collect_captures(&self, binder: &Binder) -> Vec<FieldDef> {
match &self.kind {
MatcherKind::SynType(ty) | MatcherKind::Enum { enum_name: ty, .. } => {
generate_captures(ty, binder)
.map(|def| vec![def])
.unwrap_or_default()
}
MatcherKind::Nested(children) => {
match binder {
Binder::Named(ident) => {
let type_name = quote::format_ident!("{}_Item", ident);
let ty = if let Some(scope) = crate::scope_context::get_scope_ident() {
syn::parse_quote!(#scope::#type_name)
} else {
syn::parse_quote!(#type_name)
};
vec![FieldDef {
name: ident.clone(),
ty,
is_optional: false,
is_inline: false,
}]
}
Binder::Inline(idx) => {
let ident = LitInt::new(&idx.to_string(), Span::call_site());
let type_name = quote::format_ident!("_{ident}");
let ty = if let Some(scope) = crate::scope_context::get_scope_ident() {
syn::parse_quote!(#scope::#type_name)
} else {
syn::parse_quote!(#type_name)
};
vec![FieldDef {
name: type_name,
ty,
is_optional: false,
is_inline: true,
}]
}
Binder::Anonymous => {
children.iter().flat_map(|p| p.collect_captures()).collect()
}
}
}
}
}
}
fn generate_captures(ty: &Type, binder: &Binder) -> Option<FieldDef> {
match binder {
Binder::Named(ident) => Some(FieldDef {
name: ident.clone(),
ty: ty.clone(),
is_optional: false, is_inline: false,
}),
Binder::Inline(idx) => Some(FieldDef {
name: quote::format_ident!("_{}", idx),
ty: ty.clone(),
is_optional: false,
is_inline: true,
}),
Binder::Anonymous => None, }
}
#[derive(Debug, Clone)]
pub enum ExampleItem {
Literal(String),
Capture {
name: String,
ty: String,
},
Group {
delimiter: (String, String),
example: Vec<ExampleItem>,
},
Block {
optional: bool,
example: Vec<ExampleItem>,
iter: String,
},
Poly {
name: String,
syntex_name: String,
example: Vec<ExampleItem>,
},
}
impl Capture {
pub fn collect_example(&self) -> Vec<ExampleItem> {
self.matcher.collect_example(&self.binder, &self.quantity)
}
}
type MatcherEnumVariant = (EnumVariant, Matcher);
trait CollectExample {
fn collect_example(&self) -> Vec<ExampleItem>;
}
impl CollectExample for MatcherEnumVariant {
fn collect_example(&self) -> Vec<ExampleItem> {
match self {
(EnumVariant::Type { ident, ty }, ..) => {
vec![ExampleItem::Capture {
name: quote! {#ident}.to_string(),
ty: quote! {#ty}.to_string(),
}]
}
(EnumVariant::Capture { pattern, .. }, ..) => pattern.collect_example(),
}
}
}
impl Matcher {
pub fn collect_example(&self, binder: &Binder, quantity: &Quantity) -> Vec<ExampleItem> {
let name = match binder {
Binder::Named(ident) => ident.to_string(),
_ => String::new(),
};
let wrapper = |items: Vec<ExampleItem>| match quantity {
Quantity::One => items,
Quantity::Many(sep) => {
let iter = if let Some(sep) = sep {
sep.to_string()
} else {
String::new()
};
vec![ExampleItem::Block {
optional: false,
example: items,
iter,
}]
}
Quantity::Optional => vec![ExampleItem::Block {
optional: true,
example: items,
iter: String::new(),
}],
};
let items = match &self.kind {
MatcherKind::Enum {
enum_name,
variants,
} => {
vec![ExampleItem::Poly {
name: name.to_string(),
syntex_name: quote! {#enum_name}.to_string(),
example: variants
.iter()
.map(|v| {
let examples = v.collect_example();
if let Some(example) = examples.first() {
example.clone()
} else {
ExampleItem::Block {
optional: false,
example: examples,
iter: String::new(),
}
}
})
.collect(),
}]
}
MatcherKind::SynType(ty) => {
vec![ExampleItem::Capture {
name: name.to_string(),
ty: quote! {#ty}.to_string(),
}]
}
MatcherKind::Nested(nest) => nest.iter().flat_map(|n| n.collect_example()).collect(),
};
wrapper(items)
}
}
#[cfg(test)]
mod tests {
use crate::{
ast::{keyword::Keyword, node::PatternKind},
codegen::logic::Compiler,
scope_context::reset_inline_counter,
syntax::context::ParseContext,
};
use super::*;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{
parse::{ParseStream, Parser},
parse_quote, Result,
};
fn assert_named(capture: &Capture, expected_name: &str) {
if let Binder::Named(ident) = &capture.binder {
assert_eq!(ident.to_string(), expected_name);
} else {
panic!("Expected Named capture, got {:?}", capture.binder);
}
}
fn assert_inline(capture: &Capture) {
if !matches!(capture.binder, Binder::Inline(_)) {
panic!("Expected Inline capture, got {:?}", capture.binder);
}
}
fn assert_anonymous(capture: &Capture) {
if !matches!(capture.binder, Binder::Anonymous) {
panic!("Expected Anonymous capture, got {:?}", capture.binder);
}
}
fn parse_capture(input: TokenStream, ctx: &mut ParseContext) -> Result<Capture> {
let parser = move |input: ParseStream| Capture::parse(input, ctx);
parser.parse2(input)
}
fn parse_capture_matcher(input: TokenStream, ctx: &mut ParseContext) -> Result<Matcher> {
let parser = move |input: ParseStream| Matcher::parse(input, ctx);
parser.parse2(input)
}
#[test]
fn test_parse_basic_named() {
let ctx = &mut ParseContext::default();
let input = quote! { #(my_field: syn::Ident) };
let capture: Capture = parse_capture(input, ctx).unwrap();
assert_named(&capture, "my_field");
assert_eq!(capture.quantity, Quantity::One);
assert!(matches!(capture.matcher.kind, MatcherKind::SynType(_)));
}
#[test]
fn test_parse_optional_named() {
let ctx = &mut ParseContext::default();
let input = quote! { #(maybe_val?: u32) };
let capture: Capture = parse_capture(input, ctx).unwrap();
assert_named(&capture, "maybe_val");
assert_eq!(capture.quantity, Quantity::Optional);
}
#[test]
fn test_parse_iter_named() {
let ctx = &mut ParseContext::default();
let input = quote! { #(list*[,]: Ident) };
let capture: Capture = parse_capture(input, ctx).unwrap();
assert_named(&capture, "list");
if let Quantity::Many(sep) = &capture.quantity {
if let Some(Keyword::Rust(s)) = sep {
assert_eq!(s, ",");
} else {
panic!("Expected Rust keyword separator");
}
} else {
panic!("Expected Iter mode");
}
let input = quote! { #(tokens*: Ident) };
let capture: Capture = parse_capture(input, ctx).unwrap();
assert_named(&capture, "tokens");
assert_eq!(capture.quantity, Quantity::Many(None));
let input = quote! { #(tokens*[]: Ident) };
let capture: Capture = parse_capture(input, ctx).unwrap();
assert_named(&capture, "tokens");
assert_eq!(capture.quantity, Quantity::Many(None));
let input = quote! { #(tokens*[ ]: Ident) };
let capture: Capture = parse_capture(input, ctx).unwrap();
assert_named(&capture, "tokens");
assert_eq!(capture.quantity, Quantity::Many(None));
}
#[test]
fn test_parse_inline() {
let ctx = &mut ParseContext::default();
let input1 = quote! { #(@: Ident) };
let spec1: Capture = parse_capture(input1, ctx).unwrap();
assert_inline(&spec1);
assert_eq!(spec1.quantity, Quantity::One);
let input2 = quote! { #(@?: Ident) };
let spec2: Capture = parse_capture(input2, ctx).unwrap();
assert_inline(&spec2);
assert_eq!(spec2.quantity, Quantity::Optional);
}
#[test]
fn test_parse_anonymous() {
let ctx = &mut ParseContext::default();
let input = quote! { #(syn::Type) };
let capture: Capture = parse_capture(input, ctx).unwrap();
assert_anonymous(&capture);
assert_eq!(capture.quantity, Quantity::One);
let input2 = quote! { #(?: syn::Visibility) };
let spec2: Capture = parse_capture(input2, ctx).unwrap();
assert_anonymous(&spec2);
assert_eq!(spec2.quantity, Quantity::Optional);
}
#[test]
fn test_parse_joint_nested() {
let ctx = &mut ParseContext::default();
let input = quote! { #(?: -> #( name: Ident )) };
let result = parse_capture_matcher(input, ctx);
match result {
Ok(Matcher {
kind: MatcherKind::Nested(pattern_list),
..
}) => {
assert!(!pattern_list.is_empty());
if let PatternKind::Capture(cap) = &pattern_list[0].kind {
assert_eq!(cap.quantity, Quantity::Optional);
match &cap.matcher.kind {
MatcherKind::Nested(nest) => {
match &nest[0].kind {
PatternKind::Literal(keyword) => {
assert_eq!(keyword, &Keyword::Rust("->".to_string()))
}
_ => panic!("First element of patterns should be Literal"),
}
match &nest[1].kind {
PatternKind::Capture(capture) => {
assert_eq!(capture.quantity, Quantity::One);
assert_eq!(capture.binder, Binder::Named(parse_quote!(name)));
match &capture.matcher.kind {
MatcherKind::SynType(type_matcher) => {
assert_eq!(type_matcher, &parse_quote!(Ident));
}
_ => panic!("expected SynType"),
}
}
_ => panic!("Second element of patterns should be Capture"),
}
}
_ => panic!("Capture matcher should be -> literal"),
}
} else {
panic!("First element of patterns should be Capture");
}
}
Ok(_) => panic!("Expected Nested capture type"),
Err(e) => panic!("Failed to parse joint: {}", e),
}
}
#[test]
fn test_parse_empty_separator() {
let ctx = &mut ParseContext::default();
let input = quote! { #(args*[]: Ident) };
let result = parse_capture(input, ctx).unwrap();
assert_eq!(result.quantity, Quantity::Many(None));
}
#[test]
fn test_parse_enum_capture() {
let ctx = &mut ParseContext::default();
let expect_enum_name: Type = parse_quote!(Enum);
let input = quote! { #(args: Enum { Type, syn::Ident }) };
let result = parse_capture(input, ctx).unwrap();
let MatcherKind::Enum {
enum_name,
variants,
} = result.matcher.kind
else {
panic!("Expected Enum matcher kind");
};
assert_eq!(enum_name, expect_enum_name);
assert_eq!(variants.len(), 2);
match &variants[0].0 {
EnumVariant::Type { ident, ty } => {
assert_eq!(ident, &parse_quote!(Type));
assert_eq!(ty, &parse_quote!(Type));
}
_ => panic!("Expected EnumVariant::Type"),
}
match &variants[1].0 {
EnumVariant::Type { ident, ty } => {
assert_eq!(ident, &parse_quote!(Ident));
assert_eq!(ty, &parse_quote!(syn::Ident));
}
_ => panic!("Expected EnumVariant::Type"),
}
let input = quote! { #(args: Enum { Ty: Type, Id: Ident }) };
let result = parse_capture(input, ctx).unwrap();
let MatcherKind::Enum {
enum_name,
variants,
} = result.matcher.kind
else {
panic!("Expected Enum matcher kind");
};
assert_eq!(enum_name, expect_enum_name);
assert_eq!(variants.len(), 2);
match &variants[0].0 {
EnumVariant::Type { ident, ty } => {
assert_eq!(ident, &parse_quote!(Ty));
assert_eq!(ty, &parse_quote!(Type));
}
_ => panic!("Expected EnumVariant::Type"),
}
match &variants[1].0 {
EnumVariant::Type { ident, ty } => {
assert_eq!(ident, &parse_quote!(Id));
assert_eq!(ty, &parse_quote!(Ident));
}
_ => panic!("Expected EnumVariant::Type"),
}
let input = quote! {#(args: Enum { FnArg: #(id: Ident): #(ty: Type), WithDefault: #(name: Ident) = #(default: Expr) })};
let result = parse_capture(input, ctx).unwrap();
let MatcherKind::Enum {
enum_name,
variants,
} = result.matcher.kind
else {
panic!("Expected Enum matcher kind");
};
assert_eq!(enum_name, expect_enum_name);
assert_eq!(variants.len(), 2);
match &variants[0].0 {
EnumVariant::Capture {
named,
ident,
fields,
..
} => {
assert!(named);
assert_eq!(ident, &parse_quote!(FnArg));
assert_eq!(
fields,
&vec![
FieldDef {
ty: parse_quote!(Ident),
name: parse_quote!(id),
is_inline: false,
is_optional: false,
},
FieldDef {
ty: parse_quote!(Type),
name: parse_quote!(ty),
is_inline: false,
is_optional: false,
},
]
);
}
_ => panic!("Expected EnumVariant::Capture"),
}
match &variants[1].0 {
EnumVariant::Capture {
named,
ident,
fields,
..
} => {
assert!(named);
assert_eq!(ident, &parse_quote!(WithDefault));
assert_eq!(
fields,
&vec![
FieldDef {
ty: parse_quote!(Ident),
name: parse_quote!(name),
is_inline: false,
is_optional: false,
},
FieldDef {
ty: parse_quote!(Expr),
name: parse_quote!(default),
is_inline: false,
is_optional: false,
},
]
);
}
_ => panic!("Expected EnumVariant::Capture"),
}
reset_inline_counter();
let input = quote! {#(args: Enum { FnArg: #(@: Ident): #(@: Type), WithDefault: #(@: Ident) = #(@: Expr) })};
let result = parse_capture(input, ctx).unwrap();
let MatcherKind::Enum {
enum_name,
variants,
} = result.matcher.kind
else {
panic!("Expected Enum matcher kind");
};
assert_eq!(enum_name, expect_enum_name);
assert_eq!(variants.len(), 2);
match &variants[0].0 {
EnumVariant::Capture {
named,
ident,
fields,
..
} => {
assert!(!named);
assert_eq!(ident, &parse_quote!(FnArg));
assert_eq!(
fields,
&vec![
FieldDef {
ty: parse_quote!(Ident),
name: parse_quote!(_0),
is_inline: true,
is_optional: false,
},
FieldDef {
ty: parse_quote!(Type),
name: parse_quote!(_1),
is_inline: true,
is_optional: false,
},
]
);
}
_ => panic!("Expected EnumVariant::Capture"),
}
match &variants[1].0 {
EnumVariant::Capture {
named,
ident,
fields,
..
} => {
assert!(!named);
assert_eq!(ident, &parse_quote!(WithDefault));
assert_eq!(
fields,
&vec![
FieldDef {
ty: parse_quote!(Ident),
name: parse_quote!(_2),
is_inline: true,
is_optional: false,
},
FieldDef {
ty: parse_quote!(Expr),
name: parse_quote!(_3),
is_inline: true,
is_optional: false,
},
]
);
}
_ => panic!("Expected EnumVariant::Capture"),
}
reset_inline_counter();
let input = quote! {#(args: Enum {
syn::Ident,
Ty: Type,
FnArg: #(id: Ident): #(ty: Type),
WithDefault: #(@: Ident) = #(@: Expr) })
};
let result = parse_capture(input, ctx).unwrap();
let MatcherKind::Enum {
enum_name,
variants,
} = result.matcher.kind
else {
panic!("Expected Enum matcher kind");
};
assert_eq!(enum_name, expect_enum_name);
assert_eq!(variants.len(), 4);
match &variants[0].0 {
EnumVariant::Type { ident, ty } => {
assert_eq!(ident, &parse_quote!(Ident));
assert_eq!(ty, &parse_quote!(syn::Ident));
}
_ => panic!("Expected EnumVariant::Type"),
}
match &variants[1].0 {
EnumVariant::Type { ident, ty } => {
assert_eq!(ident, &parse_quote!(Ty));
assert_eq!(ty, &parse_quote!(Type));
}
_ => panic!("Expected EnumVariant::Type"),
}
match &variants[2].0 {
EnumVariant::Capture {
named,
ident,
fields,
..
} => {
assert!(named);
assert_eq!(ident, &parse_quote!(FnArg));
assert_eq!(
fields,
&vec![
FieldDef {
ty: parse_quote!(Ident),
name: parse_quote!(id),
is_inline: false,
is_optional: false,
},
FieldDef {
ty: parse_quote!(Type),
name: parse_quote!(ty),
is_inline: false,
is_optional: false,
},
]
);
}
_ => panic!("Expected EnumVariant::Capture"),
}
match &variants[3].0 {
EnumVariant::Capture {
named,
ident,
fields,
..
} => {
assert!(!named);
assert_eq!(ident, &parse_quote!(WithDefault));
assert_eq!(
fields,
&vec![
FieldDef {
ty: parse_quote!(Ident),
name: parse_quote!(_0),
is_inline: true,
is_optional: false,
},
FieldDef {
ty: parse_quote!(Expr),
name: parse_quote!(_1),
is_inline: true,
is_optional: false,
},
]
);
}
_ => panic!("Expected EnumVariant::Capture"),
}
}
#[test]
fn test_to_tokens_smoke() {
let mut compiler = Compiler::new();
let ctx = &mut ParseContext::default();
let capture: Capture = parse_capture(quote!(#(x: Ident)), ctx).unwrap();
let tokens = compiler.compile_capture(&capture);
assert!(!tokens.is_empty());
let spec_opt: Capture = parse_capture(quote!(#(x?: Ident)), ctx).unwrap();
let tokens_opt = compiler.compile_capture(&spec_opt);
assert!(tokens_opt.to_string().contains("Option"));
let spec_iter: Capture = parse_capture(quote!(#(x*[,]: Ident)), ctx).unwrap();
let tokens_iter = compiler.compile_capture(&spec_iter);
assert!(tokens_iter.to_string().contains("parse_terminated"));
let spec_iter_blank: Capture = parse_capture(quote!(#(x*: Ident)), ctx).unwrap();
let tokens_iter_blank = compiler.compile_capture(&spec_iter_blank);
let tokens_iter_blank = tokens_iter_blank.to_string();
assert!(tokens_iter_blank.contains("Vec"));
assert!(tokens_iter_blank.contains("cursor"));
assert!(!tokens_iter_blank.contains("parse_terminated"));
}
#[test]
fn test_error_missing_colon() {
let ctx = &mut ParseContext::default();
let input = quote!(#(name Ident));
let result = parse_capture(input, ctx);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"expected ':' after capture name"
);
}
#[test]
fn test_parse_empty_separator_named() {
let ctx = &mut ParseContext::default();
let input = quote!(#(name*[]: Ident));
let result = parse_capture(input, ctx).unwrap();
assert_eq!(result.quantity, Quantity::Many(None));
}
}