#![allow(
clippy::needless_continue,
clippy::nonminimal_bool,
clippy::if_not_else,
clippy::allow_attributes,
clippy::redundant_closure_call,
reason = "derive macros (darling's FromAttributes, derive_syn_parse's #[call] closures) emit code tripping these lints, unreachable from our own logic"
)]
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{Expr, Ident, LitStr, Path, Token, Type, Visibility};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SizeConstraint {
Exact(usize),
AtMost(usize),
Below(usize),
AtLeast(usize),
Range(usize, usize),
RangeHalfOpen(usize, usize),
}
impl Parse for SizeConstraint {
fn parse(input: ParseStream) -> syn::Result<Self> {
let expr: syn::Expr = input.parse()?;
match &expr {
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Int(n),
..
}) => Ok(Self::Exact(n.base10_parse()?)),
syn::Expr::Range(r) => {
let closed = matches!(r.limits, syn::RangeLimits::Closed(_));
let start = r.start.as_deref().map(expr_to_usize).transpose()?;
let end = r.end.as_deref().map(expr_to_usize).transpose()?;
let err = |msg: &str| Err(syn::Error::new_spanned(&expr, msg.to_owned()));
match (start, end, closed) {
(None, None, _) => err(
"`size(..)` places no constraint; specify a bound (e.g. `..=N`, `..N`, `N..`, `N..M`, `N..=M`) or remove it",
),
(None, Some(e), true) => Ok(Self::AtMost(e)),
(None, Some(e), false) => {
if e == 0 {
err("`size(..0)` is unsatisfiable: a size is never negative")
} else {
Ok(Self::Below(e))
}
}
(Some(s), None, false) => Ok(Self::AtLeast(s)),
(Some(s), Some(e), true) => {
if s > e {
err("empty `size(N..=M)` range: the low bound exceeds the high bound")
} else {
Ok(Self::Range(s, e))
}
}
(Some(s), Some(e), false) => {
if s >= e {
err(
"empty `size(N..M)` range: the low bound is not below the high bound",
)
} else {
Ok(Self::RangeHalfOpen(s, e))
}
}
(Some(_), None, true) => err(
"unsupported range shape (an inclusive range needs a high bound: `N..=M`)",
),
}
}
_ => Err(syn::Error::new_spanned(
&expr,
"expected integer or range (e.g. `64`, `..=128`, `..64`, `32..`, `32..=64`, `32..64`)",
)),
}
}
}
fn expr_to_usize(expr: &syn::Expr) -> syn::Result<usize> {
match expr {
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Int(n),
..
}) => n.base10_parse(),
_ => Err(syn::Error::new_spanned(expr, "expected integer literal")),
}
}
impl darling::FromMeta for SizeConstraint {
fn from_meta(item: &syn::Meta) -> darling::Result<Self> {
SizeAttr::from_meta(item).map(|attr| attr.constraint)
}
}
#[derive(Debug, Clone)]
pub struct SizeAttr {
pub constraint: SizeConstraint,
pub span: proc_macro2::Span,
}
impl darling::FromMeta for SizeAttr {
fn from_meta(item: &syn::Meta) -> darling::Result<Self> {
match item {
syn::Meta::List(list) => {
let constraint: SizeConstraint = syn::parse2(list.tokens.clone())
.map_err(|e| darling::Error::custom(e).with_span(&list.tokens))?;
Ok(Self {
constraint,
span: size_arg_span(list),
})
}
syn::Meta::Path(_) | syn::Meta::NameValue(_) => Err(darling::Error::custom(
"expected `size(N)`, `size(..=N)`, `size(..N)`, `size(N..)`, `size(N..=M)`, or `size(N..M)`",
)
.with_span(item)),
}
}
}
fn size_arg_span(list: &syn::MetaList) -> proc_macro2::Span {
let group = list.delimiter.span().join();
let spans: Vec<proc_macro2::Span> = list.tokens.clone().into_iter().map(|t| t.span()).collect();
match spans.as_slice() {
[] => group,
[single] => *single,
[first, .., last] => first.join(*last).unwrap_or(group),
}
}
#[derive(Debug, Clone, Copy)]
pub struct ExitCodeAttr {
pub value: u8,
pub span: proc_macro2::Span,
}
impl darling::FromMeta for ExitCodeAttr {
fn from_meta(item: &syn::Meta) -> darling::Result<Self> {
let lit = match item {
syn::Meta::NameValue(nv) => &nv.value,
other => {
return Err(darling::Error::custom("expected `exit_code = N`").with_span(other));
}
};
let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Int(int),
..
}) = lit
else {
return Err(
darling::Error::custom("expected an integer literal in `1..=255`").with_span(lit),
);
};
let value: u16 = int.base10_parse().map_err(darling::Error::custom)?;
let value = u8::try_from(value)
.ok()
.filter(|&v| v != 0)
.ok_or_else(|| {
darling::Error::custom("exit code must be in `1..=255`").with_span(int)
})?;
Ok(Self {
value,
span: int.span(),
})
}
}
#[derive(Debug, Default, darling::FromMeta)]
pub struct EnumContainerAttrsInner {
#[darling(default)]
pub module: Option<crate::utils::MaybeAloneOopsieValue<Ident>>,
#[darling(default)]
pub suffix: Option<crate::utils::MaybeAloneOopsieValue<String>>,
#[darling(default)]
pub size: Option<SizeAttr>,
#[darling(default)]
pub path: Option<Path>,
#[darling(default)]
pub exit_code: Option<ExitCodeAttr>,
}
#[derive(Debug, Clone)]
pub enum ModuleSetting {
On(Option<Ident>),
Off,
}
#[derive(Debug, Clone)]
pub enum SuffixSetting {
Off,
Custom(String),
}
impl EnumContainerAttrsInner {
pub fn effective_suffix(&self, is_enum: bool) -> SuffixSetting {
use crate::utils::MaybeAloneOopsieValue as M;
use crate::utils::SuffixDefault;
match &self.suffix {
None => match crate::utils::manifest_suffix_default() {
Some(SuffixDefault::Off) => SuffixSetting::Off,
Some(SuffixDefault::Name(s)) => SuffixSetting::Custom(s),
None if is_enum => SuffixSetting::Off,
None => SuffixSetting::Custom("Oopsie".into()),
},
Some(M::Alone | M::Bool(true)) => SuffixSetting::Custom("Oopsie".into()),
Some(M::Bool(false)) => SuffixSetting::Off,
Some(M::Value(s)) => SuffixSetting::Custom(s.clone()),
}
}
pub fn effective_module(&self, is_enum: bool) -> ModuleSetting {
use crate::utils::MaybeAloneOopsieValue as M;
match &self.module {
None => match crate::utils::manifest_module_default() {
Some(true) => ModuleSetting::On(None),
Some(false) => ModuleSetting::Off,
None if is_enum => ModuleSetting::On(None),
None => ModuleSetting::Off,
},
Some(M::Alone | M::Bool(true)) => ModuleSetting::On(None),
Some(M::Bool(false)) => ModuleSetting::Off,
Some(M::Value(name)) => ModuleSetting::On(Some(name.clone())),
}
}
pub fn oopsie_path(&self) -> Path {
self.path
.clone()
.unwrap_or_else(|| syn::parse_quote! { ::oopsie })
}
}
#[derive(Debug, Default, darling::FromAttributes)]
#[darling(attributes(oopsie))]
pub struct EnumContainerAttrs {
#[darling(flatten)]
pub inner: EnumContainerAttrsInner,
#[darling(default)]
pub vis: Option<crate::utils::SynParse<Visibility>>,
}
impl std::ops::Deref for EnumContainerAttrs {
type Target = EnumContainerAttrsInner;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl EnumContainerAttrs {
pub fn from_attrs(attrs: &[syn::Attribute]) -> syn::Result<Self> {
use darling::FromAttributes as _;
let (short_display, attrs) = extract_short_display(attrs)?;
if let Some(short) = short_display {
return Err(syn::Error::new_spanned(
&short.format_str,
"display strings don't go on the enum itself; put them in an \
`#[oopsie(\"...\")]` attribute on each enum variant",
));
}
Self::from_attributes(&attrs).map_err(syn::Error::from)
}
#[inline]
pub fn visibility(&self) -> Option<&Visibility> {
self.vis.as_deref()
}
}
#[derive(Debug, Default, darling::FromMeta)]
pub struct VariantAttrsInner {
#[darling(default)]
pub display: Option<DisplayAttr>,
#[darling(default)]
pub traced: crate::utils::BetterFlag<true>,
#[darling(default)]
pub transparent: bool,
#[darling(default)]
pub help: Option<DisplayAttr>,
#[darling(default)]
pub code: Option<DisplayAttr>,
#[darling(default)]
pub exit_code: Option<ExitCodeAttr>,
#[darling(default, multiple, rename = "provide")]
pub provides: Vec<ProvideAttr>,
}
#[derive(Debug, Clone, derive_syn_parse::Parse)]
pub struct DisplayAttr {
pub format_str: LitStr,
#[prefix(Option<Token![,]> as c)]
#[call(|s| if c.is_some() { Punctuated::parse_terminated(s) } else { Ok(Punctuated::new()) })]
pub args: Punctuated<Expr, Token![,]>,
}
impl darling::FromMeta for DisplayAttr {
fn from_meta(item: &syn::Meta) -> darling::Result<Self> {
match item {
syn::Meta::List(list) => syn::parse2(list.tokens.clone())
.map_err(|e| darling::Error::custom(e).with_span(&list.tokens)),
syn::Meta::NameValue(nv) => match &nv.value {
Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) => Ok(Self {
format_str: s.clone(),
args: Punctuated::new(),
}),
other => Err(darling::Error::custom("expected string literal").with_span(other)),
},
syn::Meta::Path(p) => Err(darling::Error::custom("expected value").with_span(p)),
}
}
}
const VARIANT_KEYWORDS: &[&str] = &[
"display",
"traced",
"transparent",
"help",
"code",
"exit_code",
"provide",
];
const CONTAINER_KEYWORDS: &[&str] = &["module", "suffix", "size", "path", "vis", "exit_code"];
#[derive(Clone, Copy)]
pub enum DisplayScope {
Variant,
Struct,
}
impl DisplayScope {
fn is_keyword(self, ident: &str) -> bool {
let in_set = |set: &[&str]| set.contains(&ident);
match self {
Self::Variant => in_set(VARIANT_KEYWORDS),
Self::Struct => in_set(VARIANT_KEYWORDS) || in_set(CONTAINER_KEYWORDS),
}
}
}
fn bare_path_ident(expr: &Expr) -> Option<&Ident> {
let Expr::Path(p) = expr else { return None };
if p.qself.is_some() {
return None;
}
let seg = match p.path.segments.len() {
1 => &p.path.segments[0],
_ => return None,
};
if p.path.leading_colon.is_some() || !matches!(seg.arguments, syn::PathArguments::None) {
return None;
}
Some(&seg.ident)
}
impl DisplayAttr {
pub fn reject_keyword_args(
&self,
fields: &syn::Fields,
scope: DisplayScope,
) -> syn::Result<()> {
let field_named = |ident: &Ident| matches!(fields, syn::Fields::Named(f) if f.named.iter().any(|f| f.ident.as_ref() == Some(ident)));
for arg in &self.args {
let Some(ident) = bare_path_ident(arg) else {
continue;
};
if field_named(ident) || !scope.is_keyword(&ident.to_string()) {
continue;
}
let fmt = self.format_str.value();
return Err(syn::Error::new_spanned(
ident,
format!(
"`{ident}` is an `#[oopsie(...)]` keyword, not a display format \
argument; give it its own attribute: \
`#[oopsie(\"{fmt}\")] #[oopsie({ident})]`"
),
));
}
Ok(())
}
pub fn is_static(&self) -> bool {
self.args.is_empty() && !format_str_has_placeholder(&self.format_str.value())
}
pub fn static_lit(&self) -> syn::Result<LitStr> {
let mut value = self.format_str.value();
if let Some(pos) = unmatched_close_brace(&value) {
return Err(syn::Error::new_spanned(
&self.format_str,
format!(
"unmatched `}}` at byte {pos} in format string; \
write `}}}}` for a literal `}}`"
),
));
}
unescape_format_braces(&mut value);
Ok(LitStr::new(&value, self.format_str.span()))
}
}
fn format_str_has_placeholder(s: &str) -> bool {
let b = s.as_bytes();
let mut i = 0;
while i < b.len() {
if b[i] == b'{' {
if b.get(i + 1) == Some(&b'{') {
i += 2;
} else {
return true;
}
} else {
i += 1;
}
}
false
}
fn unmatched_close_brace(s: &str) -> Option<usize> {
let b = s.as_bytes();
let mut i = 0;
while i < b.len() {
if b[i] == b'}' {
if b.get(i + 1) == Some(&b'}') {
i += 2;
} else {
return Some(i);
}
} else {
i += 1;
}
}
None
}
fn unescape_format_braces(s: &mut String) {
let (mut after_open, mut after_close) = (false, false);
s.retain(|c| match c {
'{' if after_open => {
after_open = false;
false
}
'{' => {
after_open = true;
after_close = false;
true
}
'}' if after_close => {
after_close = false;
false
}
'}' => {
after_close = true;
after_open = false;
true
}
_ => {
after_open = false;
after_close = false;
true
}
});
}
#[derive(Debug, Default, darling::FromAttributes)]
#[darling(attributes(oopsie))]
pub struct VariantAttrs {
#[darling(flatten)]
pub inner: VariantAttrsInner,
#[darling(default)]
pub vis: Option<crate::utils::SynParse<Visibility>>,
}
impl std::ops::Deref for VariantAttrs {
type Target = VariantAttrsInner;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl VariantAttrs {
#[inline]
pub fn visibility(&self) -> Option<&Visibility> {
self.vis.as_deref()
}
pub fn from_attrs(attrs: &[syn::Attribute]) -> syn::Result<Self> {
use darling::FromAttributes as _;
let (short_display, attrs) = extract_short_display(attrs)?;
let mut result = Self::from_attributes(&attrs).map_err(syn::Error::from)?;
if let Some(short) = short_display {
merge_short_display(&mut result.inner.display, short)?;
}
Ok(result)
}
}
#[derive(Debug, Default, darling::FromAttributes)]
#[darling(attributes(oopsie))]
pub struct StructAttrs {
#[darling(flatten)]
pub container: EnumContainerAttrsInner,
#[darling(default)]
pub vis: Option<crate::utils::SynParse<Visibility>>,
#[darling(default)]
pub display: Option<DisplayAttr>,
#[darling(default)]
pub transparent: bool,
#[darling(default)]
pub help: Option<DisplayAttr>,
#[darling(default)]
pub code: Option<DisplayAttr>,
#[darling(default, multiple, rename = "provide")]
pub provides: Vec<ProvideAttr>,
}
impl StructAttrs {
#[inline]
pub fn visibility(&self) -> Option<&Visibility> {
self.vis.as_deref()
}
pub fn from_attrs(attrs: &[syn::Attribute]) -> syn::Result<Self> {
use darling::FromAttributes as _;
let (short_display, attrs) = extract_short_display(attrs)?;
let mut result = Self::from_attributes(&attrs).map_err(syn::Error::from)?;
if let Some(short) = short_display {
merge_short_display(&mut result.display, short)?;
}
Ok(result)
}
}
fn merge_short_display(target: &mut Option<DisplayAttr>, short: DisplayAttr) -> syn::Result<()> {
if target.is_some() {
return Err(syn::Error::new_spanned(
&short.format_str,
"duplicate display: short form and `display(...)` cannot both be specified",
));
}
*target = Some(short);
Ok(())
}
#[derive(Clone, Debug, Default, darling::FromMeta)]
pub struct ForwardArgs {
#[darling(default)]
pub backtrace: crate::utils::BetterFlag<true>,
#[darling(default)]
pub spantrace: crate::utils::BetterFlag<true>,
#[darling(default)]
pub location: crate::utils::BetterFlag<false>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ResolvedForward {
pub backtrace: bool,
pub spantrace: bool,
pub location: bool,
}
impl ResolvedForward {
pub fn resolve(setting: &crate::utils::FieldSetting<false, ForwardArgs>) -> Self {
if !setting.is_enabled() {
return Self::default();
}
let s = setting.opt_settings();
Self {
backtrace: s.is_none_or(|a| a.backtrace.is_enabled()),
spantrace: s.is_none_or(|a| a.spantrace.is_enabled()),
location: s.is_some_and(|a| a.location.is_enabled()),
}
}
pub const fn any(self) -> bool {
self.backtrace || self.spantrace || self.location
}
}
pub fn field_forward(field: &syn::Field) -> syn::Result<ResolvedForward> {
use darling::FromAttributes as _;
let attrs = FieldAttrs::from_attributes(&field.attrs).map_err(syn::Error::from)?;
Ok(ResolvedForward::resolve(&attrs.forward))
}
#[expect(
clippy::struct_excessive_bools,
reason = "each boolean attribute keyword maps to its own bool field"
)]
#[derive(Debug, Default, darling::FromAttributes)]
#[darling(attributes(oopsie))]
pub struct FieldAttrs {
#[darling(default)]
pub from: SourceKind,
#[darling(default)]
pub capture: crate::utils::BetterFlag<false>,
#[darling(default, multiple)]
pub provide: Vec<ProvideAttr>,
#[darling(default)]
pub backtrace: bool,
#[darling(default)]
pub spantrace: bool,
#[darling(default)]
pub traces: bool,
#[darling(default)]
pub location: bool,
#[darling(default)]
pub help: bool,
#[darling(default)]
pub forward: crate::utils::FieldSetting<false, ForwardArgs>,
}
#[derive(Debug, Default)]
pub enum SourceKind {
#[default]
No,
Disabled,
Yes,
Transformed {
source_type: Box<Type>,
transform: Expr,
},
AutoBoxed { source_type: Box<Type> },
}
#[derive(derive_syn_parse::Parse)]
struct SourceKindTransform {
ty: Type,
#[prefix(Token![,])]
transform: Expr,
}
impl darling::FromMeta for SourceKind {
fn from_word() -> darling::Result<Self> {
Ok(Self::Yes)
}
fn from_meta(item: &syn::Meta) -> darling::Result<Self> {
match item {
syn::Meta::Path(_) => Ok(Self::Yes),
syn::Meta::List(list) => {
if let Ok(b) = syn::parse2::<syn::LitBool>(list.tokens.clone()) {
return Ok(if b.value { Self::Yes } else { Self::Disabled });
}
let parsed: SourceKindTransform = syn::parse2(list.tokens.clone())
.map_err(|e| darling::Error::custom(e).with_span(&list.tokens))?;
Ok(Self::Transformed {
source_type: Box::new(parsed.ty),
transform: parsed.transform,
})
}
syn::Meta::NameValue(nv) => Err(darling::Error::custom(
"`from` does not accept a `= value` form; use `from(false)` to opt out or `from(Type, transform)` for a custom source type",
)
.with_span(&nv.value)),
}
}
fn from_none() -> Option<Self> {
Some(Self::No)
}
}
#[derive(Debug, Clone, derive_syn_parse::Parse)]
pub struct ProvideAttr {
pub ref_kw: Option<Token![ref]>,
#[parse_if(ref_kw.is_some())]
#[expect(
dead_code,
reason = "syntactic punctuation captured by the parser but never read"
)]
pub ref_comma: Option<Token![,]>,
pub provided_type: Type,
#[prefix(Token![=>])]
pub expr: Expr,
}
impl ProvideAttr {
#[inline]
pub const fn is_ref(&self) -> bool {
self.ref_kw.is_some()
}
}
impl darling::FromMeta for ProvideAttr {
fn from_meta(item: &syn::Meta) -> darling::Result<Self> {
match item {
syn::Meta::List(list) => syn::parse2(list.tokens.clone())
.map_err(|e| darling::Error::custom(e).with_span(&list.tokens)),
other => Err(darling::Error::custom(
"expected `provide(ref, Type => expr)` or `provide(Type => expr)`",
)
.with_span(other)),
}
}
}
impl FieldAttrs {
pub fn from_field(field: &syn::Field) -> syn::Result<Self> {
use darling::FromAttributes as _;
let mut result = Self::from_attributes(&field.attrs).map_err(syn::Error::from)?;
if result.backtrace || result.spantrace || result.traces || result.location {
if matches!(result.capture, crate::utils::BetterFlag::Disabled) {
return Err(syn::Error::new_spanned(
field,
"`capture(false)` cannot be combined with `backtrace`/`spantrace`/`traces`/`location`",
));
}
result.capture = crate::utils::BetterFlag::Enabled;
}
if matches!(result.from, SourceKind::No)
&& let Some(ident) = &field.ident
&& ident == "source"
{
result.from = SourceKind::Yes;
}
if matches!(result.capture, crate::utils::BetterFlag::Default)
&& (crate::traced::field_detect::is_backtrace_type(&field.ty)
|| crate::traced::field_detect::is_spantrace_type(&field.ty)
|| crate::traced::field_detect::is_traces_type(&field.ty)
|| crate::traced::field_detect::is_location_type(&field.ty))
{
result.capture = crate::utils::BetterFlag::Enabled;
}
if matches!(result.from, SourceKind::Yes)
&& let Some(inner) = crate::traced::field_detect::extract_boxed_inner(&field.ty)
&& !matches!(
crate::traced::field_detect::peel_groups(inner),
syn::Type::TraitObject(_)
)
{
result.from = SourceKind::AutoBoxed {
source_type: Box::new(inner.clone()),
};
}
Ok(result)
}
pub const fn is_source(&self) -> bool {
match self.from {
SourceKind::No | SourceKind::Disabled => false,
SourceKind::Yes | SourceKind::Transformed { .. } | SourceKind::AutoBoxed { .. } => true,
}
}
}
pub fn extract_short_display(
attrs: &[syn::Attribute],
) -> syn::Result<(Option<DisplayAttr>, Vec<syn::Attribute>)> {
let mut display: Option<DisplayAttr> = None;
let mut kept: Vec<syn::Attribute> = Vec::with_capacity(attrs.len());
for attr in attrs {
if !attr.path().is_ident("oopsie") {
kept.push(attr.clone());
continue;
}
let parsed: Option<DisplayAttr> =
attr.parse_args_with(|input: ParseStream| -> syn::Result<Option<DisplayAttr>> {
if input.peek(LitStr) {
Ok(Some(input.parse()?))
} else {
let _: proc_macro2::TokenStream = input.parse()?;
Ok(None)
}
})?;
match parsed {
Some(d) => {
if display.is_some() {
return Err(syn::Error::new_spanned(
attr,
"at most one short-display form per item; use `display(...)` if combining",
));
}
display = Some(d);
}
None => kept.push(attr.clone()),
}
}
Ok((display, kept))
}
#[cfg(test)]
mod tests {
use darling::FromMeta as _;
use syn::parse_quote;
use super::*;
#[test]
fn size_constraint_exact() {
let meta: syn::Meta = parse_quote!(size(64));
assert_eq!(
SizeConstraint::from_meta(&meta).unwrap(),
SizeConstraint::Exact(64)
);
}
#[test]
fn size_constraint_at_most() {
let meta: syn::Meta = parse_quote!(size(..=128));
assert_eq!(
SizeConstraint::from_meta(&meta).unwrap(),
SizeConstraint::AtMost(128)
);
}
#[test]
fn size_constraint_at_least() {
let meta: syn::Meta = parse_quote!(size(32..));
assert_eq!(
SizeConstraint::from_meta(&meta).unwrap(),
SizeConstraint::AtLeast(32)
);
}
#[test]
fn size_constraint_range() {
let meta: syn::Meta = parse_quote!(size(32..=64));
assert_eq!(
SizeConstraint::from_meta(&meta).unwrap(),
SizeConstraint::Range(32, 64)
);
}
#[test]
fn size_constraint_below() {
let meta: syn::Meta = parse_quote!(size(..64));
assert_eq!(
SizeConstraint::from_meta(&meta).unwrap(),
SizeConstraint::Below(64)
);
}
#[test]
fn size_constraint_range_half_open() {
let meta: syn::Meta = parse_quote!(size(32..64));
assert_eq!(
SizeConstraint::from_meta(&meta).unwrap(),
SizeConstraint::RangeHalfOpen(32, 64)
);
}
#[test]
fn size_constraint_full_range_rejected() {
let meta: syn::Meta = parse_quote!(size(..));
assert_eq!(
SizeConstraint::from_meta(&meta).unwrap_err().to_string(),
"`size(..)` places no constraint; specify a bound (e.g. `..=N`, `..N`, `N..`, `N..M`, `N..=M`) or remove it",
);
}
#[test]
fn size_constraint_below_zero_rejected() {
let meta: syn::Meta = parse_quote!(size(..0));
assert_eq!(
SizeConstraint::from_meta(&meta).unwrap_err().to_string(),
"`size(..0)` is unsatisfiable: a size is never negative",
);
}
#[test]
fn size_constraint_empty_half_open_rejected() {
assert_eq!(
SizeConstraint::from_meta(&parse_quote!(size(64..64)))
.unwrap_err()
.to_string(),
"empty `size(N..M)` range: the low bound is not below the high bound",
);
SizeConstraint::from_meta(&parse_quote!(size(64..32))).unwrap_err();
}
#[test]
fn size_constraint_empty_inclusive_rejected() {
assert_eq!(
SizeConstraint::from_meta(&parse_quote!(size(64..=32)))
.unwrap_err()
.to_string(),
"empty `size(N..=M)` range: the low bound exceeds the high bound",
);
assert_eq!(
SizeConstraint::from_meta(&parse_quote!(size(64..=64))).unwrap(),
SizeConstraint::Range(64, 64)
);
}
#[test]
fn size_constraint_at_most_zero_valid() {
assert_eq!(
SizeConstraint::from_meta(&parse_quote!(size(..=0))).unwrap(),
SizeConstraint::AtMost(0)
);
}
#[test]
fn size_constraint_name_value_rejected() {
let meta: syn::Meta = parse_quote!(size = 64);
SizeConstraint::from_meta(&meta).unwrap_err();
}
#[test]
fn exit_code_accepts_in_range() {
let meta: syn::Meta = parse_quote!(exit_code = 78);
assert_eq!(ExitCodeAttr::from_meta(&meta).unwrap().value, 78);
let meta: syn::Meta = parse_quote!(exit_code = 1);
assert_eq!(ExitCodeAttr::from_meta(&meta).unwrap().value, 1);
let meta: syn::Meta = parse_quote!(exit_code = 255);
assert_eq!(ExitCodeAttr::from_meta(&meta).unwrap().value, 255);
}
#[test]
fn exit_code_rejects_zero() {
let meta: syn::Meta = parse_quote!(exit_code = 0);
ExitCodeAttr::from_meta(&meta).unwrap_err();
}
#[test]
fn exit_code_rejects_out_of_range() {
let meta: syn::Meta = parse_quote!(exit_code = 300);
ExitCodeAttr::from_meta(&meta).unwrap_err();
}
#[test]
fn exit_code_rejects_non_integer() {
let meta: syn::Meta = parse_quote!(exit_code = "x");
ExitCodeAttr::from_meta(&meta).unwrap_err();
}
#[test]
fn display_attr_format_only() {
let meta: syn::Meta = parse_quote!(display("hello"));
let d = DisplayAttr::from_meta(&meta).unwrap();
assert_eq!(d.format_str.value(), "hello");
assert!(d.args.is_empty());
}
#[test]
fn display_attr_format_with_one_arg() {
let meta: syn::Meta = parse_quote!(display("hello {}", name));
let d = DisplayAttr::from_meta(&meta).unwrap();
assert_eq!(d.format_str.value(), "hello {}");
assert_eq!(d.args.len(), 1);
}
#[test]
fn display_attr_format_with_multiple_args() {
let meta: syn::Meta = parse_quote!(display("{} and {}", a, b));
let d = DisplayAttr::from_meta(&meta).unwrap();
assert_eq!(d.args.len(), 2);
}
#[test]
fn display_attr_with_non_trivial_expr_args() {
let meta: syn::Meta = parse_quote!(display("{} and {}", self.0, foo.bar()));
let d = DisplayAttr::from_meta(&meta).unwrap();
assert_eq!(d.args.len(), 2);
}
#[test]
fn display_attr_name_value_string_form() {
let meta: syn::Meta = parse_quote!(help = "plain");
let d = DisplayAttr::from_meta(&meta).unwrap();
assert_eq!(d.format_str.value(), "plain");
assert!(d.args.is_empty());
}
#[test]
fn placeholder_detection_matches_rustc_escapes() {
assert!(!format_str_has_placeholder("plain text"));
assert!(!format_str_has_placeholder("escaped {{ and }}"));
assert!(!format_str_has_placeholder("a }} lone } close"));
assert!(format_str_has_placeholder("{field}"));
assert!(format_str_has_placeholder("positional {}"));
assert!(format_str_has_placeholder("a {{b}} then {c}"));
assert!(format_str_has_placeholder("trailing {"));
}
#[test]
fn unescape_collapses_double_braces_in_place() {
let mut s = "wrap {{names}} and {{{{nested}}}}".to_owned();
unescape_format_braces(&mut s);
assert_eq!(s, "wrap {names} and {{nested}}");
let mut plain = "no braces here".to_owned();
unescape_format_braces(&mut plain);
assert_eq!(plain, "no braces here");
}
#[test]
fn is_static_distinguishes_literal_from_interpolated() {
let plain: DisplayAttr = DisplayAttr::from_meta(&parse_quote!(help = "plain")).unwrap();
assert!(plain.is_static());
let escaped: DisplayAttr =
DisplayAttr::from_meta(&parse_quote!(help = "use {{x}}")).unwrap();
assert!(escaped.is_static());
assert_eq!(escaped.static_lit().unwrap().value(), "use {x}");
let inline: DisplayAttr =
DisplayAttr::from_meta(&parse_quote!(help = "fix {path}")).unwrap();
assert!(!inline.is_static());
let positional: DisplayAttr =
DisplayAttr::from_meta(&parse_quote!(help("hi {}", name))).unwrap();
assert!(!positional.is_static());
}
#[test]
fn static_lit_rejects_unmatched_close_brace() {
let stray: DisplayAttr = DisplayAttr::from_meta(&parse_quote!(help = "oops }")).unwrap();
stray.static_lit().unwrap_err();
let escaped: DisplayAttr = DisplayAttr::from_meta(&parse_quote!(help = "ok }}")).unwrap();
assert_eq!(escaped.static_lit().unwrap().value(), "ok }");
}
#[test]
fn source_kind_word_is_yes() {
let meta: syn::Meta = parse_quote!(from);
assert!(matches!(
SourceKind::from_meta(&meta).unwrap(),
SourceKind::Yes
));
}
#[test]
fn source_kind_with_transform() {
let meta: syn::Meta = parse_quote!(from(MyType, MyType::from));
match SourceKind::from_meta(&meta).unwrap() {
SourceKind::Transformed {
source_type,
transform: _,
} => {
let s = quote::quote! { #source_type }.to_string();
assert!(s.contains("MyType"));
}
other => panic!("expected Transformed, got {other:?}"),
}
}
#[test]
fn source_kind_missing_transform_rejected() {
let meta: syn::Meta = parse_quote!(from(MyType));
SourceKind::from_meta(&meta).unwrap_err();
}
#[test]
fn source_kind_name_value_rejected() {
let meta: syn::Meta = parse_quote!(from = "X");
SourceKind::from_meta(&meta).unwrap_err();
}
#[test]
fn source_kind_from_none_is_no() {
assert!(matches!(SourceKind::from_none().unwrap(), SourceKind::No));
}
#[test]
fn source_kind_from_false_is_disabled() {
let meta: syn::Meta = parse_quote!(from(false));
assert!(matches!(
SourceKind::from_meta(&meta).unwrap(),
SourceKind::Disabled
));
}
#[test]
fn source_kind_from_true_is_yes() {
let meta: syn::Meta = parse_quote!(from(true));
assert!(matches!(
SourceKind::from_meta(&meta).unwrap(),
SourceKind::Yes
));
}
#[test]
fn provide_attr_value_form() {
let meta: syn::Meta = parse_quote!(provide(MyType => self.value));
let p = ProvideAttr::from_meta(&meta).unwrap();
assert!(!p.is_ref());
}
#[test]
fn provide_attr_ref_form() {
let meta: syn::Meta = parse_quote!(provide(ref, MyType => self.value.as_ref()));
let p = ProvideAttr::from_meta(&meta).unwrap();
assert!(p.is_ref());
}
#[test]
fn provide_attr_missing_arrow_rejected() {
let meta: syn::Meta = parse_quote!(provide(MyType));
ProvideAttr::from_meta(&meta).unwrap_err();
}
#[test]
fn short_display_extracts_simple_string() {
let attrs: Vec<syn::Attribute> = parse_quote! {
#[oopsie("simple message")]
};
let (display, kept) = extract_short_display(&attrs).unwrap();
let d = display.expect("expected a display attr");
assert_eq!(d.format_str.value(), "simple message");
assert!(d.args.is_empty());
assert!(kept.is_empty());
}
#[test]
fn short_display_extracts_with_args() {
let attrs: Vec<syn::Attribute> = parse_quote! {
#[oopsie("fmt {}", arg)]
};
let (display, _kept) = extract_short_display(&attrs).unwrap();
let d = display.expect("expected a display attr");
assert_eq!(d.format_str.value(), "fmt {}");
assert_eq!(d.args.len(), 1);
}
#[test]
fn short_display_allows_non_keyword_call_arg() {
let attrs: Vec<syn::Attribute> = parse_quote! {
#[oopsie("fmt {}", helper(x))]
};
let (display, _kept) = extract_short_display(&attrs).unwrap();
let d = display.expect("expected a display attr");
assert_eq!(d.args.len(), 1);
}
#[test]
fn short_display_keeps_long_form_attrs() {
let attrs: Vec<syn::Attribute> = parse_quote! {
#[oopsie(display("fmt"), help = "X")]
};
let (display, kept) = extract_short_display(&attrs).unwrap();
assert!(display.is_none());
assert_eq!(kept.len(), 1);
}
#[test]
fn short_display_rejects_two() {
let attrs: Vec<syn::Attribute> = parse_quote! {
#[oopsie("first")]
#[oopsie("second")]
};
extract_short_display(&attrs).unwrap_err();
}
#[test]
fn short_display_passes_through_non_oopsie_attrs() {
let attrs: Vec<syn::Attribute> = parse_quote! {
#[other(stuff)]
#[oopsie("display me")]
#[cfg(feature = "x")]
};
let (display, kept) = extract_short_display(&attrs).unwrap();
assert!(display.is_some());
assert_eq!(kept.len(), 2);
}
#[test]
fn short_display_allows_bare_arg_named_like_keyword() {
let attrs: Vec<syn::Attribute> = parse_quote! { #[oopsie("trace: {}", backtrace)] };
let (display, _) = extract_short_display(&attrs).unwrap();
assert_eq!(display.unwrap().args.len(), 1);
}
fn fields_of(item: syn::ItemStruct) -> syn::Fields {
item.fields
}
#[test]
fn reject_keyword_args_flags_non_field_keyword() {
let d: DisplayAttr =
DisplayAttr::from_meta(&parse_quote!(display("wrapped", transparent))).unwrap();
let fields = fields_of(parse_quote! { struct S { source: std::io::Error } });
d.reject_keyword_args(&fields, DisplayScope::Variant)
.unwrap_err();
}
#[test]
fn reject_keyword_args_allows_keyword_named_field() {
let d: DisplayAttr = DisplayAttr::from_meta(&parse_quote!(display("{}", code))).unwrap();
let fields = fields_of(parse_quote! { struct S { code: u16 } });
d.reject_keyword_args(&fields, DisplayScope::Variant)
.unwrap();
}
#[test]
fn reject_keyword_args_allows_non_keyword_and_rich_exprs() {
let d: DisplayAttr =
DisplayAttr::from_meta(&parse_quote!(display("{} {}", extra, self.0))).unwrap();
let fields = fields_of(parse_quote! { struct S { whatever: u8 } });
d.reject_keyword_args(&fields, DisplayScope::Variant)
.unwrap();
}
#[test]
fn reject_keyword_args_struct_scope_flags_container_keyword() {
let d: DisplayAttr = DisplayAttr::from_meta(&parse_quote!(display("x", size))).unwrap();
let fields = fields_of(parse_quote! { struct S { msg: String } });
d.reject_keyword_args(&fields, DisplayScope::Variant)
.unwrap();
d.reject_keyword_args(&fields, DisplayScope::Struct)
.unwrap_err();
}
#[test]
fn capture_false_keeps_trace_typed_field_on_selector() {
let item: syn::ItemStruct = parse_quote! {
struct S { #[oopsie(capture = false)] bt: Backtrace, msg: String }
};
let categorized = CategorizedFields::from_fields(&item.fields).unwrap();
assert!(categorized.user_fields.iter().any(|f| f.ident == "bt"));
assert!(!categorized.auto_fields.iter().any(|f| f.ident == "bt"));
assert_eq!(
categorized
.backtrace_field
.as_ref()
.map(ToString::to_string),
Some("bt".into())
);
}
#[test]
fn capture_false_with_backtrace_flag_errors() {
let item: syn::ItemStruct = parse_quote! {
struct S { #[oopsie(backtrace, capture(false))] bt: Backtrace }
};
CategorizedFields::from_fields(&item.fields).unwrap_err();
}
#[test]
fn categorize_detects_packed_traces_field_by_attr() {
let item: syn::ItemStruct = parse_quote! {
struct S { #[oopsie(traces)] t: Box<(Backtrace, SpanTrace)>, msg: String }
};
let categorized = CategorizedFields::from_fields(&item.fields).unwrap();
assert_eq!(
categorized.traces_field.as_ref().map(ToString::to_string),
Some("t".to_owned())
);
assert!(categorized.auto_fields.iter().any(|f| f.ident == "t"));
assert!(categorized.backtrace_field.is_none());
assert!(categorized.spantrace_field.is_none());
}
#[test]
fn categorize_detects_packed_traces_field_by_type() {
let item: syn::ItemStruct = parse_quote! {
struct S { t: (Backtrace, SpanTrace) }
};
let categorized = CategorizedFields::from_fields(&item.fields).unwrap();
assert!(categorized.traces_field.is_some());
assert!(categorized.auto_fields.iter().any(|f| f.ident == "t"));
}
#[test]
fn categorize_ignores_wrongly_typed_trace_named_fields() {
let item: syn::ItemStruct = parse_quote! {
struct S { backtrace: String, spantrace: u32, traces: Vec<String> }
};
let categorized = CategorizedFields::from_fields(&item.fields).unwrap();
assert!(categorized.backtrace_field.is_none());
assert!(categorized.spantrace_field.is_none());
assert!(categorized.traces_field.is_none());
for name in ["backtrace", "spantrace", "traces"] {
assert!(categorized.user_fields.iter().any(|f| f.ident == name));
assert!(!categorized.auto_fields.iter().any(|f| f.ident == name));
}
}
#[test]
fn categorize_classifies_trace_fields_by_type_regardless_of_name() {
let item: syn::ItemStruct = parse_quote! {
struct S { bt: Backtrace, st: SpanTrace }
};
let categorized = CategorizedFields::from_fields(&item.fields).unwrap();
assert_eq!(
categorized
.backtrace_field
.as_ref()
.map(ToString::to_string),
Some("bt".to_owned())
);
assert_eq!(
categorized
.spantrace_field
.as_ref()
.map(ToString::to_string),
Some("st".to_owned())
);
}
#[test]
fn categorize_rejects_explicit_backtrace_attr_on_wrong_type() {
let item: syn::ItemStruct = parse_quote! {
struct S { #[oopsie(backtrace)] b: String }
};
CategorizedFields::from_fields(&item.fields).unwrap_err();
}
#[test]
fn categorize_rejects_packed_traces_alongside_standalone_backtrace() {
let item: syn::ItemStruct = parse_quote! {
struct S { traces: (Backtrace, SpanTrace), bt: Backtrace }
};
CategorizedFields::from_fields(&item.fields).unwrap_err();
}
}
#[derive(Debug)]
pub struct CategorizedFields {
pub source: Option<SourceField>,
pub auto_fields: Vec<AutoField>,
pub user_fields: Vec<UserField>,
pub provides: Vec<(Ident, ProvideAttr)>,
pub backtrace_field: Option<Ident>,
pub spantrace_field: Option<Ident>,
pub traces_field: Option<Ident>,
pub location_field: Option<Ident>,
pub help_field: Option<Ident>,
}
#[derive(Debug)]
pub struct SourceField {
pub ident: Ident,
pub ty: Type,
pub kind: SourceKind,
pub forward: ResolvedForward,
pub cfg_attrs: Vec<syn::Attribute>,
}
#[derive(Debug)]
pub struct AutoField {
pub ident: Ident,
pub ty: Type,
pub cfg_attrs: Vec<syn::Attribute>,
}
#[derive(Debug)]
pub struct UserField {
pub ident: Ident,
pub ty: Type,
pub cfg_attrs: Vec<syn::Attribute>,
}
pub fn any_variant_has_cfg(data: &syn::DataEnum) -> bool {
data.variants
.iter()
.any(|v| v.attrs.iter().any(|a| a.path().is_ident("cfg")))
}
fn field_cfg_attrs(field: &syn::Field) -> Vec<syn::Attribute> {
field
.attrs
.iter()
.filter(|a| a.path().is_ident("cfg") || a.path().is_ident("cfg_attr"))
.cloned()
.collect()
}
impl CategorizedFields {
pub fn from_fields(fields: &syn::Fields) -> syn::Result<Self> {
use crate::traced::field_detect::{
is_backtrace_type, is_location_type, is_spantrace_type, is_traces_type,
};
let mut source = None;
let mut auto_fields = Vec::new();
let mut user_fields = Vec::new();
let mut provides = Vec::new();
let mut backtrace_field = None;
let mut spantrace_field = None;
let mut traces_field = None;
let mut location_field = None;
let mut help_field = None;
let named = match fields {
syn::Fields::Named(f) => &f.named,
syn::Fields::Unit => {
return Ok(Self {
source,
auto_fields,
user_fields,
provides,
backtrace_field,
spantrace_field,
traces_field,
location_field,
help_field,
});
}
syn::Fields::Unnamed(unnamed) => {
return Err(syn::Error::new_spanned(
unnamed,
"#[derive(Oopsie)] does not support tuple variants/structs",
));
}
};
for field in named {
let Some(ident) = field.ident.clone() else {
continue;
};
let attrs = FieldAttrs::from_field(field)?;
let cfg_attrs = field_cfg_attrs(field);
for p in &attrs.provide {
provides.push((ident.clone(), p.clone()));
}
if attrs.backtrace && !is_backtrace_type(&field.ty) {
return Err(syn::Error::new_spanned(
field,
"`#[oopsie(backtrace)]` requires a field whose type's last path segment is `Backtrace`",
));
}
if attrs.spantrace && !is_spantrace_type(&field.ty) {
return Err(syn::Error::new_spanned(
field,
"`#[oopsie(spantrace)]` requires a field whose type's last path segment is `SpanTrace`",
));
}
if attrs.traces && !is_traces_type(&field.ty) {
return Err(syn::Error::new_spanned(
field,
"`#[oopsie(traces)]` requires a field of type `(Backtrace, SpanTrace)`",
));
}
if attrs.location && !is_location_type(&field.ty) {
return Err(syn::Error::new_spanned(
field,
"`#[oopsie(location)]` requires a field of type `&'static Location<'static>`",
));
}
let is_traces = attrs.traces || is_traces_type(&field.ty);
if attrs.backtrace || is_backtrace_type(&field.ty) {
if backtrace_field.is_some() {
return Err(syn::Error::new_spanned(
field,
"at most one backtrace field per variant/struct",
));
}
backtrace_field = Some(ident.clone());
}
if attrs.spantrace || is_spantrace_type(&field.ty) {
if spantrace_field.is_some() {
return Err(syn::Error::new_spanned(
field,
"at most one spantrace field per variant/struct",
));
}
spantrace_field = Some(ident.clone());
}
if is_traces {
if traces_field.is_some() {
return Err(syn::Error::new_spanned(
field,
"at most one packed `traces` field per variant/struct",
));
}
traces_field = Some(ident.clone());
}
if attrs.location || is_location_type(&field.ty) {
if location_field.is_some() {
return Err(syn::Error::new_spanned(
field,
"at most one location field per variant/struct",
));
}
location_field = Some(ident.clone());
}
if attrs.help {
if help_field.is_some() {
return Err(syn::Error::new_spanned(
field,
"at most one `#[oopsie(help)]` field per variant/struct",
));
}
help_field = Some(ident.clone());
}
let forward = ResolvedForward::resolve(&attrs.forward);
if forward.any() && !attrs.is_source() {
return Err(syn::Error::new_spanned(
field,
"`#[oopsie(forward)]` must be on a source field (named `source` or marked `#[oopsie(from)]`)",
));
}
if attrs.is_source() {
if source.is_some() {
return Err(syn::Error::new_spanned(
field,
"at most one source field per variant/struct",
));
}
source = Some(SourceField {
ident: ident.clone(),
ty: field.ty.clone(),
kind: attrs.from,
forward,
cfg_attrs,
});
} else if attrs.capture.is_enabled() {
auto_fields.push(AutoField {
ident,
ty: field.ty.clone(),
cfg_attrs,
});
} else {
user_fields.push(UserField {
ident,
ty: field.ty.clone(),
cfg_attrs,
});
}
}
if traces_field.is_some() {
if let Some(bt) = &backtrace_field {
return Err(syn::Error::new_spanned(
bt,
"a packed `traces` field cannot coexist with a separate `backtrace` field",
));
}
if let Some(st) = &spantrace_field {
return Err(syn::Error::new_spanned(
st,
"a packed `traces` field cannot coexist with a separate `spantrace` field",
));
}
}
if let Some(src) = &source {
if src.forward.backtrace && (backtrace_field.is_some() || traces_field.is_some()) {
return Err(syn::Error::new_spanned(
&src.ident,
"`forward(backtrace)` cannot coexist with an own backtrace/traces field",
));
}
if src.forward.spantrace && (spantrace_field.is_some() || traces_field.is_some()) {
return Err(syn::Error::new_spanned(
&src.ident,
"`forward(spantrace)` cannot coexist with an own spantrace/traces field",
));
}
if src.forward.location && location_field.is_some() {
return Err(syn::Error::new_spanned(
&src.ident,
"`forward(location)` cannot coexist with an own location field",
));
}
}
Ok(Self {
source,
auto_fields,
user_fields,
provides,
backtrace_field,
spantrace_field,
traces_field,
location_field,
help_field,
})
}
}
#[cfg(test)]
mod forward_tests {
use darling::FromAttributes as _;
use super::*;
fn attrs(a: syn::Attribute) -> FieldAttrs {
FieldAttrs::from_attributes(&[a]).expect("parse field attrs")
}
#[test]
fn bare_forward_forwards_backtrace_and_spantrace_not_location() {
let rf = ResolvedForward::resolve(&attrs(syn::parse_quote!(#[oopsie(forward)])).forward);
assert_eq!(
rf,
ResolvedForward {
backtrace: true,
spantrace: true,
location: false
}
);
}
#[test]
fn forward_disables_backtrace_and_enables_location() {
let rf = ResolvedForward::resolve(
&attrs(syn::parse_quote!(#[oopsie(forward(backtrace = false, location = true))]))
.forward,
);
assert_eq!(
rf,
ResolvedForward {
backtrace: false,
spantrace: true,
location: true
}
);
}
#[test]
fn forward_accepts_paren_flag_spelling() {
let rf = ResolvedForward::resolve(
&attrs(syn::parse_quote!(#[oopsie(forward(location(true)))])).forward,
);
assert!(rf.location);
}
#[test]
fn absent_forward_resolves_to_nothing() {
let rf = ResolvedForward::resolve(&attrs(syn::parse_quote!(#[oopsie(from)])).forward);
assert_eq!(rf, ResolvedForward::default());
}
#[test]
fn explicit_forward_false_resolves_to_nothing() {
let rf =
ResolvedForward::resolve(&attrs(syn::parse_quote!(#[oopsie(forward = false)])).forward);
assert_eq!(rf, ResolvedForward::default());
}
}