diff --git a/annotation/src/lib.rs b/annotation/src/lib.rs index 71d9a0d5..79887a4a 100644 --- a/annotation/src/lib.rs +++ b/annotation/src/lib.rs @@ -3,7 +3,7 @@ extern crate proc_macro; use proc_macro::TokenStream; use quote::ToTokens; -use syn::{parse, Attribute, Data, DeriveInput, Fields}; +use syn::{parse, punctuated::Punctuated, Attribute, Data, DeriveInput, Fields, Meta, Token}; /// Marks a type as a type shared across the FFI boundary using typeshare. /// @@ -50,11 +50,27 @@ pub fn typeshare(_attr: TokenStream, item: TokenStream) -> TokenStream { } } +const CONFIG_ATTRIBUTE_NAME: &str = "typeshare"; + +fn is_typeshare_attribute(attribute: &Attribute) -> bool { + let has_cfg_attr = || { + if attribute.path().is_ident("cfg_attr") { + if let Ok(meta) = + attribute.parse_args_with(Punctuated::::parse_terminated) + { + return meta.into_iter().any( + |meta| matches!(meta, Meta::List(meta_list) if meta_list.path.is_ident(CONFIG_ATTRIBUTE_NAME)), + ); + } + } + false + }; + attribute.path().is_ident(CONFIG_ATTRIBUTE_NAME) || has_cfg_attr() +} + fn strip_configuration_attribute(item: &mut DeriveInput) { fn remove_configuration_from_attributes(attributes: &mut Vec) { - const CONFIG_ATTRIBUTE_NAME: &str = "typeshare"; - - attributes.retain(|x| x.path().to_token_stream().to_string() != CONFIG_ATTRIBUTE_NAME); + attributes.retain(|attribute| !is_typeshare_attribute(attribute)); } fn remove_configuration_from_fields(fields: &mut Fields) { diff --git a/core/data/tests/cfg_if_attribute_typeshare/input.rs b/core/data/tests/cfg_if_attribute_typeshare/input.rs new file mode 100644 index 00000000..71208b1a --- /dev/null +++ b/core/data/tests/cfg_if_attribute_typeshare/input.rs @@ -0,0 +1,40 @@ +/// Example of a type that is conditionally typeshared +/// based on a feature "typeshare-support". This does not +/// conditionally typeshare but allows a conditionally +/// typeshared type to generate typeshare types when behind +/// a `cfg_attr` condition. +#[cfg_attr(feature = "typeshare-support", typeshare)] +pub struct TestStruct1 { + field: String, +} + +#[cfg_attr(feature = "typeshare-support", typeshare(transparent))] +#[derive(Debug, Default, PartialEq, Eq, Clone, Hash)] +#[repr(transparent)] +pub struct Bytes(Vec); + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[cfg_attr( + feature = "typeshare-support", + typeshare( + swift = "Equatable, Hashable", + swiftGenericConstraints = "R: Equatable & Hashable" + ) +)] +pub struct TestStruct2 { + field_1: String, + field_2: R, +} + +#[cfg_attr( + feature = "typeshare-support", + typeshare(kotlin = "JvmInline", redacted) +)] +pub struct TestStruct3(String); + +#[cfg_attr(feature = "typeshare-support", typeshare)] +pub struct TestStruct4 { + #[cfg_attr(feature = "typeshare-support", typeshare(serialized_as = "I54"))] + pub field: i64, +} diff --git a/core/data/tests/cfg_if_attribute_typeshare/output.kt b/core/data/tests/cfg_if_attribute_typeshare/output.kt new file mode 100644 index 00000000..be62ab47 --- /dev/null +++ b/core/data/tests/cfg_if_attribute_typeshare/output.kt @@ -0,0 +1,38 @@ +package com.agilebits.onepassword + +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerialName + +typealias Bytes = List + +@Serializable +@JvmInline +value class TestStruct3( + private val value: String +) { + fun unwrap() = value + + override fun toString(): String = "***" +} + +/// Example of a type that is conditionally typeshared +/// based on a feature "typeshare-support". This does not +/// conditionally typeshare but allows a conditionally +/// typeshared type to generate typeshare types when behind +/// a `cfg_attr` condition. +@Serializable +data class TestStruct1 ( + val field: String +) + +@Serializable +data class TestStruct2 ( + val field1: String, + val field2: R +) + +@Serializable +data class TestStruct4 ( + val field: Long +) + diff --git a/core/data/tests/cfg_if_attribute_typeshare/output.swift b/core/data/tests/cfg_if_attribute_typeshare/output.swift new file mode 100644 index 00000000..10bd5e98 --- /dev/null +++ b/core/data/tests/cfg_if_attribute_typeshare/output.swift @@ -0,0 +1,36 @@ +import Foundation + +public typealias Bytes = [UInt8] + +public typealias TestStruct3 = String + +/// Example of a type that is conditionally typeshared +/// based on a feature "typeshare-support". This does not +/// conditionally typeshare but allows a conditionally +/// typeshared type to generate typeshare types when behind +/// a `cfg_attr` condition. +public struct TestStruct1: Codable { + public let field: String + + public init(field: String) { + self.field = field + } +} + +public struct TestStruct2: Codable, Equatable, Hashable { + public let field1: String + public let field2: R + + public init(field1: String, field2: R) { + self.field1 = field1 + self.field2 = field2 + } +} + +public struct TestStruct4: Codable { + public let field: Int64 + + public init(field: Int64) { + self.field = field + } +} diff --git a/core/data/tests/cfg_if_attribute_typeshare/output.ts b/core/data/tests/cfg_if_attribute_typeshare/output.ts new file mode 100644 index 00000000..fb2c6aed --- /dev/null +++ b/core/data/tests/cfg_if_attribute_typeshare/output.ts @@ -0,0 +1,24 @@ +export type Bytes = number[]; + +export type TestStruct3 = string; + +/** + * Example of a type that is conditionally typeshared + * based on a feature "typeshare-support". This does not + * conditionally typeshare but allows a conditionally + * typeshared type to generate typeshare types when behind + * a `cfg_attr` condition. + */ +export interface TestStruct1 { + field: string; +} + +export interface TestStruct2 { + field1: string; + field2: R; +} + +export interface TestStruct4 { + field: number; +} + diff --git a/core/src/parser.rs b/core/src/parser.rs index 6cb9868b..a87f1414 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -11,7 +11,6 @@ use crate::{ target_os_check::accept_target_os, visitors::{ImportedType, TypeShareVisitor}, }; -use itertools::Either; use log::debug; use proc_macro2::Ident; use std::{ @@ -41,7 +40,7 @@ pub enum DecoratorKind { impl DecoratorKind { /// This decorator as a str. - fn as_str(&self) -> &str { + fn as_str(&self) -> &'static str { match self { DecoratorKind::Swift => "swift", DecoratorKind::SwiftGenericConstraints => "swiftGenericConstraints", @@ -151,7 +150,7 @@ pub fn parse( ) -> Result, ParseErrorWithSpan> { // We will only produce output for files that contain the `#[typeshare]` // attribute, so this is a quick and easy performance win - if !parse_file_context.source_code.contains("#[typeshare") { + if !parse_file_context.source_code.contains("typeshare") { return Ok(None); } @@ -560,12 +559,25 @@ fn parse_const_expr(e: &Expr) -> Result { // Helpers -/// Checks the given attrs for `#[typeshare]` +/// Checks the given attrs for `#[typeshare]` or within `#[cfg_attr(, typeshare)]` pub(crate) fn has_typeshare_annotation(attrs: &[syn::Attribute]) -> bool { - attrs - .iter() - .flat_map(|attr| attr.path().segments.clone()) - .any(|segment| segment.ident == TYPESHARE) + let check_cfg_attr = |attr| { + get_meta_items(attr, "cfg_attr").any(|item| match item { + Meta::Path(path) => path + .segments + .iter() + .any(|segment| segment.ident == TYPESHARE), + Meta::List(meta_list) => meta_list.path.is_ident(TYPESHARE), + Meta::NameValue(_meta_name_value) => false, + }) + }; + attrs.iter().any(|attr| { + attr.path() + .segments + .iter() + .any(|segment| segment.ident == TYPESHARE) + || check_cfg_attr(attr) + }) } pub(crate) fn serde_rename_all(attrs: &[syn::Attribute]) -> Option { @@ -580,7 +592,7 @@ pub(crate) fn get_field_type_override(attrs: &[syn::Attribute]) -> Option( +fn get_name_value_meta_items<'a>( attrs: &'a [syn::Attribute], name: &'a str, ident: &'static str, @@ -593,22 +605,49 @@ pub(crate) fn get_name_value_meta_items<'a>( } _ => None, }) - .collect::>() + .chain( + // If we are searching for typeshare attributes then we'll look into cfg_att as well. + (ident == TYPESHARE) + .then(|| { + attrs.iter().flat_map(move |attr| { + get_meta_items(attr, "cfg_attr") + .filter_map(|meta| match meta { + Meta::List(list) if list.path.is_ident(TYPESHARE) => list + .parse_args_with( + Punctuated::::parse_terminated, + ) + .ok(), + _ => None, + }) + .flatten() + .filter_map(|meta| match &meta { + Meta::NameValue(meta_name_value) + if meta_name_value.path.is_ident(name) => + { + expr_to_string(&meta_name_value.value) + } + _ => None, + }) + }) + }) + .into_iter() + .flatten(), + ) }) } /// Returns all arguments passed into `#[{ident}(...)]` where `{ident}` can be `serde` or `typeshare` attributes #[inline(always)] pub(crate) fn get_meta_items(attr: &syn::Attribute, ident: &str) -> impl Iterator { - if attr.path().is_ident(ident) { - Either::Left( + attr.path() + .is_ident(ident) + .then(|| { attr.parse_args_with(Punctuated::::parse_terminated) .into_iter() - .flat_map(|punctuated| punctuated.into_iter()), - ) - } else { - Either::Right(std::iter::empty()) - } + .flat_map(|punctuated| punctuated.into_iter()) + }) + .into_iter() + .flatten() } fn get_ident( @@ -679,11 +718,29 @@ fn is_skipped(attrs: &[syn::Attribute], target_os: &[String]) -> bool { typeshare_skip || !accept_target_os(attrs, target_os) } +/// Find the ident within a typeshare meta list within a cfg_attr. For +/// example, find "redacted" for typeshare as `#[cfg_attr(feature = "something", typeshare(redacted))]` +fn has_typeshare_ident_within_cfg_attr(attr: &Attribute, ident: &str) -> bool { + get_meta_items(attr, "cfg_attr").any(|item| match item { + Meta::List(meta_list) if meta_list.path.is_ident(TYPESHARE) => meta_list + .parse_args_with(Punctuated::::parse_terminated) + .map(|metas| { + metas + .into_iter() + .any(|meta| matches!(meta, Meta::Path(path) if path.is_ident(ident))) + }) + .unwrap_or(false), + + _ => false, + }) +} + // `#[typeshare(redacted)]` fn is_redacted(attrs: &[syn::Attribute]) -> bool { attrs.iter().any(|attr| { get_meta_items(attr, TYPESHARE) .any(|arg| matches!(arg, Meta::Path(path) if path.is_ident("redacted"))) + || has_typeshare_ident_within_cfg_attr(attr, "redacted") }) } @@ -814,17 +871,20 @@ fn literal_to_string(lit: &syn::Lit) -> Option { fn get_decorators(attrs: &[syn::Attribute]) -> DecoratorMap { let mut decorator_map: DecoratorMap = DecoratorMap::new(); - for decorator_kind in [ + let decorator_kinds = [ DecoratorKind::Swift, DecoratorKind::SwiftGenericConstraints, DecoratorKind::Kotlin, - ] { - for value in get_name_value_meta_items(attrs, decorator_kind.as_str(), TYPESHARE) { - decorator_map - .entry(decorator_kind) - .or_default() - .extend(value.split(',').map(|s| s.trim().to_string())); - } + ]; + + for (decorator_kind, value) in decorator_kinds.into_iter().flat_map(|decorator_kind| { + get_name_value_meta_items(attrs, decorator_kind.as_str(), TYPESHARE) + .map(move |value| (decorator_kind, value)) + }) { + decorator_map + .entry(decorator_kind) + .or_default() + .extend(value.split(',').map(|s| s.trim().to_string())); } decorator_map @@ -844,26 +904,139 @@ pub(crate) fn remove_dash_from_identifier(name: &str) -> String { name.replace('-', "_") } -#[test] -fn test_rename_all_to_case() { - let test_word = "test_case"; - - let tests = [ - ("lowercase", "test_case"), - ("UPPERCASE", "TEST_CASE"), - ("PascalCase", "TestCase"), - ("camelCase", "testCase"), - ("snake_case", "test_case"), - ("SCREAMING_SNAKE_CASE", "TEST_CASE"), - ("kebab-case", "test-case"), - ("SCREAMING-KEBAB-CASE", "TEST-CASE"), - ("invalid case", "test_case"), - ]; +#[cfg(test)] +mod test { + use crate::{ + parser::{ + get_decorators, has_typeshare_annotation, is_redacted, parse_struct, + rename_all_to_case, DecoratorKind, + }, + rust_types::RustItem, + }; + use std::collections::BTreeSet; + use syn::{Attribute, ItemStruct}; + + #[test] + fn test_rename_all_to_case() { + let test_word = "test_case"; + + let tests = [ + ("lowercase", "test_case"), + ("UPPERCASE", "TEST_CASE"), + ("PascalCase", "TestCase"), + ("camelCase", "testCase"), + ("snake_case", "test_case"), + ("SCREAMING_SNAKE_CASE", "TEST_CASE"), + ("kebab-case", "test-case"), + ("SCREAMING-KEBAB-CASE", "TEST-CASE"), + ("invalid case", "test_case"), + ]; + + for test in tests { + assert_eq!( + rename_all_to_case(test_word.to_string(), &Some(test.0.to_string())), + test.1 + ); + } + } + + #[test] + fn test_cfg_attr() { + let attr: Attribute = syn::parse_quote! { + #[cfg_attr(feature = "typeshare-support", typeshare)] + }; + assert!(has_typeshare_annotation(&[attr])); + } + + #[test] + fn test_cfg_attr_with_nvps() { + let attr: Attribute = syn::parse_quote! { + #[cfg_attr( + feature = "typeshare-support", + typeshare( + swift = "Equatable, Hashable", + swiftGenericConstraints = "R: Equatable & Hashable" + ) + )] + }; + + let attrs = [attr]; + + assert!(has_typeshare_annotation(&attrs)); + + let decorators = get_decorators(&attrs); + + let swift_decorators = decorators + .get(&DecoratorKind::Swift) + .expect("No swift decorators"); + let swift_constraints = decorators + .get(&DecoratorKind::SwiftGenericConstraints) + .expect("No swift generic constraints"); - for test in tests { assert_eq!( - rename_all_to_case(test_word.to_string(), &Some(test.0.to_string())), - test.1 + swift_decorators, + &BTreeSet::from_iter(["Equatable".into(), "Hashable".into()]) ); + assert_eq!( + swift_constraints, + &BTreeSet::from_iter(["R: Equatable & Hashable".into()]) + ); + } + + #[test] + fn test_cfg_attr_redacted() { + let attr: Attribute = syn::parse_quote! { + #[cfg_attr(feature = "typeshare-support", typeshare(redacted))] + }; + + let attrs = [attr]; + + assert!(has_typeshare_annotation(&attrs)); + assert!(is_redacted(&attrs)); + } + + #[test] + fn test_item_struct_redacted_list() { + let item_struct: ItemStruct = syn::parse_quote! { + #[cfg_attr(feature = "typeshare-support", typeshare(redacted, kotlin = "JvmInline"))] + pub struct Secret(String); + }; + + let RustItem::Alias(rust_struct) = + parse_struct(&item_struct, &[]).expect("Failed to parse struct") + else { + panic!("Not a struct"); + }; + assert!(rust_struct.is_redacted); + } + + #[test] + fn test_kotlin_decorators() { + let attr: Attribute = syn::parse_quote! { + #[cfg_attr( + feature = "typeshare-support", + typeshare(kotlin = "JvmInline", redacted) + )] + }; + + let attrs = [attr]; + assert!(has_typeshare_annotation(&attrs)); + let decorators = get_decorators(&attrs); + let kotlin_decorator = decorators + .get(&DecoratorKind::Kotlin) + .expect("No kotlin decorator"); + assert_eq!(kotlin_decorator, &BTreeSet::from_iter(["JvmInline".into()])); + } + + #[test] + fn test_typeshare_with_fully_qualified() { + let item_struct: ItemStruct = syn::parse_quote! { + #[typeshare::typeshare] + pub struct Test { + field_1: i64 + } + }; + + assert!(has_typeshare_annotation(&item_struct.attrs)); } } diff --git a/core/tests/snapshot_tests.rs b/core/tests/snapshot_tests.rs index 521cd664..042684c7 100644 --- a/core/tests/snapshot_tests.rs +++ b/core/tests/snapshot_tests.rs @@ -715,4 +715,5 @@ tests! { } ]; no_mangle: [swift, kotlin, scala, typescript, go]; + cfg_if_attribute_typeshare: [swift, kotlin, typescript]; }