use std::collections::HashSet;
use crate::core::ir::{CoreWrapper, EnumVariant, FieldDef, TypeRef};
use ahash::AHashMap;
use syn;
use crate::extract::type_resolver;
pub(crate) fn is_pub(vis: &syn::Visibility) -> bool {
matches!(vis, syn::Visibility::Public(_))
}
pub(crate) fn extract_doc_comments(attrs: &[syn::Attribute]) -> String {
let mut lines = Vec::new();
for attr in attrs {
if attr.path().is_ident("doc") {
if let syn::Meta::NameValue(meta) = &attr.meta {
if let syn::Expr::Lit(expr_lit) = &meta.value {
if let syn::Lit::Str(lit_str) = &expr_lit.lit {
let val = lit_str.value();
let trimmed = val.strip_prefix(' ').unwrap_or(&val);
lines.push(trimmed.to_string());
}
}
}
}
}
let raw = lines.join("\n");
normalize_rustdoc(&raw)
}
pub fn normalize_rustdoc(raw: &str) -> String {
if raw.is_empty() {
return String::new();
}
let mut filtered = String::with_capacity(raw.len());
let mut in_rust_fence = false;
for line in raw.lines() {
let trimmed = line.trim_start();
if let Some(rest) = trimmed.strip_prefix("```") {
if in_rust_fence {
in_rust_fence = false;
} else {
let lang = rest.split(',').next().unwrap_or("").trim();
if lang.is_empty() || lang.eq_ignore_ascii_case("rust") {
in_rust_fence = true;
}
}
filtered.push_str(line);
filtered.push('\n');
continue;
}
if in_rust_fence {
let after_hash = trimmed.strip_prefix('#');
if let Some(suffix) = after_hash {
if suffix.is_empty() || suffix.starts_with(' ') {
continue;
}
}
}
filtered.push_str(line);
filtered.push('\n');
}
let mut out = String::with_capacity(filtered.len());
let chars: Vec<char> = filtered.chars().collect();
let mut i = 0;
while i < chars.len() {
if i + 1 < chars.len() && chars[i] == '[' && chars[i + 1] == '`' {
let start = i + 2;
let mut j = start;
while j + 1 < chars.len() {
if chars[j] == '`' && chars[j + 1] == ']' {
break;
}
j += 1;
}
if j + 1 < chars.len() && chars[j] == '`' && chars[j + 1] == ']' {
let inner: String = chars[start..j].iter().collect();
let stripped = inner
.strip_prefix("crate::")
.or_else(|| inner.strip_prefix("super::"))
.or_else(|| inner.strip_prefix("self::"));
if let Some(rest) = stripped {
let last = rest.rsplit("::").next().unwrap_or(rest);
out.push('`');
out.push_str(last);
out.push('`');
i = j + 2;
if i < chars.len() && chars[i] == '(' {
let mut depth = 1;
i += 1;
while i < chars.len() && depth > 0 {
match chars[i] {
'(' => depth += 1,
')' => depth -= 1,
_ => {}
}
i += 1;
}
}
continue;
}
}
}
out.push(chars[i]);
i += 1;
}
if out.ends_with('\n') {
out.pop();
}
out
}
pub(crate) fn has_derive(attrs: &[syn::Attribute], derive_name: &str) -> bool {
for attr in attrs {
if attr.path().is_ident("derive") {
if let Ok(nested) =
attr.parse_args_with(syn::punctuated::Punctuated::<syn::Path, syn::token::Comma>::parse_terminated)
{
for path in &nested {
if path.is_ident(derive_name) || path.segments.last().is_some_and(|seg| seg.ident == derive_name) {
return true;
}
}
}
} else if attr.path().is_ident("cfg_attr") {
if cfg_attr_has_derive_name(attr, derive_name) {
return true;
}
}
}
false
}
fn cfg_attr_has_derive_name(attr: &syn::Attribute, derive_name: &str) -> bool {
cfg_attr_walk_derives(attr, |path| {
path.is_ident(derive_name) || path.segments.last().is_some_and(|seg| seg.ident == derive_name)
})
}
fn cfg_attr_has_derive_path(attr: &syn::Attribute, segments: &[&str]) -> bool {
cfg_attr_walk_derives(attr, |path| {
path.segments.len() == segments.len()
&& path
.segments
.iter()
.zip(segments.iter())
.all(|(seg, expected)| seg.ident == *expected)
})
}
fn cfg_attr_walk_derives(attr: &syn::Attribute, mut predicate: impl FnMut(&syn::Path) -> bool) -> bool {
let meta_list = match attr.meta.require_list() {
Ok(list) => list,
Err(_) => return false,
};
use syn::Token;
use syn::parse::ParseStream;
let mut found = false;
let parse_fn = |input: ParseStream<'_>| -> syn::Result<()> {
let _condition: syn::Meta = input.parse()?;
let _: Token![,] = input.parse()?;
while !input.is_empty() {
let attr_meta: syn::Meta = input.parse()?;
if let syn::Meta::List(list) = &attr_meta {
if list.path.is_ident("derive") {
let inner_paths =
list.parse_args_with(syn::punctuated::Punctuated::<syn::Path, Token![,]>::parse_terminated)?;
for path in &inner_paths {
if predicate(path) {
found = true;
}
}
}
}
if input.peek(Token![,]) {
let _: Token![,] = input.parse()?;
}
}
Ok(())
};
let _ = syn::parse::Parser::parse2(parse_fn, meta_list.tokens.clone());
found
}
pub(crate) fn has_cfg_attribute(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(|a| a.path().is_ident("cfg"))
}
pub(crate) fn extract_cfg_condition(attrs: &[syn::Attribute]) -> Option<String> {
for attr in attrs {
if attr.path().is_ident("cfg") {
if let Ok(tokens) = attr.meta.require_list() {
return Some(tokens.tokens.to_string());
}
}
}
None
}
pub(crate) fn extract_serde_rename_all(attrs: &[syn::Attribute]) -> Option<String> {
fn extract_from_serde(attr: &syn::Attribute) -> Option<String> {
let mut found: Option<String> = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename_all") {
if let Ok(value) = meta.value() {
if let Ok(s) = value.parse::<syn::LitStr>() {
found = Some(s.value());
}
}
} else if let Ok(value) = meta.value() {
let _: syn::Expr = value.parse()?;
}
Ok(())
});
found
}
for attr in attrs {
if attr.path().is_ident("serde") {
if let Some(v) = extract_from_serde(attr) {
return Some(v);
}
} else if attr.path().is_ident("cfg_attr") {
let mut inner: Option<String> = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("serde") {
let _ = meta.parse_nested_meta(|inner_meta| {
if inner_meta.path.is_ident("rename_all") {
if let Ok(value) = inner_meta.value() {
if let Ok(s) = value.parse::<syn::LitStr>() {
inner = Some(s.value());
}
}
} else if let Ok(value) = inner_meta.value() {
let _: syn::Expr = value.parse()?;
}
Ok(())
});
} else if let Ok(value) = meta.value() {
let _: syn::Expr = value.parse()?;
}
Ok(())
});
if let Some(v) = inner {
return Some(v);
}
}
}
None
}
pub(crate) fn build_rust_path(crate_name: &str, module_path: &str, name: &str) -> String {
if module_path.is_empty() {
format!("{crate_name}::{name}")
} else {
format!("{crate_name}::{module_path}::{name}")
}
}
pub(crate) fn syn_type_is_boxed(ty: &syn::Type) -> bool {
if let syn::Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
let ident = segment.ident.to_string();
if ident == "Box" {
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
for arg in &args.args {
if let syn::GenericArgument::Type(inner) = arg {
if matches!(inner, syn::Type::TraitObject(_)) {
return false;
}
return true;
}
}
}
} else if ident == "Option" {
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
for arg in &args.args {
if let syn::GenericArgument::Type(inner) = arg {
return syn_type_is_boxed(inner);
}
}
}
}
}
}
false
}
pub(crate) fn extract_field_type_rust_path(ty: &syn::Type, crate_name: Option<&str>) -> Option<String> {
let inner_ty = if let syn::Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
if segment.ident == "Option" {
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
args.args.iter().find_map(|arg| {
if let syn::GenericArgument::Type(inner) = arg {
Some(inner)
} else {
None
}
})
} else {
None
}
} else {
None
}
} else {
None
}
} else {
None
};
let check_ty = inner_ty.unwrap_or(ty);
let check_ty = if let syn::Type::Path(type_path) = check_ty {
if let Some(segment) = type_path.path.segments.last() {
if segment.ident == "Box" {
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
args.args
.iter()
.find_map(|arg| {
if let syn::GenericArgument::Type(inner) = arg {
Some(inner)
} else {
None
}
})
.unwrap_or(check_ty)
} else {
check_ty
}
} else {
check_ty
}
} else {
check_ty
}
} else {
check_ty
};
if let syn::Type::Path(type_path) = check_ty {
if type_path.path.segments.len() >= 2 {
let first_segment = type_path.path.segments[0].ident.to_string();
if first_segment == "super" {
return None;
}
if first_segment == "crate" {
if let Some(name) = crate_name {
let mut segments: Vec<String> =
type_path.path.segments.iter().map(|s| s.ident.to_string()).collect();
segments[0] = name.replace('-', "_").to_string();
return Some(segments.join("::"));
}
return None;
}
let segments: Vec<String> = type_path.path.segments.iter().map(|s| s.ident.to_string()).collect();
return Some(segments.join("::"));
}
}
None
}
fn outermost_ident(ty: &syn::Type) -> Option<String> {
if let syn::Type::Path(p) = ty {
if let Some(seg) = p.path.segments.last() {
let ident = seg.ident.to_string();
if ident == "Option" {
if let Some(inner) = type_resolver::extract_single_generic_arg_syn(seg) {
return outermost_ident(&inner);
}
}
return Some(ident);
}
}
None
}
pub(crate) fn detect_core_wrapper(ty: &syn::Type) -> crate::core::ir::CoreWrapper {
use crate::core::ir::CoreWrapper;
let inner_ty: Option<Box<syn::Type>> = if let syn::Type::Path(p) = ty {
p.path.segments.last().and_then(|seg| {
if seg.ident == "Option" {
type_resolver::extract_single_generic_arg_syn(seg)
} else {
None
}
})
} else {
None
};
let probe: &syn::Type = inner_ty.as_deref().unwrap_or(ty);
if let syn::Type::Path(p) = probe {
if let Some(seg) = p.path.segments.last() {
let ident = seg.ident.to_string();
match ident.as_str() {
"Cow" => return CoreWrapper::Cow,
"Bytes" => return CoreWrapper::Bytes,
"Box" => {
if let Some(box_inner) = type_resolver::extract_single_generic_arg_syn(seg) {
if let syn::Type::Path(inner_path) = &*box_inner {
if let Some(inner_seg) = inner_path.path.segments.last() {
let inner_ident = inner_seg.ident.to_string();
if inner_ident == "str" {
return CoreWrapper::Box;
}
}
}
}
}
"Arc" => {
if let Some(arc_inner) = type_resolver::extract_single_generic_arg_syn(seg) {
if let syn::Type::Path(inner_path) = &*arc_inner {
if let Some(inner_seg) = inner_path.path.segments.last() {
let inner_ident = inner_seg.ident.to_string();
if inner_ident == "Mutex" || inner_ident == "RwLock" {
return CoreWrapper::ArcMutex;
}
}
}
}
return CoreWrapper::Arc;
}
_ => {}
}
}
}
CoreWrapper::None
}
pub(crate) fn detect_vec_inner_core_wrapper(ty: &syn::Type) -> crate::core::ir::CoreWrapper {
use crate::core::ir::CoreWrapper;
let check_ty = if let syn::Type::Path(p) = ty {
if let Some(seg) = p.path.segments.last() {
if seg.ident == "Option" {
type_resolver::extract_single_generic_arg_syn(seg)
} else {
None
}
} else {
None
}
} else {
None
};
let ty_ref = check_ty.as_deref().unwrap_or(ty);
if let syn::Type::Path(p) = ty_ref {
if let Some(seg) = p.path.segments.last() {
if seg.ident == "Vec" {
if let Some(vec_inner) = type_resolver::extract_single_generic_arg_syn(seg) {
if let Some(ident) = outermost_ident(&vec_inner) {
if ident == "Arc" {
return CoreWrapper::Arc;
}
}
}
}
}
}
CoreWrapper::None
}
pub(crate) fn unwrap_optional(ty: TypeRef) -> (TypeRef, bool) {
match ty {
TypeRef::Optional(inner) => (*inner, true),
other => (other, false),
}
}
pub(crate) fn extract_field(field: &syn::Field, crate_name: Option<&str>) -> FieldDef {
let name = field.ident.as_ref().map(|i| i.to_string()).unwrap_or_default();
let doc = extract_doc_comments(&field.attrs);
let cfg = extract_cfg_condition(&field.attrs);
let binding_exclusion_reason = extract_field_binding_exclusion_reason(&field.attrs, &field.ty);
let binding_excluded = binding_exclusion_reason.is_some();
let is_boxed = syn_type_is_boxed(&field.ty);
let type_rust_path = extract_field_type_rust_path(&field.ty, crate_name);
let core_wrapper = detect_core_wrapper(&field.ty);
let vec_inner_core_wrapper = detect_vec_inner_core_wrapper(&field.ty);
let resolved = type_resolver::resolve_type(&field.ty);
let (ty, optional) = unwrap_optional(resolved);
let serde_rename = extract_serde_rename(&field.attrs);
let serde_flatten = extract_serde_flatten(&field.attrs);
let has_serde_default_attr = has_serde_default(&field.attrs);
let default = if has_serde_default_attr {
Some("/* serde(default) */".to_string())
} else {
None
};
FieldDef {
name,
ty,
optional,
default,
doc,
sanitized: false,
is_boxed,
type_rust_path,
cfg,
typed_default: None,
core_wrapper,
vec_inner_core_wrapper,
newtype_wrapper: None,
serde_rename,
serde_flatten,
binding_excluded,
binding_exclusion_reason,
original_type: None,
}
}
pub(crate) fn has_dyn_trait_object(ty: &syn::Type) -> bool {
match ty {
syn::Type::TraitObject(_) => true,
syn::Type::Path(type_path) => type_path.path.segments.iter().any(|seg| {
if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
args.args.iter().any(|arg| {
if let syn::GenericArgument::Type(inner) = arg {
has_dyn_trait_object(inner)
} else {
false
}
})
} else {
false
}
}),
syn::Type::Reference(type_ref) => has_dyn_trait_object(&type_ref.elem),
syn::Type::Tuple(type_tuple) => type_tuple.elems.iter().any(has_dyn_trait_object),
syn::Type::Group(type_group) => has_dyn_trait_object(&type_group.elem),
syn::Type::Paren(type_paren) => has_dyn_trait_object(&type_paren.elem),
_ => false,
}
}
pub(crate) fn extract_binding_exclusion_reason(attrs: &[syn::Attribute]) -> Option<String> {
if has_doc_hidden(attrs) {
return Some("doc(hidden)".to_string());
}
if has_alef_skip(attrs) {
return Some("alef(skip)".to_string());
}
None
}
pub(crate) fn extract_field_binding_exclusion_reason(attrs: &[syn::Attribute], ty: &syn::Type) -> Option<String> {
if let Some(reason) = extract_binding_exclusion_reason(attrs) {
return Some(reason);
}
if has_dyn_trait_object(ty) {
return Some("dyn-trait-object".to_string());
}
None
}
fn has_doc_hidden(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(|attr| {
if !attr.path().is_ident("doc") {
return false;
}
let Ok(list) = attr.meta.require_list() else {
return false;
};
list.parse_args::<syn::Ident>()
.map(|ident| ident == "hidden")
.unwrap_or(false)
})
}
fn has_alef_skip(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(|attr| {
let attr_str = quote::quote!(#attr).to_string();
let is_direct_alef = attr.path().is_ident("alef") && attr_str.contains("skip");
let is_cfg_attr_alef =
attr.path().is_ident("cfg_attr") && attr_str.contains("alef") && attr_str.contains("skip");
is_direct_alef || is_cfg_attr_alef
})
}
pub(crate) fn extract_serde_flatten(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(|attr| {
let attr_str = quote::quote!(#attr).to_string();
if !attr_str.contains("serde") {
return false;
}
attr_str.contains("flatten ,")
|| attr_str.contains("flatten,")
|| attr_str.contains("flatten )")
|| attr_str.contains("flatten)")
|| attr_str.ends_with("flatten")
})
}
pub(crate) fn extract_serde_rename(attrs: &[syn::Attribute]) -> Option<String> {
attrs.iter().find_map(|attr| {
let attr_str = quote::quote!(#attr).to_string();
if !attr_str.contains("serde") || !attr_str.contains("rename") {
return None;
}
let needles = ["rename =", "rename="];
for needle in &needles {
if let Some(pos) = attr_str.find(needle) {
let before = &attr_str[..pos];
if before.ends_with("rename_all_") || before.ends_with("rename_all") {
continue;
}
let rest = &attr_str[pos + needle.len()..];
let after = rest.trim_start();
let start = after.find('"')?;
let value_start = &after[start + 1..];
let end = value_start.find('"')?;
return Some(value_start[..end].to_string());
}
}
None
})
}
pub(crate) fn has_serde_default(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(|attr| {
let attr_str = quote::quote!(#attr).to_string();
if !attr_str.contains("serde") {
return false;
}
attr_str.contains("default =")
|| attr_str.contains("default ,")
|| attr_str.contains("default,")
|| attr_str.contains("default )")
|| attr_str.contains("default)")
|| attr_str.ends_with("default")
})
}
pub(crate) fn extract_enum_variant(v: &syn::Variant) -> EnumVariant {
let is_tuple = matches!(&v.fields, syn::Fields::Unnamed(_));
let variant_fields = match &v.fields {
syn::Fields::Named(named) => named.named.iter().map(|f| extract_field(f, None)).collect(),
syn::Fields::Unnamed(unnamed) => unnamed
.unnamed
.iter()
.enumerate()
.map(|(i, f)| {
let ty = type_resolver::resolve_type(&f.ty);
let optional = type_resolver::is_option_type(&f.ty).is_some();
FieldDef {
name: format!("_{i}"),
ty,
optional,
default: None,
doc: extract_doc_comments(&f.attrs),
sanitized: false,
is_boxed: syn_type_is_boxed(&f.ty),
type_rust_path: extract_field_type_rust_path(&f.ty, None),
cfg: None,
typed_default: None,
core_wrapper: CoreWrapper::None,
vec_inner_core_wrapper: CoreWrapper::None,
newtype_wrapper: None,
serde_rename: None,
serde_flatten: false,
binding_excluded: false,
binding_exclusion_reason: None,
original_type: None,
}
})
.collect(),
syn::Fields::Unit => vec![],
};
let serde_rename = v.attrs.iter().find_map(|attr| {
let attr_str = quote::quote!(#attr).to_string();
if !attr_str.contains("rename") {
return None;
}
let pos = attr_str.find("rename")?;
let rest = &attr_str[pos..];
let eq_pos = rest.find('=')?;
let after_eq = rest[eq_pos + 1..].trim_start();
let start = after_eq.find('"')?;
let value_start = &after_eq[start + 1..];
let end = value_start.find('"')?;
Some(value_start[..end].to_string())
});
let binding_exclusion_reason = extract_binding_exclusion_reason(&v.attrs);
let binding_excluded = binding_exclusion_reason.is_some();
EnumVariant {
name: v.ident.to_string(),
fields: variant_fields,
doc: extract_doc_comments(&v.attrs),
is_default: v.attrs.iter().any(|a| a.path().is_ident("default")),
serde_rename,
is_tuple,
binding_excluded,
binding_exclusion_reason,
originally_had_data_fields: false,
version: extract_version_annotation(&v.attrs),
}
}
pub(crate) fn has_derive_path(attrs: &[syn::Attribute], segments: &[&str]) -> bool {
for attr in attrs {
if attr.path().is_ident("derive") {
if let Ok(nested) =
attr.parse_args_with(syn::punctuated::Punctuated::<syn::Path, syn::token::Comma>::parse_terminated)
{
for path in &nested {
if path.segments.len() == segments.len()
&& path
.segments
.iter()
.zip(segments.iter())
.all(|(seg, expected)| seg.ident == expected)
{
return true;
}
}
}
} else if attr.path().is_ident("cfg_attr") {
if cfg_attr_has_derive_path(attr, segments) {
return true;
}
}
}
false
}
pub(crate) fn is_thiserror_enum(attrs: &[syn::Attribute]) -> bool {
has_derive(attrs, "Error") || has_derive_path(attrs, &["thiserror", "Error"])
}
pub(crate) fn extract_error_message_template(attrs: &[syn::Attribute]) -> Option<String> {
for attr in attrs {
if attr.path().is_ident("error") {
if let Ok(lit) = attr.parse_args::<syn::LitStr>() {
return Some(lit.value());
}
}
}
None
}
pub(crate) fn has_field_attr(attrs: &[syn::Attribute], name: &str) -> bool {
attrs.iter().any(|a| a.path().is_ident(name))
}
#[derive(Debug)]
pub(crate) enum ReexportKind {
Glob,
Names(HashSet<String>),
}
pub(crate) fn collect_reexport_map(items: &[syn::Item]) -> AHashMap<String, ReexportKind> {
let mut map: AHashMap<String, ReexportKind> = AHashMap::new();
for item in items {
if let syn::Item::Use(item_use) = item {
if is_pub(&item_use.vis) {
collect_reexport_from_tree(&item_use.tree, &mut map);
}
}
}
map
}
fn collect_reexport_from_tree(tree: &syn::UseTree, map: &mut AHashMap<String, ReexportKind>) {
if let syn::UseTree::Path(use_path) = tree {
let root_ident = use_path.ident.to_string();
if root_ident == "self" {
collect_reexport_from_tree(&use_path.tree, map);
return;
}
if root_ident == "super" || root_ident == "crate" {
return;
}
collect_reexport_leaves(&root_ident, &use_path.tree, map);
} else if let syn::UseTree::Group(group) = tree {
for item in &group.items {
collect_reexport_from_tree(item, map);
}
}
}
fn collect_reexport_leaves(module: &str, tree: &syn::UseTree, map: &mut AHashMap<String, ReexportKind>) {
match tree {
syn::UseTree::Glob(_) => {
map.insert(module.to_string(), ReexportKind::Glob);
}
syn::UseTree::Name(use_name) => {
let name = use_name.ident.to_string();
match map.get_mut(module) {
Some(ReexportKind::Glob) => {} Some(ReexportKind::Names(names)) => {
names.insert(name);
}
None => {
let mut names = HashSet::new();
names.insert(name);
map.insert(module.to_string(), ReexportKind::Names(names));
}
}
}
syn::UseTree::Rename(use_rename) => {
let name = use_rename.rename.to_string();
match map.get_mut(module) {
Some(ReexportKind::Glob) => {}
Some(ReexportKind::Names(names)) => {
names.insert(name);
}
None => {
let mut names = HashSet::new();
names.insert(name);
map.insert(module.to_string(), ReexportKind::Names(names));
}
}
}
syn::UseTree::Path(use_path) => {
collect_reexport_leaves(module, &use_path.tree, map);
}
syn::UseTree::Group(group) => {
for item in &group.items {
collect_reexport_leaves(module, item, map);
}
}
}
}
pub(crate) fn extract_deprecation(attrs: &[syn::Attribute]) -> Option<crate::core::ir::DeprecationInfo> {
attrs.iter().find_map(|attr| {
if !attr.path().is_ident("deprecated") {
return None;
}
let mut info = crate::core::ir::DeprecationInfo::default();
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("since") {
if let Ok(v) = meta.value() {
if let Ok(s) = v.parse::<syn::LitStr>() {
let raw = s.value();
info.since = Some(raw.strip_prefix('v').map(str::to_owned).unwrap_or(raw));
}
}
} else if meta.path.is_ident("note") {
if let Ok(v) = meta.value() {
if let Ok(s) = v.parse::<syn::LitStr>() {
info.note = Some(s.value());
}
}
} else if let Ok(v) = meta.value() {
let _: syn::Expr = v.parse()?;
}
Ok(())
});
Some(info)
})
}
pub(crate) fn extract_alef_since(attrs: &[syn::Attribute]) -> Option<String> {
let raw = attrs.iter().find_map(|attr| {
if attr.path().is_ident("alef") {
let mut found = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("since") {
if let Ok(v) = meta.value() {
if let Ok(s) = v.parse::<syn::LitStr>() {
found = Some(s.value());
}
}
} else if let Ok(v) = meta.value() {
let _: syn::Expr = v.parse()?;
}
Ok(())
});
return found;
}
if attr.path().is_ident("cfg_attr") {
let mut found = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("alef") {
let _ = meta.parse_nested_meta(|inner| {
if inner.path.is_ident("since") {
if let Ok(v) = inner.value() {
if let Ok(s) = v.parse::<syn::LitStr>() {
found = Some(s.value());
}
}
} else if let Ok(v) = inner.value() {
let _: syn::Expr = v.parse()?;
}
Ok(())
});
} else if let Ok(v) = meta.value() {
let _: syn::Expr = v.parse()?;
} else {
let _ = meta.parse_nested_meta(|_| Ok(()));
}
Ok(())
});
return found;
}
None
})?;
Some(raw.strip_prefix('v').map(str::to_owned).unwrap_or(raw))
}
pub(crate) fn extract_version_annotation(attrs: &[syn::Attribute]) -> crate::core::ir::VersionAnnotation {
crate::core::ir::VersionAnnotation {
since: extract_alef_since(attrs),
deprecated: extract_deprecation(attrs),
}
}
#[cfg(test)]
mod tests {
use super::{has_derive, has_derive_path, normalize_rustdoc};
#[test]
fn test_normalize_rustdoc_strips_rustdoc_hidden_lines_inside_rust_fence() {
let raw = "Convert document.\n\n```rust\n# tokio_test::block_on(async {\nuse foo::Bar;\nlet x = 1;\n# Ok::<(), Error>(())\n# });\n```\n";
let normalized = normalize_rustdoc(raw);
assert!(
!normalized.contains("tokio_test"),
"must drop tokio_test scaffolding: {normalized}"
);
assert!(
!normalized.contains("# }"),
"must drop closing brace scaffolding: {normalized}"
);
assert!(
!normalized.contains("# Ok::"),
"must drop trailing Ok scaffolding: {normalized}"
);
assert!(normalized.contains("use foo::Bar;"));
assert!(normalized.contains("let x = 1;"));
}
#[test]
fn test_normalize_rustdoc_preserves_pound_outside_fence() {
let raw = "Summary line.\n\n# Errors\n\nMay fail.";
assert_eq!(normalize_rustdoc(raw), "Summary line.\n\n# Errors\n\nMay fail.");
}
#[test]
fn test_normalize_rustdoc_preserves_pound_in_non_rust_fence() {
let raw = "Example:\n\n```python\n# This is a python comment\nx = 1\n```";
let normalized = normalize_rustdoc(raw);
assert!(normalized.contains("# This is a python comment"));
}
#[test]
fn test_normalize_rustdoc_rewrites_crate_link() {
let raw = "See [`crate::ConversionOptions`] for details.";
assert_eq!(normalize_rustdoc(raw), "See `ConversionOptions` for details.");
}
#[test]
fn test_normalize_rustdoc_rewrites_super_link() {
let raw = "Inherits from [`super::ExtractionConfig`] field.";
assert_eq!(normalize_rustdoc(raw), "Inherits from `ExtractionConfig` field.");
}
#[test]
fn test_normalize_rustdoc_rewrites_link_with_target() {
let raw = "When set on [`ExtractionConfig`](super::ExtractionConfig), it works.";
assert_eq!(
normalize_rustdoc(raw),
"When set on [`ExtractionConfig`](super::ExtractionConfig), it works."
);
}
#[test]
fn test_normalize_rustdoc_self_link() {
let raw = "See [`self::Foo`] for details.";
assert_eq!(normalize_rustdoc(raw), "See `Foo` for details.");
}
#[test]
fn test_normalize_rustdoc_empty() {
assert_eq!(normalize_rustdoc(""), "");
}
#[test]
fn test_normalize_rustdoc_no_changes_for_plain_prose() {
let raw = "Plain documentation without fences or links.";
assert_eq!(normalize_rustdoc(raw), raw);
}
#[test]
fn test_normalize_rustdoc_handles_rust_no_run_fence() {
let raw = "```rust,no_run\n# async fn example() {\nlet result = foo().await;\n# }\n```";
let normalized = normalize_rustdoc(raw);
assert!(
!normalized.contains("# async fn"),
"must drop async fn scaffolding: {normalized}"
);
assert!(normalized.contains("let result = foo().await;"));
}
fn parse_attrs(input: &str) -> Vec<syn::Attribute> {
let item: syn::ItemStruct = syn::parse_str(&format!("{input} struct _Dummy;")).unwrap();
item.attrs
}
#[test]
fn test_has_derive_bare_positive() {
let attrs = parse_attrs("#[derive(Debug, Clone)]");
assert!(has_derive(&attrs, "Debug"));
assert!(has_derive(&attrs, "Clone"));
}
#[test]
fn test_has_derive_bare_negative() {
let attrs = parse_attrs("#[derive(Debug)]");
assert!(!has_derive(&attrs, "Clone"));
}
#[test]
fn test_has_derive_cfg_attr_simple() {
let attrs = parse_attrs(r#"#[cfg_attr(feature = "x", derive(Foo))]"#);
assert!(has_derive(&attrs, "Foo"));
assert!(!has_derive(&attrs, "Bar"));
}
#[test]
fn test_has_derive_cfg_attr_multi_derive() {
let attrs = parse_attrs(r#"#[cfg_attr(feature = "x", derive(Foo, Bar, Baz))]"#);
assert!(has_derive(&attrs, "Foo"));
assert!(has_derive(&attrs, "Bar"));
assert!(has_derive(&attrs, "Baz"));
assert!(!has_derive(&attrs, "Qux"));
}
#[test]
fn test_has_derive_cfg_attr_any_condition() {
let attrs = parse_attrs(r#"#[cfg_attr(any(feature = "x", test), derive(thiserror::Error))]"#);
assert!(has_derive(&attrs, "Error"));
assert!(!has_derive(&attrs, "thiserror"));
}
#[test]
fn test_has_derive_cfg_attr_qualified_path_last_segment() {
let attrs = parse_attrs(r#"#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]"#);
assert!(has_derive(&attrs, "Serialize"));
assert!(has_derive(&attrs, "Deserialize"));
assert!(!has_derive(&attrs, "serde"));
}
#[test]
fn test_has_derive_cfg_attr_negative_no_derive() {
let attrs = parse_attrs(r#"#[cfg_attr(feature = "x", serde(rename_all = "camelCase"))]"#);
assert!(!has_derive(&attrs, "Serialize"));
}
#[test]
fn test_has_derive_path_bare_single_segment() {
let attrs = parse_attrs("#[derive(Debug)]");
assert!(has_derive_path(&attrs, &["Debug"]));
assert!(!has_derive_path(&attrs, &["Clone"]));
}
#[test]
fn test_has_derive_path_bare_multi_segment() {
let attrs = parse_attrs("#[derive(thiserror::Error)]");
assert!(has_derive_path(&attrs, &["thiserror", "Error"]));
assert!(!has_derive_path(&attrs, &["Error"]));
assert!(!has_derive_path(&attrs, &["thiserror"]));
}
#[test]
fn test_has_derive_path_cfg_attr_simple() {
let attrs = parse_attrs(r#"#[cfg_attr(feature = "x", derive(Foo))]"#);
assert!(has_derive_path(&attrs, &["Foo"]));
assert!(!has_derive_path(&attrs, &["Bar"]));
}
#[test]
fn test_has_derive_path_cfg_attr_multi_segment() {
let attrs = parse_attrs(r#"#[cfg_attr(feature = "x", derive(thiserror::Error))]"#);
assert!(has_derive_path(&attrs, &["thiserror", "Error"]));
assert!(!has_derive_path(&attrs, &["Error"]));
}
#[test]
fn test_has_derive_path_cfg_attr_any_condition() {
let attrs = parse_attrs(r#"#[cfg_attr(any(feature = "x", test), derive(thiserror::Error))]"#);
assert!(has_derive_path(&attrs, &["thiserror", "Error"]));
assert!(!has_derive_path(&attrs, &["thiserror"]));
assert!(!has_derive_path(&attrs, &["Error"]));
}
#[test]
fn test_has_derive_path_cfg_attr_negative() {
let attrs = parse_attrs(r#"#[cfg_attr(feature = "x", serde(rename_all = "camelCase"))]"#);
assert!(!has_derive_path(&attrs, &["serde"]));
assert!(!has_derive_path(&attrs, &["rename_all"]));
}
#[test]
fn test_has_derive_path_empty_attrs() {
let attrs: Vec<syn::Attribute> = vec![];
assert!(!has_derive(&attrs, "Debug"));
assert!(!has_derive_path(&attrs, &["Debug"]));
}
use super::detect_core_wrapper;
use crate::core::ir::CoreWrapper;
fn parse_type(s: &str) -> syn::Type {
syn::parse_str(s).unwrap()
}
#[test]
fn test_detect_core_wrapper_arc_mutex_returns_arc_mutex() {
let ty = parse_type("Arc<Mutex<HashMap<String, String>>>");
assert_eq!(detect_core_wrapper(&ty), CoreWrapper::ArcMutex);
}
#[test]
fn test_detect_core_wrapper_arc_rwlock_returns_arc_mutex() {
let ty = parse_type("Arc<RwLock<Vec<u8>>>");
assert_eq!(detect_core_wrapper(&ty), CoreWrapper::ArcMutex);
}
#[test]
fn test_detect_core_wrapper_option_arc_mutex_peeks_through_option() {
let ty = parse_type("Option<Arc<Mutex<String>>>");
assert_eq!(detect_core_wrapper(&ty), CoreWrapper::ArcMutex);
}
#[test]
fn test_detect_core_wrapper_plain_arc_returns_arc() {
let ty = parse_type("Arc<String>");
assert_eq!(detect_core_wrapper(&ty), CoreWrapper::Arc);
}
#[test]
fn test_detect_core_wrapper_arc_dyn_trait_stays_plain_arc() {
let ty = parse_type("Arc<dyn MyTrait>");
assert_eq!(detect_core_wrapper(&ty), CoreWrapper::Arc);
}
#[test]
fn test_detect_core_wrapper_tokio_sync_mutex_last_segment_match() {
let ty = parse_type("Arc<tokio::sync::Mutex<u64>>");
assert_eq!(detect_core_wrapper(&ty), CoreWrapper::ArcMutex);
}
#[test]
fn test_extract_deprecation_bare_deprecated_returns_empty_info() {
let attrs = parse_attrs("#[deprecated]");
let result = super::extract_deprecation(&attrs);
assert!(result.is_some());
let info = result.unwrap();
assert!(info.since.is_none());
assert!(info.note.is_none());
}
#[test]
fn test_extract_deprecation_with_since_returns_version() {
let attrs = parse_attrs(r#"#[deprecated(since = "1.2.0")]"#);
let info = super::extract_deprecation(&attrs).unwrap();
assert_eq!(info.since.as_deref(), Some("1.2.0"));
assert!(info.note.is_none());
}
#[test]
fn test_extract_deprecation_with_note_returns_message() {
let attrs = parse_attrs(r#"#[deprecated(note = "use new_fn instead")]"#);
let info = super::extract_deprecation(&attrs).unwrap();
assert!(info.since.is_none());
assert_eq!(info.note.as_deref(), Some("use new_fn instead"));
}
#[test]
fn test_extract_deprecation_with_both_fields() {
let attrs = parse_attrs(r#"#[deprecated(since = "2.0.0", note = "use new_fn instead")]"#);
let info = super::extract_deprecation(&attrs).unwrap();
assert_eq!(info.since.as_deref(), Some("2.0.0"));
assert_eq!(info.note.as_deref(), Some("use new_fn instead"));
}
#[test]
fn test_extract_deprecation_absent_returns_none() {
let attrs = parse_attrs("#[derive(Clone)]");
assert!(super::extract_deprecation(&attrs).is_none());
}
#[test]
fn test_extract_alef_since_bare_alef_since_returns_version() {
let attrs = parse_attrs(r#"#[alef(since = "1.5.0")]"#);
assert_eq!(super::extract_alef_since(&attrs).as_deref(), Some("1.5.0"));
}
#[test]
fn test_extract_alef_since_cfg_attr_alef_since_returns_version() {
let attrs = parse_attrs(r#"#[cfg_attr(feature = "alef-meta", alef(since = "0.9.0"))]"#);
assert_eq!(super::extract_alef_since(&attrs).as_deref(), Some("0.9.0"));
}
#[test]
fn test_extract_alef_since_strips_leading_v_prefix() {
let attrs = parse_attrs(r#"#[alef(since = "v1.2.0")]"#);
assert_eq!(super::extract_alef_since(&attrs).as_deref(), Some("1.2.0"));
}
#[test]
fn test_extract_deprecation_since_strips_leading_v_prefix() {
let attrs = parse_attrs(r#"#[deprecated(since = "v2.0.0", note = "use new_fn")]"#);
let info = super::extract_deprecation(&attrs).unwrap();
assert_eq!(info.since.as_deref(), Some("2.0.0"));
}
#[test]
fn test_extract_alef_since_absent_returns_none() {
let attrs = parse_attrs("#[alef(skip)]");
assert!(super::extract_alef_since(&attrs).is_none());
}
}