help.rs90.98%
1
// Copyright 2024 Google LLC2
//3
// Licensed under the Apache License, Version 2.0 (the "License");4
// you may not use this file except in compliance with the License.5
// You may obtain a copy of the License at6
//7
// https://www.apache.org/licenses/LICENSE-2.08
//9
// Unless required by applicable law or agreed to in writing, software10
// distributed under the License is distributed on an "AS IS" BASIS,11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12
// See the License for the specific language governing permissions and13
// limitations under the License.14
15
use std::cmp::Ordering;16
use std::iter::zip;17
18
use proc_macro::TokenStream;19
use proc_macro2::TokenStream as TokenStream2;20
use quote::quote;21
use syn::meta::ParseNestedMeta;22
use syn::punctuated::Punctuated;23
use syn::{24
Attribute, Data, DataEnum, DataStruct, DeriveInput, Expr, ExprLit, Fields, FieldsNamed,25
FieldsUnnamed, Ident, Lit, Meta, MetaNameValue, Token, parse_macro_input,26
};27
28
fn get_doc_from_attrs(attrs: &[Attribute]) -> String {279x29
let mut lines = vec![];279x30
for attr in attrs.iter() {528x31
let Meta::NameValue(MetaNameValue {32
path,319x33
value: Expr::Lit(ExprLit {34
lit: Lit::Str(s), ..319x35
}),36
..37
}) = &attr.meta319x38
else {39
continue;209x40
};41
if path.is_ident("doc") {319x42
let v = s.value();319x43
let mut trimmed = v.trim_end();319x44
if let Some(t) = trimmed.strip_prefix(' ') {319x45
trimmed = t;311x46
}311x47
if !trimmed.is_empty() {319x48
lines.push(trimmed.to_string());311x49
}311x50
}51
}52
lines.join("\n")279x53
}279x54
55
fn get_serde_aliases_from_attrs(ident: &Ident, attrs: &[Attribute]) -> Vec<String> {277x56
let mut aliases = vec![];277x57
for attr in attrs.iter() {524x58
if !attr.path().is_ident("serde") {524x59
continue;370x60
}154x61
let Ok(nested) = attr.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)154x62
else {63
continue;64
};65
for meta in nested {168x66
let Meta::NameValue(MetaNameValue {67
path,88x68
value:69
Expr::Lit(ExprLit {70
lit: Lit::Str(s), ..88x71
}),72
..73
}) = meta88x74
else {75
continue;80x76
};77
if !path.is_ident("alias") {88x78
continue;12x79
}76x80
aliases.push(s.value());76x81
}82
}83
aliases.push(ident.to_string());277x84
aliases.sort_by(|l, r| {277x85
if l.len() != r.len() {82x86
l.len().cmp(&r.len())36x87
} else {88
for (a, b) in zip(l.chars(), r.chars()) {46x89
if a == b {46x90
continue;91
}46x92
if a.is_lowercase() == b.is_lowercase() {46x93
return a.cmp(&b);94
} else if a.is_lowercase() {46x95
return Ordering::Less;96
} else {97
return Ordering::Greater;46x98
}99
}100
Ordering::Equal101
}102
});82x103
aliases277x104
}277x105
106
fn has_aco_attr(attrs: &[Attribute], name: &str) -> bool {503x107
for attr in attrs.iter() {899x108
if !attr.path().is_ident("serde_aco") {899x109
continue;889x110
}10x111
let mut found = false;10x112
let has_name = |meta: ParseNestedMeta| {10x113
if meta.path.is_ident(name) {10x114
found = true;8x115
}8x116
Ok(())10x117
};10x118
if attr.parse_nested_meta(has_name).is_err() {10x119
continue;120
}10x121
if found {10x122
return true;8x123
}2x124
}125
false495x126
}503x127
128
fn is_hidden(attrs: &[Attribute]) -> bool {285x129
has_aco_attr(attrs, "hide")285x130
}285x131
132
fn is_flattened(attrs: &[Attribute]) -> bool {218x133
has_aco_attr(attrs, "flatten")218x134
}218x135
136
fn derive_named_struct_help(name: &Ident, fields: &FieldsNamed) -> TokenStream2 {98x137
let mut field_docs = Vec::new();98x138
for field in &fields.named {221x139
if is_hidden(&field.attrs) {221x140
continue;3x141
}218x142
let aliases;143
let ident = if is_flattened(&field.attrs) {218x144
""2x145
} else {146
aliases = get_serde_aliases_from_attrs(field.ident.as_ref().unwrap(), &field.attrs);216x147
&aliases[0]216x148
};149
let ty = &field.ty;218x150
let doc = get_doc_from_attrs(&field.attrs);218x151
field_docs.push(quote! {218x152
FieldHelp {153
ident: #ident,154
doc: #doc,155
ty: <#ty as Help>::HELP,156
}157
})158
}159
160
quote! {98x161
TypedHelp::Struct{162
name: stringify!(#name),163
fields: &[#(#field_docs,)*],164
}165
}166
}98x167
168
fn derive_unnamed_struct_help(fields: &FieldsUnnamed) -> TokenStream2 {48x169
if let Some(first) = fields.unnamed.first() {48x170
let ty = &first.ty;48x171
quote! { <#ty as Help>::HELP }48x172
} else if fields.unnamed.is_empty() {173
quote! { TypedHelp::Unit }174
} else {175
panic!("Unnamed struct must have only one field")176
}177
}48x178
179
fn derive_struct_help(name: &Ident, data: &DataStruct) -> TokenStream2 {107x180
match &data.fields {107x181
Fields::Named(fields) => derive_named_struct_help(name, fields),89x182
Fields::Unnamed(fields) => derive_unnamed_struct_help(fields),18x183
Fields::Unit => quote! { TypedHelp::Unit },184
}185
}107x186
187
fn derive_enum_help(name: &Ident, data: &DataEnum) -> TokenStream2 {41x188
let mut variant_docs = vec![];41x189
for variant in data.variants.iter() {65x190
if is_hidden(&variant.attrs) {64x191
continue;3x192
}61x193
let doc = get_doc_from_attrs(&variant.attrs);61x194
let ty = match &variant.fields {61x195
Fields::Unit => quote! {TypedHelp::Unit},22x196
Fields::Named(fields) => derive_named_struct_help(name, fields),9x197
Fields::Unnamed(fields) => derive_unnamed_struct_help(fields),30x198
};199
let aliases = get_serde_aliases_from_attrs(&variant.ident, &variant.attrs);61x200
let ident = &aliases[0];61x201
variant_docs.push(quote! {61x202
FieldHelp {203
ident: #ident,204
doc: #doc,205
ty: #ty,206
}207
})208
}209
quote! {41x210
TypedHelp::Enum {211
name: stringify!(#name),212
variants: &[#(#variant_docs,)*],213
}214
}215
}41x216
217
pub fn derive_help(input: TokenStream) -> TokenStream {148x218
let input = parse_macro_input!(input as DeriveInput);148x219
let ty_name = &input.ident;148x220
let body = match &input.data {148x221
Data::Struct(data) => derive_struct_help(ty_name, data),107x222
Data::Enum(data) => derive_enum_help(ty_name, data),41x223
Data::Union(_) => unimplemented!("Data::Union not supported"),224
};225
TokenStream::from(quote! {148x226
const _:() = {227
use ::serde_aco::{Help, TypedHelp, FieldHelp};228
impl Help for #ty_name {229
const HELP: TypedHelp = #body;230
}231
};232
})233
}148x234