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 {275x29
let mut lines = vec![];275x30
for attr in attrs.iter() {518x31
let Meta::NameValue(MetaNameValue {32
path,313x33
value: Expr::Lit(ExprLit {34
lit: Lit::Str(s), ..313x35
}),36
..37
}) = &attr.meta313x38
else {39
continue;205x40
};41
if path.is_ident("doc") {313x42
let v = s.value();313x43
let mut trimmed = v.trim_end();313x44
if let Some(t) = trimmed.strip_prefix(' ') {313x45
trimmed = t;305x46
}305x47
if !trimmed.is_empty() {313x48
lines.push(trimmed.to_string());305x49
}305x50
}51
}52
lines.join("\n")275x53
}275x54
55
fn get_serde_aliases_from_attrs(ident: &Ident, attrs: &[Attribute]) -> Vec<String> {273x56
let mut aliases = vec![];273x57
for attr in attrs.iter() {514x58
if !attr.path().is_ident("serde") {514x59
continue;362x60
}152x61
let Ok(nested) = attr.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)152x62
else {63
continue;64
};65
for meta in nested {166x66
let Meta::NameValue(MetaNameValue {67
path,86x68
value:69
Expr::Lit(ExprLit {70
lit: Lit::Str(s), ..86x71
}),72
..73
}) = meta86x74
else {75
continue;80x76
};77
if !path.is_ident("alias") {86x78
continue;12x79
}74x80
aliases.push(s.value());74x81
}82
}83
aliases.push(ident.to_string());273x84
aliases.sort_by(|l, r| {273x85
if l.len() != r.len() {80x86
l.len().cmp(&r.len())34x87
} 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
});80x103
aliases273x104
}273x105
106
fn has_aco_attr(attrs: &[Attribute], name: &str) -> bool {499x107
for attr in attrs.iter() {893x108
if !attr.path().is_ident("serde_aco") {893x109
continue;881x110
}12x111
let mut found = false;12x112
let has_name = |meta: ParseNestedMeta| {12x113
if meta.path.is_ident(name) {12x114
found = true;10x115
}10x116
Ok(())12x117
};12x118
if attr.parse_nested_meta(has_name).is_err() {12x119
continue;120
}12x121
if found {12x122
return true;10x123
}2x124
}125
false489x126
}499x127
128
fn is_hidden(attrs: &[Attribute]) -> bool {283x129
has_aco_attr(attrs, "hide")283x130
}283x131
132
fn is_flattened(attrs: &[Attribute]) -> bool {216x133
has_aco_attr(attrs, "flatten")216x134
}216x135
136
fn derive_named_struct_help(name: &Ident, fields: &FieldsNamed) -> TokenStream2 {96x137
let mut field_docs = Vec::new();96x138
for field in &fields.named {219x139
if is_hidden(&field.attrs) {219x140
continue;3x141
}216x142
let aliases;143
let ident = if is_flattened(&field.attrs) {216x144
""2x145
} else {146
aliases = get_serde_aliases_from_attrs(field.ident.as_ref().unwrap(), &field.attrs);214x147
&aliases[0]214x148
};149
let ty = &field.ty;216x150
let doc = get_doc_from_attrs(&field.attrs);216x151
field_docs.push(quote! {216x152
FieldHelp {153
ident: #ident,154
doc: #doc,155
ty: <#ty as Help>::HELP,156
}157
})158
}159
160
quote! {96x161
TypedHelp::Struct{162
name: stringify!(#name),163
fields: &[#(#field_docs,)*],164
}165
}166
}96x167
168
fn derive_unnamed_struct_help(fields: &FieldsUnnamed) -> TokenStream2 {36x169
if let Some(first) = fields.unnamed.first() {36x170
let ty = &first.ty;36x171
quote! { <#ty as Help>::HELP }36x172
} else if fields.unnamed.is_empty() {173
quote! { TypedHelp::Unit }174
} else {175
panic!("Unnamed struct must have only one field")176
}177
}36x178
179
fn derive_struct_help(name: &Ident, data: &DataStruct) -> TokenStream2 {95x180
match &data.fields {95x181
Fields::Named(fields) => derive_named_struct_help(name, fields),89x182
Fields::Unnamed(fields) => derive_unnamed_struct_help(fields),6x183
Fields::Unit => quote! { TypedHelp::Unit },184
}185
}95x186
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;5x192
}59x193
let doc = get_doc_from_attrs(&variant.attrs);59x194
let ty = match &variant.fields {59x195
Fields::Unit => quote! {TypedHelp::Unit},22x196
Fields::Named(fields) => derive_named_struct_help(name, fields),7x197
Fields::Unnamed(fields) => derive_unnamed_struct_help(fields),30x198
};199
let aliases = get_serde_aliases_from_attrs(&variant.ident, &variant.attrs);59x200
let ident = &aliases[0];59x201
variant_docs.push(quote! {59x202
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 {136x218
let input = parse_macro_input!(input as DeriveInput);136x219
let ty_name = &input.ident;136x220
let body = match &input.data {136x221
Data::Struct(data) => derive_struct_help(ty_name, data),95x222
Data::Enum(data) => derive_enum_help(ty_name, data),41x223
Data::Union(_) => unimplemented!("Data::Union not supported"),224
};225
TokenStream::from(quote! {136x226
const _:() = {227
use ::serde_aco::{Help, TypedHelp, FieldHelp};228
impl Help for #ty_name {229
const HELP: TypedHelp = #body;230
}231
};232
})233
}136x234