typeshare_engine/
parser.rs

1//! Source file parsing.
2use ignore::Walk;
3use itertools::Itertools;
4use proc_macro2::{Delimiter, Group};
5use rayon::iter::{IntoParallelIterator, ParallelIterator};
6use std::{
7    collections::{hash_map::Entry, HashMap, HashSet},
8    path::PathBuf,
9};
10use syn::{
11    ext::IdentExt,
12    parse::{Parse, Parser},
13    punctuated::Punctuated,
14    visit::Visit,
15    Attribute, Expr, ExprGroup, ExprLit, ExprParen, Fields, GenericParam, Ident, ItemConst,
16    ItemEnum, ItemStruct, ItemType, Lit, Meta, Token,
17};
18
19use typeshare_model::{
20    decorator::{self, DecoratorSet},
21    prelude::*,
22};
23
24use crate::{
25    rename::RenameExt,
26    target_os,
27    type_parser::{parse_rust_type, parse_rust_type_from_string, type_name},
28    visitors::TypeShareVisitor,
29    FileParseErrors, ParseError, ParseErrorKind, ParseErrorSet,
30};
31
32const SERDE: &str = "serde";
33const TYPESHARE: &str = "typeshare";
34
35/// An enum that encapsulates units of code generation for Typeshare.
36/// Analogous to `syn::Item`, even though our variants are more limited.
37#[non_exhaustive]
38#[derive(Debug, Clone)]
39pub enum RustItem {
40    /// A `struct` definition
41    Struct(RustStruct),
42    /// An `enum` definition
43    Enum(RustEnum),
44    /// A `type` definition or newtype struct.
45    Alias(RustTypeAlias),
46    /// A `const` definition
47    Const(RustConst),
48}
49
50/// The results of parsing Rust source input.
51#[derive(Default, Debug)]
52pub struct ParsedData {
53    /// Structs defined in the source
54    pub structs: Vec<RustStruct>,
55    /// Enums defined in the source
56    pub enums: Vec<RustEnum>,
57    /// Type aliases defined in the source
58    pub aliases: Vec<RustTypeAlias>,
59    /// Constant variables defined in the source
60    pub consts: Vec<RustConst>,
61    /// Imports used by this file
62    /// TODO: This is currently almost empty. Import computation was found to
63    /// be pretty broken during the migration to Typeshare 2, so that part
64    /// of multi-file output was stripped out to be restored later.
65    pub import_types: HashSet<ImportedType>,
66}
67
68impl ParsedData {
69    pub fn merge(&mut self, other: Self) {
70        self.structs.extend(other.structs);
71        self.enums.extend(other.enums);
72        self.aliases.extend(other.aliases);
73        self.consts.extend(other.consts);
74        self.import_types.extend(other.import_types);
75    }
76
77    pub fn add(&mut self, item: RustItem) {
78        match item {
79            RustItem::Struct(rust_struct) => self.structs.push(rust_struct),
80            RustItem::Enum(rust_enum) => self.enums.push(rust_enum),
81            RustItem::Alias(rust_type_alias) => self.aliases.push(rust_type_alias),
82            RustItem::Const(rust_const) => self.consts.push(rust_const),
83        }
84    }
85
86    pub fn all_type_names(&self) -> impl Iterator<Item = &'_ TypeName> + use<'_> {
87        let s = self.structs.iter().map(|s| &s.id.renamed);
88        let e = self.enums.iter().map(|e| &e.shared().id.renamed);
89        let a = self.aliases.iter().map(|a| &a.id.renamed);
90        // currently we ignore consts, which aren't types. May revisit this
91        // later.
92
93        s.chain(e).chain(a)
94    }
95
96    pub fn sort_contents(&mut self) {
97        self.structs
98            .sort_unstable_by(|lhs, rhs| Ord::cmp(&lhs.id.original, &rhs.id.original));
99
100        self.enums.sort_unstable_by(|lhs, rhs| {
101            Ord::cmp(&lhs.shared().id.original, &rhs.shared().id.original)
102        });
103
104        self.aliases
105            .sort_unstable_by(|lhs, rhs| Ord::cmp(&lhs.id.original, &rhs.id.original));
106
107        self.consts
108            .sort_unstable_by(|lhs, rhs| Ord::cmp(&lhs.id.original, &rhs.id.original));
109    }
110}
111
112/// Input data for parsing each source file.
113#[derive(Debug)]
114pub struct ParserInput {
115    /// Rust source file path.
116    file_path: PathBuf,
117    /// The crate name the source file belongs to, if we could detect it
118    crate_name: Option<CrateName>,
119}
120
121/// Walk the source folder and collect all parser inputs.
122pub fn parser_inputs(walker_builder: Walk) -> Vec<ParserInput> {
123    walker_builder
124        .filter_map(Result::ok)
125        .filter(|dir_entry| !dir_entry.path().is_dir())
126        .map(|dir_entry| {
127            let path = dir_entry.path();
128            let crate_name = CrateName::find_crate_name(path);
129            let file_path = path.to_path_buf();
130
131            ParserInput {
132                file_path,
133                crate_name,
134            }
135        })
136        .collect()
137}
138
139// /// This function produces the `import_candidates`
140// /// Collect all the typeshared types into a mapping of crate names to typeshared types. This
141// /// mapping is used to lookup and generated import statements for generated files.
142// pub fn all_types(file_mappings: &HashMap<CrateName, ParsedData>) -> CrateTypes {
143//     file_mappings
144//         .iter()
145//         .map(|(crate_name, parsed_data)| (crate_name, &parsed_data.type_names))
146//         .fold(
147//             HashMap::new(),
148//             |mut import_map: CrateTypes, (crate_name, type_names)| {
149//                 match import_map.entry(crate_name.clone()) {
150//                     Entry::Occupied(mut e) => {
151//                         e.get_mut().extend(type_names.iter().cloned());
152//                     }
153//                     Entry::Vacant(e) => {
154//                         e.insert(type_names.clone());
155//                     }
156//                 }
157//                 import_map
158//             },
159//         )
160// }
161
162fn add_parsed_data(
163    container: &mut HashMap<Option<CrateName>, ParsedData>,
164    crate_name: Option<CrateName>,
165    parsed_data: ParsedData,
166) {
167    match container.entry(crate_name) {
168        Entry::Vacant(entry) => {
169            entry.insert(parsed_data);
170        }
171        Entry::Occupied(entry) => {
172            entry.into_mut().merge(parsed_data);
173        }
174    }
175}
176
177/// Collect all the parsed sources into a mapping of crate name to parsed data.
178pub fn parse_input(
179    inputs: Vec<ParserInput>,
180    ignored_types: &[&str],
181    mode: FilesMode<()>,
182    target_os: Option<&[&str]>,
183) -> Result<HashMap<Option<CrateName>, ParsedData>, Vec<FileParseErrors>> {
184    inputs
185        .into_par_iter()
186        .map(|parser_input| {
187            // Performance nit: we don't need to clone in the error case;
188            // map_err is taking unconditional ownership unnecessarily
189            let content = std::fs::read_to_string(&parser_input.file_path).map_err(|err| {
190                FileParseErrors::new(
191                    parser_input.file_path.clone(),
192                    parser_input.crate_name.clone(),
193                    crate::FileErrorKind::ReadError(err),
194                )
195            })?;
196
197            let parsed_data = parse(
198                &content,
199                ignored_types,
200                match mode {
201                    FilesMode::Single => FilesMode::Single,
202                    FilesMode::Multi(()) => match parser_input.crate_name {
203                        None => {
204                            return Err(FileParseErrors::new(
205                                parser_input.file_path.clone(),
206                                parser_input.crate_name,
207                                crate::FileErrorKind::UnknownCrate,
208                            ))
209                        }
210                        Some(ref crate_name) => FilesMode::Multi(crate_name),
211                    },
212                    _ => panic!("unsupported mode {mode:?}; this is probably a typeshare bug"),
213                },
214                target_os,
215            )
216            .map_err(|err| {
217                FileParseErrors::new(
218                    parser_input.file_path.clone(),
219                    parser_input.crate_name.clone(),
220                    crate::FileErrorKind::ParseErrors(err),
221                )
222            })?;
223
224            let parsed_data = parsed_data.and_then(|parsed_data| {
225                if is_parsed_data_empty(&parsed_data) {
226                    None
227                } else {
228                    Some(parsed_data)
229                }
230            });
231
232            Ok(parsed_data.map(|parsed_data| (parser_input.crate_name, parsed_data)))
233        })
234        .filter_map(|data| data.transpose())
235        .fold(
236            || Ok(HashMap::new()),
237            |mut accum, result| {
238                match (&mut accum, result) {
239                    (Ok(accum), Ok((crate_name, parsed_data))) => {
240                        add_parsed_data(accum, crate_name, parsed_data)
241                    }
242                    (Ok(_), Err(error)) => {
243                        accum = Err(Vec::from([error]));
244                    }
245                    (Err(accum), Err(error)) => accum.push(error),
246                    (Err(_), Ok(_)) => {}
247                }
248
249                accum
250            },
251        )
252        .reduce(
253            || Ok(HashMap::new()),
254            |old, new| match (old, new) {
255                (Ok(mut old), Ok(new)) => {
256                    new.into_iter().for_each(|(crate_name, parsed_data)| {
257                        add_parsed_data(&mut old, crate_name, parsed_data)
258                    });
259                    Ok(old)
260                }
261                (Err(errors), Ok(_)) | (Ok(_), Err(errors)) => Err(errors),
262                (Err(mut err1), Err(err2)) => {
263                    err1.extend(err2);
264                    Err(err1)
265                }
266            },
267        )
268}
269
270/// Check if we have not parsed any relavent typehsared types.
271fn is_parsed_data_empty(parsed_data: &ParsedData) -> bool {
272    parsed_data.enums.is_empty()
273        && parsed_data.aliases.is_empty()
274        && parsed_data.structs.is_empty()
275        && parsed_data.consts.is_empty()
276}
277
278/// Parse the given Rust source string into `ParsedData`.
279pub fn parse(
280    source_code: &str,
281    ignored_types: &[&str],
282    file_mode: FilesMode<&CrateName>,
283    target_os: Option<&[&str]>,
284) -> Result<Option<ParsedData>, ParseErrorSet> {
285    // We will only produce output for files that contain the `#[typeshare]`
286    // attribute, so this is a quick and easy performance win
287    if !source_code.contains("#[typeshare") {
288        return Ok(None);
289    }
290
291    // Parse and process the input, ensuring we parse only items marked with
292    // `#[typeshare]`
293    let mut import_visitor = TypeShareVisitor::new(ignored_types, file_mode, target_os);
294    let file_contents = syn::parse_file(source_code)
295        .map_err(|err| ParseError::new(&err.span(), ParseErrorKind::SynError(err)))?;
296
297    import_visitor.visit_file(&file_contents);
298
299    import_visitor.parsed_data().map(Some)
300}
301
302/// Parses a struct into a definition that more succinctly represents what
303/// typeshare needs to generate code for other languages.
304///
305/// This function can currently return something other than a struct, which is a
306/// hack.
307pub(crate) fn parse_struct(
308    s: &ItemStruct,
309    valid_os: Option<&[&str]>,
310) -> Result<RustItem, ParseError> {
311    let serde_rename_all = serde_rename_all(&s.attrs);
312
313    let generic_types = s
314        .generics
315        .params
316        .iter()
317        .filter_map(|param| match param {
318            GenericParam::Type(type_param) => Some(type_name(&type_param.ident)),
319            _ => None,
320        })
321        .collect();
322
323    let decorators = get_decorators(&s.attrs);
324
325    // Check if this struct should be parsed as a type alias.
326    // TODO: we shouldn't lie and return a type alias when parsing a struct. this
327    // is a temporary hack
328    if let Some(ty) = get_serialized_as_type(&decorators) {
329        return Ok(RustItem::Alias(RustTypeAlias {
330            id: get_ident(Some(&s.ident), &s.attrs, None),
331            ty: parse_rust_type_from_string(&ty)?,
332            comments: parse_comment_attrs(&s.attrs),
333            generic_types,
334            decorators,
335        }));
336    }
337
338    Ok(match &s.fields {
339        // Structs
340        Fields::Named(f) => {
341            let fields = f
342                .named
343                .iter()
344                .filter(|field| !is_skipped(&field.attrs))
345                .filter(|field| match valid_os {
346                    Some(valid) => check_target_os(&field.attrs, valid),
347                    None => true,
348                })
349                .map(|f| {
350                    let decorators = get_decorators(&f.attrs);
351
352                    let ty = match get_serialized_as_type(&decorators) {
353                        Some(ty) => parse_rust_type_from_string(&ty)?,
354                        None => parse_rust_type(&f.ty)?,
355                    };
356
357                    if serde_flatten(&f.attrs) {
358                        return Err(ParseError::new(&f, ParseErrorKind::SerdeFlattenNotAllowed));
359                    }
360
361                    let has_default = serde_default(&f.attrs);
362
363                    Ok(RustField {
364                        id: get_ident(f.ident.as_ref(), &f.attrs, serde_rename_all.as_deref()),
365                        ty,
366                        comments: parse_comment_attrs(&f.attrs),
367                        has_default,
368                        decorators,
369                    })
370                })
371                .collect::<Result<_, ParseError>>()?;
372
373            RustItem::Struct(RustStruct {
374                id: get_ident(Some(&s.ident), &s.attrs, None),
375                generic_types,
376                fields,
377                comments: parse_comment_attrs(&s.attrs),
378                decorators,
379            })
380        }
381        // Tuple structs
382        Fields::Unnamed(fields) => {
383            let Some(field) = fields.unnamed.iter().exactly_one().ok() else {
384                return Err(ParseError::new(fields, ParseErrorKind::ComplexTupleStruct));
385            };
386
387            let field_decorators = get_decorators(&field.attrs);
388
389            let ty = match get_serialized_as_type(&field_decorators) {
390                Some(ty) => parse_rust_type_from_string(&ty)?,
391                None => parse_rust_type(&field.ty)?,
392            };
393
394            RustItem::Alias(RustTypeAlias {
395                id: get_ident(Some(&s.ident), &s.attrs, None),
396                ty: ty,
397                comments: parse_comment_attrs(&s.attrs),
398                generic_types,
399                decorators,
400            })
401        }
402        // Unit structs or `None`
403        Fields::Unit => RustItem::Struct(RustStruct {
404            id: get_ident(Some(&s.ident), &s.attrs, None),
405            generic_types,
406            fields: vec![],
407            comments: parse_comment_attrs(&s.attrs),
408            decorators,
409        }),
410    })
411}
412
413/// Parses an enum into a definition that more succinctly represents what
414/// typeshare needs to generate code for other languages.
415///
416/// This function can currently return something other than an enum, which is a
417/// hack.
418pub(crate) fn parse_enum(e: &ItemEnum, valid_os: Option<&[&str]>) -> Result<RustItem, ParseError> {
419    let generic_types = e
420        .generics
421        .params
422        .iter()
423        .filter_map(|param| match param {
424            GenericParam::Type(type_param) => Some(type_name(&type_param.ident)),
425            _ => None,
426        })
427        .collect();
428
429    let serde_rename_all = serde_rename_all(&e.attrs);
430    let decorators = get_decorators(&e.attrs);
431
432    // TODO: we shouldn't lie and return a type alias when parsing an enum. this
433    // is a temporary hack
434    if let Some(ty) = get_serialized_as_type(&decorators) {
435        return Ok(RustItem::Alias(RustTypeAlias {
436            id: get_ident(Some(&e.ident), &e.attrs, None),
437            ty: parse_rust_type_from_string(&ty)?,
438            comments: parse_comment_attrs(&e.attrs),
439            generic_types,
440            decorators,
441        }));
442    }
443
444    let original_enum_ident = type_name(&e.ident);
445
446    // Grab the `#[serde(tag = "...", content = "...")]` values if they exist
447    let maybe_tag_key = get_tag_key(&e.attrs);
448    let maybe_content_key = get_content_key(&e.attrs);
449
450    // Parse all of the enum's variants
451    let variants = e
452        .variants
453        .iter()
454        // Filter out variants we've been told to skip
455        .filter(|v| !is_skipped(&v.attrs))
456        .filter(|field| match valid_os {
457            Some(valid) => check_target_os(&field.attrs, valid),
458            None => true,
459        })
460        .map(|v| parse_enum_variant(v, serde_rename_all.as_deref(), valid_os))
461        .collect::<Result<Vec<_>, _>>()?;
462
463    // Check if the enum references itself recursively in any of its variants
464    let is_recursive = variants.iter().any(|v| match v {
465        RustEnumVariant::Unit(_) => false,
466        RustEnumVariant::Tuple { ty, .. } => ty.contains_type(&original_enum_ident),
467        RustEnumVariant::AnonymousStruct { fields, .. } => fields
468            .iter()
469            .any(|f| f.ty.contains_type(&original_enum_ident)),
470        _ => panic!("unrecgonized enum type"),
471    });
472
473    let shared = RustEnumShared {
474        id: get_ident(Some(&e.ident), &e.attrs, None),
475        comments: parse_comment_attrs(&e.attrs),
476        decorators,
477        generic_types,
478        is_recursive,
479    };
480
481    // Figure out if we're dealing with a unit enum or an algebraic enum
482    if variants
483        .iter()
484        .all(|v| matches!(v, RustEnumVariant::Unit(_)))
485    {
486        // All enum variants are unit-type
487        if maybe_tag_key.is_some() {
488            return Err(ParseError::new(
489                &e,
490                ParseErrorKind::SerdeTagNotAllowed {
491                    enum_ident: original_enum_ident,
492                },
493            ));
494        }
495        if maybe_content_key.is_some() {
496            return Err(ParseError::new(
497                &e,
498                ParseErrorKind::SerdeContentNotAllowed {
499                    enum_ident: original_enum_ident,
500                },
501            ));
502        }
503
504        Ok(RustItem::Enum(RustEnum::Unit {
505            shared,
506            unit_variants: variants
507                .into_iter()
508                .map(|variant| match variant {
509                    RustEnumVariant::Unit(unit) => unit,
510                    _ => unreachable!("non-unit variant; this was checked earlier"),
511                })
512                .collect(),
513        }))
514    } else {
515        // At least one enum variant is either a tuple or an anonymous struct
516        Ok(RustItem::Enum(RustEnum::Algebraic {
517            tag_key: maybe_tag_key.ok_or_else(|| {
518                ParseError::new(
519                    &e,
520                    ParseErrorKind::SerdeTagRequired {
521                        enum_ident: original_enum_ident.clone(),
522                    },
523                )
524            })?,
525            content_key: maybe_content_key.ok_or_else(|| {
526                ParseError::new(
527                    &e,
528                    ParseErrorKind::SerdeContentRequired {
529                        enum_ident: original_enum_ident.clone(),
530                    },
531                )
532            })?,
533            shared,
534            variants,
535        }))
536    }
537}
538
539/// Parse an enum variant.
540fn parse_enum_variant(
541    v: &syn::Variant,
542    enum_serde_rename_all: Option<&str>,
543    valid_os: Option<&[&str]>,
544) -> Result<RustEnumVariant, ParseError> {
545    let shared = RustEnumVariantShared {
546        id: get_ident(Some(&v.ident), &v.attrs, enum_serde_rename_all),
547        comments: parse_comment_attrs(&v.attrs),
548    };
549
550    // Get the value of `#[serde(rename_all)]` for this specific variant rather
551    // than the overall enum
552    //
553    // The value of the attribute for the enum overall does not apply to enum
554    // variant fields.
555    let variant_serde_rename_all = serde_rename_all(&v.attrs);
556
557    match &v.fields {
558        syn::Fields::Unit => Ok(RustEnumVariant::Unit(shared)),
559        syn::Fields::Unnamed(associated_type) => {
560            let Some(field) = associated_type.unnamed.iter().exactly_one().ok() else {
561                return Err(ParseError::new(
562                    associated_type,
563                    ParseErrorKind::MultipleUnnamedAssociatedTypes,
564                ));
565            };
566            let decorators = get_decorators(&field.attrs);
567
568            let ty = match get_serialized_as_type(&decorators) {
569                Some(ty) => parse_rust_type_from_string(&ty)?,
570                None => parse_rust_type(&field.ty)?,
571            };
572
573            Ok(RustEnumVariant::Tuple { ty, shared })
574        }
575        syn::Fields::Named(fields_named) => Ok(RustEnumVariant::AnonymousStruct {
576            fields: fields_named
577                .named
578                .iter()
579                .filter(|f| !is_skipped(&f.attrs))
580                .filter(|field| match valid_os {
581                    Some(valid) => check_target_os(&field.attrs, valid),
582                    None => true,
583                })
584                .map(|f| {
585                    let decorators = get_decorators(&f.attrs);
586
587                    let field_type = match get_serialized_as_type(&decorators) {
588                        Some(ty) => parse_rust_type_from_string(&ty)?,
589                        None => parse_rust_type(&f.ty)?,
590                    };
591
592                    let has_default = serde_default(&f.attrs);
593
594                    Ok(RustField {
595                        id: get_ident(
596                            f.ident.as_ref(),
597                            &f.attrs,
598                            variant_serde_rename_all.as_deref(),
599                        ),
600                        ty: field_type,
601                        comments: parse_comment_attrs(&f.attrs),
602                        has_default,
603                        decorators,
604                    })
605                })
606                .try_collect()?,
607            shared,
608        }),
609    }
610}
611
612/// Parses a type alias into a definition that more succinctly represents what
613/// typeshare needs to generate code for other languages.
614pub(crate) fn parse_type_alias(t: &ItemType) -> Result<RustItem, ParseError> {
615    let decorators = get_decorators(&t.attrs);
616
617    let ty = match get_serialized_as_type(&decorators) {
618        Some(ty) => parse_rust_type_from_string(&ty)?,
619        None => parse_rust_type(&t.ty)?,
620    };
621
622    let generic_types = t
623        .generics
624        .params
625        .iter()
626        .filter_map(|param| match param {
627            GenericParam::Type(type_param) => Some(type_name(&type_param.ident)),
628            _ => None,
629        })
630        .collect();
631
632    Ok(RustItem::Alias(RustTypeAlias {
633        id: get_ident(Some(&t.ident), &t.attrs, None),
634        ty,
635        comments: parse_comment_attrs(&t.attrs),
636        generic_types,
637        decorators,
638    }))
639}
640
641/// Parses a const variant.
642pub(crate) fn parse_const(c: &ItemConst) -> Result<RustItem, ParseError> {
643    let expr = parse_const_expr(&c.expr)?;
644    let decorators = get_decorators(&c.attrs);
645
646    // serialized_as needs to be supported in case the user wants to use a different type
647    // for the constant variable in a different language
648    let ty = match get_serialized_as_type(&decorators) {
649        Some(ty) => parse_rust_type_from_string(ty)?,
650        None => parse_rust_type(&c.ty)?,
651    };
652
653    match &ty {
654        RustType::Special(SpecialRustType::HashMap(_, _))
655        | RustType::Special(SpecialRustType::Vec(_))
656        | RustType::Special(SpecialRustType::Option(_)) => {
657            return Err(ParseError::new(&c.ty, ParseErrorKind::RustConstTypeInvalid));
658        }
659        RustType::Special(_) => (),
660        RustType::Simple { .. } => (),
661        _ => return Err(ParseError::new(&c.ty, ParseErrorKind::RustConstTypeInvalid)),
662    };
663
664    Ok(RustItem::Const(RustConst {
665        id: get_ident(Some(&c.ident), &c.attrs, None),
666        ty,
667        expr,
668    }))
669}
670
671fn parse_const_expr(e: &Expr) -> Result<RustConstExpr, ParseError> {
672    let value = match e {
673        Expr::Lit(ExprLit {
674            lit: Lit::Int(lit), ..
675        }) => lit
676            .base10_parse()
677            .map_err(|_| ParseError::new(&lit, ParseErrorKind::RustConstExprInvalid))?,
678
679        Expr::Group(ExprGroup { expr, .. }) | Expr::Paren(ExprParen { expr, .. }) => {
680            return parse_const_expr(expr)
681        }
682        _ => return Err(ParseError::new(e, ParseErrorKind::RustConstExprInvalid)),
683    };
684
685    Ok(RustConstExpr::Int(value))
686}
687
688// Helpers
689
690/// Checks the given attrs for `#[typeshare]`
691pub(crate) fn has_typeshare_annotation(attrs: &[syn::Attribute]) -> bool {
692    attrs
693        .iter()
694        .flat_map(|attr| attr.path().segments.clone())
695        .any(|segment| segment.ident == TYPESHARE)
696}
697
698pub(crate) fn serde_rename_all(attrs: &[syn::Attribute]) -> Option<String> {
699    get_name_value_meta_items(attrs, "rename_all", SERDE).next()
700}
701
702pub(crate) fn get_serialized_as_type(decorators: &DecoratorSet) -> Option<&str> {
703    // TODO: what to do if there are multiple instances of serialized_as?
704    match decorators.get("serialized_as")? {
705        decorator::Value::String(s) => Some(s),
706        _ => None,
707    }
708}
709
710pub(crate) fn get_name_value_meta_items<'a>(
711    attrs: &'a [syn::Attribute],
712    name: &'a str,
713    ident: &'static str,
714) -> impl Iterator<Item = String> + 'a {
715    attrs.iter().flat_map(move |attr| {
716        get_meta_items(attr, ident)
717            .iter()
718            .filter_map(|arg| match arg {
719                Meta::NameValue(name_value) if name_value.path.is_ident(name) => {
720                    expr_to_string(&name_value.value)
721                }
722                _ => None,
723            })
724            .collect::<Vec<_>>()
725    })
726}
727
728/// Returns all arguments passed into `#[{ident}(...)]` where `{ident}` can be `serde` or `typeshare` attributes
729fn get_meta_items(attr: &syn::Attribute, ident: &str) -> Vec<Meta> {
730    if attr.path().is_ident(ident) {
731        attr.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
732            .iter()
733            .flat_map(|meta| meta.iter())
734            .cloned()
735            .collect()
736    } else {
737        Vec::default()
738    }
739}
740
741fn get_ident(ident: Option<&Ident>, attrs: &[syn::Attribute], rename_all: Option<&str>) -> Id {
742    let original = ident.map_or("???".to_string(), |id| id.to_string().replace("r#", ""));
743
744    let mut renamed = rename_all_to_case(original.clone(), rename_all);
745
746    if let Some(s) = serde_rename(attrs) {
747        renamed = s;
748    }
749
750    Id {
751        original: TypeName::new_string(original),
752        renamed: TypeName::new_string(renamed),
753    }
754}
755
756fn rename_all_to_case(original: String, case: Option<&str>) -> String {
757    // TODO: we'd like to replace this with `heck`, but it's not clear that
758    // we'd preserve backwards compatibility
759    match case {
760        None => original,
761        Some(value) => match value {
762            "lowercase" => original.to_lowercase(),
763            "UPPERCASE" => original.to_uppercase(),
764            "PascalCase" => original.to_pascal_case(),
765            "camelCase" => original.to_camel_case(),
766            "snake_case" => original.to_snake_case(),
767            "SCREAMING_SNAKE_CASE" => original.to_screaming_snake_case(),
768            "kebab-case" => original.to_kebab_case(),
769            "SCREAMING-KEBAB-CASE" => original.to_screaming_kebab_case(),
770            _ => original,
771        },
772    }
773}
774
775fn serde_rename(attrs: &[syn::Attribute]) -> Option<String> {
776    get_name_value_meta_items(attrs, "rename", SERDE).next()
777}
778
779/// Parses any comment out of the given slice of attributes
780fn parse_comment_attrs(attrs: &[Attribute]) -> Vec<String> {
781    attrs
782        .iter()
783        .map(|attr| attr.meta.clone())
784        .filter_map(|meta| match meta {
785            Meta::NameValue(name_value) if name_value.path.is_ident("doc") => {
786                expr_to_string(&name_value.value)
787            }
788            _ => None,
789        })
790        .collect()
791}
792
793// `#[typeshare(skip)]` or `#[serde(skip)]`
794fn is_skipped(attrs: &[syn::Attribute]) -> bool {
795    attrs.iter().any(|attr| {
796        get_meta_items(attr, SERDE)
797            .into_iter()
798            .chain(get_meta_items(attr, TYPESHARE))
799            .any(|arg| matches!(arg, Meta::Path(path) if path.is_ident("skip")))
800    })
801}
802
803fn serde_attr(attrs: &[syn::Attribute], ident: &str) -> bool {
804    attrs.iter().any(|attr| {
805        get_meta_items(attr, SERDE)
806            .iter()
807            .any(|arg| matches!(arg, Meta::Path(path) if path.is_ident(ident)))
808    })
809}
810
811fn serde_default(attrs: &[syn::Attribute]) -> bool {
812    serde_attr(attrs, "default")
813}
814
815fn serde_flatten(attrs: &[syn::Attribute]) -> bool {
816    serde_attr(attrs, "flatten")
817}
818
819/// Checks the struct or enum for decorators like `#[typeshare(typescript = "readonly")]`
820/// Takes a slice of `syn::Attribute`, returns a `HashMap<language, Vec<decorator>>`, where `language` is `SupportedLanguage`
821/// and `decorator` is `FieldDecorator`. Field decorators are ordered in a `BTreeSet` for consistent code generation.
822fn get_decorators(attrs: &[Attribute]) -> DecoratorSet {
823    attrs
824        .iter()
825        .flat_map(|attr| match attr.meta {
826            Meta::List(ref meta) => Some(meta),
827            Meta::Path(_) | Meta::NameValue(_) => None,
828        })
829        .filter(|meta| meta.path.is_ident(TYPESHARE))
830        .filter_map(|meta| meta.parse_args_with(KeyValueSeq::parse_terminated).ok())
831        .flatten()
832        .map(|pair| (pair.key, pair.value))
833        .collect()
834}
835
836/// Check if the thing tagged by these attributes (type, field, whatever) is
837/// accepted by at least one of the given valid OSes. This returns true for a
838/// given OS so long as it isn't explicitly rejected.
839pub fn check_target_os(attrs: &[Attribute], valid: &[&str]) -> bool {
840    attrs
841        .iter()
842        .filter_map(|attr| match attr.meta {
843            Meta::List(ref list) if list.path.is_ident("cfg") => Some(&list.tokens),
844            _ => None,
845        })
846        .filter_map(|cfg_tokens| target_os::Cfg::parse.parse2(cfg_tokens.clone()).ok())
847        .all(|cfg| target_os::target_os_good(&cfg, valid))
848}
849
850type KeyValueSeq = Punctuated<KeyMaybeValue, Token![,]>;
851
852fn expr_to_string(expr: &Expr) -> Option<String> {
853    match expr {
854        Expr::Lit(expr_lit) => literal_to_string(&expr_lit.lit),
855        _ => None,
856    }
857}
858
859fn literal_to_string(lit: &syn::Lit) -> Option<String> {
860    match lit {
861        syn::Lit::Str(str) => Some(str.value().trim().to_string()),
862        _ => None,
863    }
864}
865
866fn get_tag_key(attrs: &[syn::Attribute]) -> Option<String> {
867    get_name_value_meta_items(attrs, "tag", SERDE).next()
868}
869
870fn get_content_key(attrs: &[syn::Attribute]) -> Option<String> {
871    get_name_value_meta_items(attrs, "content", SERDE).next()
872}
873
874/// For parsing decorators: a single `key` or `key = "value"` in an attribute,
875/// where `key` is an identifier and `value` is some literal
876struct KeyMaybeValue {
877    key: String,
878    value: decorator::Value,
879}
880
881impl syn::parse::Parse for KeyMaybeValue {
882    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
883        // Use `parse_any` to allow parsing keyword identifiers like `type`
884        let key = input.call(Ident::parse_any)?;
885
886        // If this is `key = value,`, parse a literal
887        let value = if let Some(syn::token::Eq { .. }) = input.parse()? {
888            match input.parse()? {
889                syn::Lit::Str(lit) => decorator::Value::String(lit.value()),
890                syn::Lit::Int(lit) => decorator::Value::Int(lit.base10_parse()?),
891                syn::Lit::Bool(lit) => decorator::Value::Bool(lit.value),
892                lit => {
893                    return Err(syn::Error::new(
894                        lit.span(),
895                        "unsupported decorator type (need string, int, or bool)",
896                    ))
897                }
898            }
899        }
900        // If this is `key(...)`, parse a nested decorator set
901        else if let Some(group @ Group { .. }) = input.parse()? {
902            let Delimiter::Parenthesis = group.delimiter() else {
903                return Err(syn::Error::new(
904                    group.span(),
905                    "expected a parenthesized group",
906                ));
907            };
908
909            let pairs = KeyValueSeq::parse_terminated.parse2(group.stream())?;
910
911            decorator::Value::Nested(
912                pairs
913                    .into_iter()
914                    .map(|pair| (pair.key, pair.value))
915                    .collect(),
916            )
917        }
918        // If this is `key,`, the key is plain, no value attached
919        else {
920            decorator::Value::None
921        };
922
923        Ok(KeyMaybeValue {
924            key: key.to_string(),
925            value,
926        })
927    }
928}
929
930#[test]
931fn test_rename_all_to_case() {
932    let test_word = "test_case";
933
934    let tests = [
935        ("lowercase", "test_case"),
936        ("UPPERCASE", "TEST_CASE"),
937        ("PascalCase", "TestCase"),
938        ("camelCase", "testCase"),
939        ("snake_case", "test_case"),
940        ("SCREAMING_SNAKE_CASE", "TEST_CASE"),
941        ("kebab-case", "test-case"),
942        ("SCREAMING-KEBAB-CASE", "TEST-CASE"),
943        ("invalid case", "test_case"),
944    ];
945
946    for test in tests {
947        assert_eq!(
948            rename_all_to_case(test_word.to_string(), Some(test.0)),
949            test.1
950        );
951    }
952}
953
954#[cfg(test)]
955mod test_get_decorators {
956    use std::str::FromStr;
957
958    use cool_asserts::assert_matches;
959    use proc_macro2::TokenStream;
960    use syn::parse::Parser;
961    use typeshare_model::decorator::Value;
962
963    use super::*;
964
965    fn parse_attr(input: &str) -> Vec<Attribute> {
966        let tokens = TokenStream::from_str(input).expect("failed to create token stream");
967        let attr =
968            Parser::parse2(Attribute::parse_outer, tokens).expect("failed to parse attribute");
969
970        attr
971    }
972
973    #[test]
974    fn basic() {
975        let attr = parse_attr("#[typeshare(foo)]");
976        let decorators = get_decorators(&attr);
977
978        assert_eq!(decorators.get_all("foo"), &[Value::None]);
979        assert_eq!(decorators.get_all("baz"), &[])
980    }
981
982    #[test]
983    fn several() {
984        let attr = parse_attr("#[typeshare(foo, int=10, string=\"foo\")]");
985        let decorators = get_decorators(&attr);
986
987        assert_eq!(decorators.get_all("foo"), &[Value::None]);
988        assert_eq!(decorators.get_all("int"), &[Value::Int(10)]);
989        assert_eq!(
990            decorators.get_all("string"),
991            &[Value::String(String::from("foo"))]
992        );
993        assert_eq!(decorators.get_all("baz"), &[])
994    }
995
996    #[test]
997    fn multi_key() {
998        let attr = parse_attr("#[typeshare(thing=10, foo, thing=\"hello\")]");
999        let decorators = get_decorators(&attr);
1000
1001        assert_eq!(decorators.get_all("foo"), &[Value::None]);
1002        assert_eq!(
1003            decorators.get_all("thing"),
1004            &[Value::Int(10), Value::String(String::from("hello"))]
1005        )
1006    }
1007
1008    #[test]
1009    fn multiple_attributes() {
1010        let attr = parse_attr(
1011            "#[typeshare(foo, bar = \"baz\")]
1012             #[typeshare(baz = 42, qux)]",
1013        );
1014        let decorators = get_decorators(&attr);
1015
1016        assert_eq!(decorators.get_all("foo"), &[Value::None]);
1017        assert_eq!(
1018            decorators.get_all("bar"),
1019            &[Value::String(String::from("baz"))]
1020        );
1021        assert_eq!(decorators.get_all("baz"), &[Value::Int(42)]);
1022        assert_eq!(decorators.get_all("qux"), &[Value::None]);
1023    }
1024
1025    #[test]
1026    fn duplicate_keys_in_multiple_attributes() {
1027        let attr = parse_attr(
1028            "#[typeshare(foo = \"bar\", foo = 42)]
1029             #[typeshare(foo)]",
1030        );
1031        let decorators = get_decorators(&attr);
1032
1033        assert_eq!(
1034            decorators.get_all("foo"),
1035            &[
1036                Value::String(String::from("bar")),
1037                Value::Int(42),
1038                Value::None
1039            ]
1040        );
1041    }
1042
1043    // Regression test for an earlier breakage
1044    #[test]
1045    fn jvm_inline() {
1046        let attr = parse_attr(
1047            "#[typeshare(kotlin =\"JvmInline\", redacted)]
1048             #[derive(Serialize, Debug, Clone, PartialEq, Eq, Hash)]
1049             #[serde(rename_all = \"camelCase\")]",
1050        );
1051
1052        let decorators = get_decorators(&attr);
1053
1054        assert_eq!(decorators.get_all("redacted"), &[Value::None]);
1055        assert_eq!(
1056            decorators.get_all("kotlin"),
1057            &[Value::String(String::from("JvmInline"))]
1058        )
1059    }
1060
1061    #[test]
1062    fn nested() {
1063        let attr = parse_attr("#[typeshare(a, b(c=1, d=2, d=3))]");
1064
1065        let decorators = get_decorators(&attr);
1066
1067        assert_eq!(decorators.get_all("a"), &[Value::None]);
1068
1069        let (inner,) = assert_matches!(decorators.get_all("b"), [
1070            Value::Nested(inner) => inner,
1071        ]);
1072
1073        assert_eq!(inner.get_all("c"), &[Value::Int(1)]);
1074        assert_eq!(inner.get_all("d"), &[Value::Int(2), Value::Int(3)]);
1075    }
1076
1077    #[test]
1078    fn type_override() {
1079        let attr = parse_attr(
1080            "#[typeshare(typescript(type = \"string\"))]
1081             #[typeshare(swift = \"Foo\", swift(type=\"NSString\"))]",
1082        );
1083
1084        let decorators = get_decorators(&attr);
1085
1086        eprintln!("{decorators:#?}");
1087
1088        assert_eq!(
1089            decorators.type_override_for_lang("swift").unwrap(),
1090            "NSString"
1091        );
1092        assert_eq!(
1093            decorators.type_override_for_lang("typescript").unwrap(),
1094            "string"
1095        );
1096        assert_eq!(decorators.type_override_for_lang("kotlin"), None);
1097    }
1098}