derive: refactor #[template] parsing

This allows parsing a `#[template]` attribute without expecting all
attributes to be set, which makes the parsing re-usable, which is a
prerequisit to implement `enum` variant handling.
This commit is contained in:
René Kijewski 2024-11-14 21:15:08 +01:00
parent 5772a6849e
commit cedc1d8aa5
2 changed files with 285 additions and 229 deletions

View File

@ -3,6 +3,7 @@ use std::collections::hash_map::{Entry, HashMap};
use std::fs::read_to_string; use std::fs::read_to_string;
use std::iter::FusedIterator; use std::iter::FusedIterator;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::{Arc, OnceLock}; use std::sync::{Arc, OnceLock};
use mime::Mime; use mime::Mime;
@ -11,6 +12,7 @@ use proc_macro2::Span;
use rustc_hash::FxBuildHasher; use rustc_hash::FxBuildHasher;
use syn::punctuated::Punctuated; use syn::punctuated::Punctuated;
use syn::spanned::Spanned; use syn::spanned::Spanned;
use syn::{Attribute, Expr, ExprLit, Ident, Lit, LitBool, LitStr, Meta, Token};
use crate::config::{Config, SyntaxAndCache}; use crate::config::{Config, SyntaxAndCache};
use crate::{CompileError, FileInfo, MsgValidEscapers, OnceMap}; use crate::{CompileError, FileInfo, MsgValidEscapers, OnceMap};
@ -40,7 +42,7 @@ impl TemplateInput<'_> {
args: &'n TemplateArgs, args: &'n TemplateArgs,
) -> Result<TemplateInput<'n>, CompileError> { ) -> Result<TemplateInput<'n>, CompileError> {
let TemplateArgs { let TemplateArgs {
source, source: (source, source_span),
block, block,
print, print,
escaping, escaping,
@ -53,15 +55,6 @@ impl TemplateInput<'_> {
// Validate the `source` and `ext` value together, since they are // Validate the `source` and `ext` value together, since they are
// related. In case `source` was used instead of `path`, the value // related. In case `source` was used instead of `path`, the value
// of `ext` is merged into a synthetic `path` value here. // of `ext` is merged into a synthetic `path` value here.
let &(ref source, source_span) = source.as_ref().ok_or_else(|| {
CompileError::new(
#[cfg(not(feature = "code-in-doc"))]
"specify one template argument `path` or `source`",
#[cfg(feature = "code-in-doc")]
"specify one template argument `path`, `source` or `in_doc`",
None,
)
})?;
let path = match (&source, &ext) { let path = match (&source, &ext) {
(Source::Path(path), _) => config.find_template(path, None, None)?, (Source::Path(path), _) => config.find_template(path, None, None)?,
(&Source::Source(_), Some(ext)) => { (&Source::Source(_), Some(ext)) => {
@ -117,7 +110,7 @@ impl TemplateInput<'_> {
extension_to_mime_type(ext_default_to_path(ext.as_deref(), &path).unwrap_or("txt")) extension_to_mime_type(ext_default_to_path(ext.as_deref(), &path).unwrap_or("txt"))
.to_string(); .to_string();
let empty_punctuated = syn::punctuated::Punctuated::new(); let empty_punctuated = Punctuated::new();
let fields = match ast.data { let fields = match ast.data {
syn::Data::Struct(ref struct_) => { syn::Data::Struct(ref struct_) => {
if let syn::Fields::Named(ref fields) = &struct_.fields { if let syn::Fields::Named(ref fields) = &struct_.fields {
@ -141,7 +134,7 @@ impl TemplateInput<'_> {
config, config,
syntax, syntax,
source, source,
source_span, source_span: *source_span,
block: block.as_deref(), block: block.as_deref(),
print: *print, print: *print,
escaper, escaper,
@ -285,9 +278,9 @@ impl TemplateInput<'_> {
} }
} }
#[derive(Debug, Default)] #[derive(Debug)]
pub(crate) struct TemplateArgs { pub(crate) struct TemplateArgs {
pub(crate) source: Option<(Source, Option<Span>)>, pub(crate) source: (Source, Option<Span>),
block: Option<String>, block: Option<String>,
print: Print, print: Print,
escaping: Option<String>, escaping: Option<String>,
@ -311,128 +304,61 @@ impl TemplateArgs {
)); ));
} }
// Check that an attribute called `template()` exists at least once and that it is let args = PartialTemplateArgs::new(&ast.attrs)?;
// the proper type (list). let Some(template) = args.template else {
let mut templates_attrs = ast return Err(CompileError::no_file_info(
.attrs "no attribute `template` found",
.iter() None,
.filter(|attr| attr.path().is_ident("template")) ));
.peekable();
let mut args = match templates_attrs.peek() {
Some(attr) => Self {
template_span: Some(attr.path().span()),
..Self::default()
},
None => {
return Err(CompileError::no_file_info(
"no attribute `template` found",
None,
));
}
}; };
let attrs = templates_attrs Ok(Self {
.map(|attr| { source: match args.source {
type Attrs = Punctuated<syn::Meta, syn::Token![,]>; Some((_, PartialTemplateArgsSource::Path(s))) => {
match attr.parse_args_with(Attrs::parse_terminated) { (Source::Path(s.value()), Some(s.span()))
Ok(args) => Ok(args),
Err(e) => Err(CompileError::no_file_info(
format!("unable to parse template arguments: {e}"),
Some(attr.path().span()),
)),
} }
}) Some((_, PartialTemplateArgsSource::Source(s))) => {
.flat_map(ResultIter::from); (Source::Source(s.value().into()), Some(s.span()))
}
// Loop over the meta attributes and find everything that we #[cfg(feature = "code-in-doc")]
// understand. Return a CompileError if something is not right. Some((ident, PartialTemplateArgsSource::InDoc(_))) => {
// `source` contains an enum that can represent `path` or `source`. source_from_docs(&ident, &args.meta_docs, ast)?
for item in attrs { }
let pair = match item? { None => {
syn::Meta::NameValue(pair) => pair,
v => {
return Err(CompileError::no_file_info( return Err(CompileError::no_file_info(
"unsupported attribute argument", #[cfg(not(feature = "code-in-doc"))]
Some(v.span()), "specify one template argument `path` or `source`",
#[cfg(feature = "code-in-doc")]
"specify one template argument `path`, `source` or `in_doc`",
Some(template.span()),
)); ));
} }
}; },
block: args.block.map(|(_, value)| value.value()),
let ident = match pair.path.get_ident() { print: args.print.map(|(_, _, value)| value).unwrap_or_default(),
Some(ident) => ident, escaping: args.escape.map(|(_, value)| value.value()),
None => unreachable!("not possible in syn::Meta::NameValue(…)"), ext: args.ext.as_ref().map(|(_, s)| s.value()),
}; ext_span: args.ext.as_ref().map(|(_, s)| s.span()),
syntax: args.syntax.map(|(_, value)| value.value()),
let mut value_expr = &pair.value; config: args.config.as_ref().map(|(_, s)| s.value()),
let value = loop { whitespace: args.whitespace.map(|(_, value)| value.value()),
match value_expr { template_span: Some(template.span()),
syn::Expr::Lit(lit) => break lit, config_span: args.config.as_ref().map(|(_, s)| s.span()),
syn::Expr::Group(group) => value_expr = &group.expr, })
v => {
return Err(CompileError::no_file_info(
format!("unsupported argument value type for `{ident}`"),
Some(v.span()),
));
}
}
};
if ident == "path" {
source_or_path(ident, value, &mut args.source, Source::Path)?;
args.ext_span = Some(value.span());
} else if ident == "source" {
source_or_path(ident, value, &mut args.source, |s| Source::Source(s.into()))?;
} else if ident == "in_doc" {
source_from_docs(ident, value, &mut args.source, ast)?;
} else if ident == "block" {
set_template_str_attr(ident, value, &mut args.block)?;
} else if ident == "print" {
if let syn::Lit::Str(s) = &value.lit {
args.print = match s.value().as_str() {
"all" => Print::All,
"ast" => Print::Ast,
"code" => Print::Code,
"none" => Print::None,
v => {
return Err(CompileError::no_file_info(
format!("invalid value for `print` option: {v}"),
Some(s.span()),
));
}
};
} else {
return Err(CompileError::no_file_info(
"`print` value must be string literal",
Some(value.lit.span()),
));
}
} else if ident == "escape" {
set_template_str_attr(ident, value, &mut args.escaping)?;
} else if ident == "ext" {
set_template_str_attr(ident, value, &mut args.ext)?;
args.ext_span = Some(value.span());
} else if ident == "syntax" {
set_template_str_attr(ident, value, &mut args.syntax)?;
} else if ident == "config" {
set_template_str_attr(ident, value, &mut args.config)?;
args.config_span = Some(value.span());
} else if ident == "whitespace" {
set_template_str_attr(ident, value, &mut args.whitespace)?;
} else {
return Err(CompileError::no_file_info(
format!("unsupported attribute key `{ident}` found"),
Some(ident.span()),
));
}
}
Ok(args)
} }
pub(crate) fn fallback() -> Self { pub(crate) fn fallback() -> Self {
Self { Self {
source: Some((Source::Source("".into()), None)), source: (Source::Source("".into()), None),
block: None,
print: Print::default(),
escaping: None,
ext: Some("txt".to_string()), ext: Some("txt".to_string()),
..Self::default() ext_span: None,
syntax: None,
config: None,
whitespace: None,
template_span: None,
config_span: None,
} }
} }
@ -444,47 +370,22 @@ impl TemplateArgs {
/// Try to find the source in the comment, in a `rinja` code block. /// Try to find the source in the comment, in a `rinja` code block.
/// ///
/// This is only done if no path or source was given in the `#[template]` attribute. /// This is only done if no path or source was given in the `#[template]` attribute.
#[cfg(feature = "code-in-doc")]
fn source_from_docs( fn source_from_docs(
name: &syn::Ident, name: &Ident,
value: &syn::ExprLit, docs: &[Attribute],
dest: &mut Option<(Source, Option<Span>)>,
ast: &syn::DeriveInput, ast: &syn::DeriveInput,
) -> Result<(), CompileError> { ) -> Result<(Source, Option<Span>), CompileError> {
match &value.lit { let (span, source) = collect_comment_blocks(name, docs, ast)?;
syn::Lit::Bool(syn::LitBool { value, .. }) => { let source = strip_common_ws_prefix(source);
if !value { let source = collect_rinja_code_blocks(name, ast, source)?;
return Ok(()); Ok((source, span))
}
}
lit => {
return Err(CompileError::no_file_info(
"argument `in_doc` expects as boolean value",
Some(lit.span()),
));
}
};
#[cfg(not(feature = "code-in-doc"))]
{
let _ = (name, dest, ast);
Err(CompileError::no_file_info(
"enable feature `code-in-doc` to use `in_doc` argument",
Some(name.span()),
))
}
#[cfg(feature = "code-in-doc")]
{
ensure_source_once(name, dest)?;
let (span, source) = collect_comment_blocks(name, ast)?;
let source = strip_common_ws_prefix(source);
let source = collect_rinja_code_blocks(name, ast, source)?;
*dest = Some((source, span));
Ok(())
}
} }
#[cfg(feature = "code-in-doc")] #[cfg(feature = "code-in-doc")]
fn collect_comment_blocks( fn collect_comment_blocks(
name: &syn::Ident, name: &Ident,
docs: &[Attribute],
ast: &syn::DeriveInput, ast: &syn::DeriveInput,
) -> Result<(Option<Span>, String), CompileError> { ) -> Result<(Option<Span>, String), CompileError> {
let mut span: Option<Span> = None; let mut span: Option<Span> = None;
@ -502,9 +403,9 @@ fn collect_comment_blocks(
}; };
let mut source = String::new(); let mut source = String::new();
for a in &ast.attrs { for a in docs {
// is a comment? // is a comment?
let syn::Meta::NameValue(kv) = &a.meta else { let Meta::NameValue(kv) = &a.meta else {
continue; continue;
}; };
if !kv.path.is_ident("doc") { if !kv.path.is_ident("doc") {
@ -515,12 +416,12 @@ fn collect_comment_blocks(
let mut value = &kv.value; let mut value = &kv.value;
let value = loop { let value = loop {
match value { match value {
syn::Expr::Lit(lit) => break lit, Expr::Lit(lit) => break lit,
syn::Expr::Group(group) => value = &group.expr, Expr::Group(group) => value = &group.expr,
_ => continue, _ => continue,
} }
}; };
let syn::Lit::Str(value) = &value.lit else { let Lit::Str(value) = &value.lit else {
continue; continue;
}; };
@ -536,7 +437,7 @@ fn collect_comment_blocks(
} }
#[cfg(feature = "code-in-doc")] #[cfg(feature = "code-in-doc")]
fn no_rinja_code_block(name: &syn::Ident, ast: &syn::DeriveInput) -> CompileError { fn no_rinja_code_block(name: &Ident, ast: &syn::DeriveInput) -> CompileError {
let kind = match &ast.data { let kind = match &ast.data {
syn::Data::Struct(_) => "struct", syn::Data::Struct(_) => "struct",
syn::Data::Enum(_) => "enum", syn::Data::Enum(_) => "enum",
@ -581,7 +482,7 @@ fn strip_common_ws_prefix(source: String) -> String {
#[cfg(feature = "code-in-doc")] #[cfg(feature = "code-in-doc")]
fn collect_rinja_code_blocks( fn collect_rinja_code_blocks(
name: &syn::Ident, name: &Ident,
ast: &syn::DeriveInput, ast: &syn::DeriveInput,
source: String, source: String,
) -> Result<Source, CompileError> { ) -> Result<Source, CompileError> {
@ -637,62 +538,6 @@ impl<I: Iterator, E> Iterator for ResultIter<I, E> {
impl<I: FusedIterator, E> FusedIterator for ResultIter<I, E> {} impl<I: FusedIterator, E> FusedIterator for ResultIter<I, E> {}
fn source_or_path(
name: &syn::Ident,
value: &syn::ExprLit,
dest: &mut Option<(Source, Option<Span>)>,
ctor: fn(String) -> Source,
) -> Result<(), CompileError> {
ensure_source_once(name, dest)?;
if let syn::Lit::Str(s) = &value.lit {
*dest = Some((ctor(s.value()), Some(value.span())));
Ok(())
} else {
Err(CompileError::no_file_info(
format!("`{name}` value must be string literal"),
Some(value.lit.span()),
))
}
}
fn ensure_source_once(
name: &syn::Ident,
source: &mut Option<(Source, Option<Span>)>,
) -> Result<(), CompileError> {
if source.is_none() {
Ok(())
} else {
Err(CompileError::no_file_info(
#[cfg(feature = "code-in-doc")]
"must specify `source`, `path` or `is_doc` exactly once",
#[cfg(not(feature = "code-in-doc"))]
"must specify `source` or `path` exactly once",
Some(name.span()),
))
}
}
fn set_template_str_attr(
name: &syn::Ident,
value: &syn::ExprLit,
dest: &mut Option<String>,
) -> Result<(), CompileError> {
if dest.is_some() {
Err(CompileError::no_file_info(
format!("attribute `{name}` already set"),
Some(name.span()),
))
} else if let syn::Lit::Str(s) = &value.lit {
*dest = Some(s.value());
Ok(())
} else {
Err(CompileError::no_file_info(
format!("`{name}` value must be string literal"),
Some(value.lit.span()),
))
}
}
#[inline] #[inline]
fn ext_default_to_path<'a>(ext: Option<&'a str>, path: &'a Path) -> Option<&'a str> { fn ext_default_to_path<'a>(ext: Option<&'a str>, path: &'a Path) -> Option<&'a str> {
ext.or_else(|| extension(path)) ext.or_else(|| extension(path))
@ -731,6 +576,20 @@ impl Default for Print {
} }
} }
impl FromStr for Print {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"all" => Ok(Self::All),
"ast" => Ok(Self::Ast),
"code" => Ok(Self::Code),
"none" => Ok(Self::None),
_ => Err(format!("invalid value for `print` option: {s}")),
}
}
}
pub(crate) fn extension_to_mime_type(ext: &str) -> Mime { pub(crate) fn extension_to_mime_type(ext: &str) -> Mime {
let basic_type = mime_guess::from_ext(ext).first_or_octet_stream(); let basic_type = mime_guess::from_ext(ext).first_or_octet_stream();
for (simple, utf_8) in &TEXT_TYPES { for (simple, utf_8) in &TEXT_TYPES {
@ -799,6 +658,207 @@ pub(crate) fn get_template_source(
) )
} }
#[derive(Default)]
pub(crate) struct PartialTemplateArgs {
pub(crate) template: Option<Ident>,
pub(crate) meta_docs: Vec<Attribute>,
pub(crate) source: Option<(Ident, PartialTemplateArgsSource)>,
pub(crate) block: Option<(Ident, LitStr)>,
pub(crate) print: Option<(Ident, LitStr, Print)>,
pub(crate) escape: Option<(Ident, LitStr)>,
pub(crate) ext: Option<(Ident, LitStr)>,
pub(crate) syntax: Option<(Ident, LitStr)>,
pub(crate) config: Option<(Ident, LitStr)>,
pub(crate) whitespace: Option<(Ident, LitStr)>,
}
pub(crate) enum PartialTemplateArgsSource {
Path(LitStr),
Source(LitStr),
#[cfg(feature = "code-in-doc")]
InDoc(#[allow(dead_code)] LitBool),
}
// implement PartialTemplateArgs::new()
const _: () = {
impl PartialTemplateArgs {
pub(crate) fn new(attrs: &[Attribute]) -> Result<Self, CompileError> {
new(attrs)
}
}
#[inline]
fn new(attrs: &[Attribute]) -> Result<PartialTemplateArgs, CompileError> {
let mut this = PartialTemplateArgs::default();
for attr in attrs {
let Some(ident) = attr.path().get_ident() else {
continue;
};
if ident == "doc" {
this.meta_docs.push(attr.clone());
continue;
} else if ident == "template" {
this.template = Some(ident.clone());
} else {
continue;
}
let args = attr
.parse_args_with(<Punctuated<Meta, Token![,]>>::parse_terminated)
.map_err(|e| {
CompileError::no_file_info(
format!("unable to parse template arguments: {e}"),
Some(attr.path().span()),
)
})?;
for arg in args {
let pair = match arg {
Meta::NameValue(pair) => pair,
v => {
return Err(CompileError::no_file_info(
"unsupported attribute argument",
Some(v.span()),
));
}
};
let ident = match pair.path.get_ident() {
Some(ident) => ident,
None => unreachable!("not possible in syn::Meta::NameValue(…)"),
};
let mut value_expr = pair.value;
let value = loop {
match value_expr {
Expr::Lit(lit) => break lit,
Expr::Group(group) => value_expr = *group.expr,
v => {
return Err(CompileError::no_file_info(
format!("unsupported argument value type for `{ident}`"),
Some(v.span()),
));
}
}
};
if ident == "path" {
ensure_source_only_once(ident, &this.source)?;
let value = get_strlit(ident, value)?;
this.source = Some((ident.clone(), PartialTemplateArgsSource::Path(value)));
} else if ident == "source" {
ensure_source_only_once(ident, &this.source)?;
let value = get_strlit(ident, value)?;
this.source = Some((ident.clone(), PartialTemplateArgsSource::Source(value)));
} else if ident == "in_doc" {
let value = get_strbool(ident, value)?;
if !value.value() {
continue;
}
ensure_source_only_once(ident, &this.source)?;
#[cfg(not(feature = "code-in-doc"))]
{
return Err(CompileError::no_file_info(
"enable feature `code-in-doc` to use `in_doc` argument",
Some(ident.span()),
));
}
#[cfg(feature = "code-in-doc")]
{
this.source =
Some((ident.clone(), PartialTemplateArgsSource::InDoc(value)));
}
} else if ident == "block" {
set_strlit_pair(ident, value, &mut this.block)?;
} else if ident == "print" {
ensure_only_once(ident, &mut this.print)?;
let str_value = get_strlit(ident, value)?;
let value = str_value
.value()
.parse()
.map_err(|msg| CompileError::no_file_info(msg, Some(ident.span())))?;
this.print = Some((ident.clone(), str_value, value));
} else if ident == "escape" {
set_strlit_pair(ident, value, &mut this.escape)?;
} else if ident == "ext" {
set_strlit_pair(ident, value, &mut this.ext)?;
} else if ident == "syntax" {
set_strlit_pair(ident, value, &mut this.syntax)?;
} else if ident == "config" {
set_strlit_pair(ident, value, &mut this.config)?;
} else if ident == "whitespace" {
set_strlit_pair(ident, value, &mut this.whitespace)?;
} else {
return Err(CompileError::no_file_info(
format!("unsupported attribute key `{ident}` found"),
Some(ident.span()),
));
}
}
}
Ok(this)
}
fn set_strlit_pair(
name: &Ident,
value: ExprLit,
dest: &mut Option<(Ident, LitStr)>,
) -> Result<(), CompileError> {
ensure_only_once(name, dest)?;
let value = get_strlit(name, value)?;
*dest = Some((name.clone(), value));
Ok(())
}
fn ensure_only_once<T>(name: &Ident, dest: &mut Option<T>) -> Result<(), CompileError> {
if dest.is_none() {
Ok(())
} else {
Err(CompileError::no_file_info(
format!("attribute `{name}` already set"),
Some(name.span()),
))
}
}
fn get_strlit(name: &Ident, value: ExprLit) -> Result<LitStr, CompileError> {
if let Lit::Str(s) = value.lit {
Ok(s)
} else {
Err(CompileError::no_file_info(
format!("`{name}` value must be string literal"),
Some(value.lit.span()),
))
}
}
fn get_strbool(name: &Ident, value: ExprLit) -> Result<LitBool, CompileError> {
if let Lit::Bool(s) = value.lit {
Ok(s)
} else {
Err(CompileError::no_file_info(
format!("argument `{name}` expects as boolean value"),
Some(value.lit.span()),
))
}
}
fn ensure_source_only_once(
name: &Ident,
source: &Option<(Ident, PartialTemplateArgsSource)>,
) -> Result<(), CompileError> {
if source.is_some() {
return Err(CompileError::no_file_info(
#[cfg(feature = "code-in-doc")]
"must specify `source`, `path` or `is_doc` exactly once",
#[cfg(not(feature = "code-in-doc"))]
"must specify `source` or `path` exactly once",
Some(name.span()),
));
}
Ok(())
}
};
const JINJA_EXTENSIONS: &[&str] = &["j2", "jinja", "jinja2", "rinja"]; const JINJA_EXTENSIONS: &[&str] = &["j2", "jinja", "jinja2", "rinja"];
#[cfg(test)] #[cfg(test)]

View File

@ -183,11 +183,7 @@ pub(crate) fn build_template(ast: &syn::DeriveInput) -> Result<String, CompileEr
let mut result = build_template_inner(ast, &template_args); let mut result = build_template_inner(ast, &template_args);
if let Err(err) = &mut result { if let Err(err) = &mut result {
if err.span.is_none() { if err.span.is_none() {
err.span = template_args err.span = template_args.source.1.or(template_args.template_span);
.source
.as_ref()
.and_then(|(_, span)| *span)
.or(template_args.template_span);
} }
} }
result result