From 1936fb6a0732cc5955c99943606e975d2aad3086 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Fri, 24 Oct 2025 09:35:26 -0400 Subject: [PATCH 1/9] Update parser.rs --- .../tests/cfg_if_attribute_typeshare/input.rs | 33 ++++ .../cfg_if_attribute_typeshare/output.kt | 30 ++++ .../cfg_if_attribute_typeshare/output.swift | 34 ++++ .../cfg_if_attribute_typeshare/output.ts | 22 +++ core/src/parser.rs | 160 +++++++++++++++--- core/tests/snapshot_tests.rs | 1 + 6 files changed, 258 insertions(+), 22 deletions(-) create mode 100644 core/data/tests/cfg_if_attribute_typeshare/input.rs create mode 100644 core/data/tests/cfg_if_attribute_typeshare/output.kt create mode 100644 core/data/tests/cfg_if_attribute_typeshare/output.swift create mode 100644 core/data/tests/cfg_if_attribute_typeshare/output.ts 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..2412104c --- /dev/null +++ b/core/data/tests/cfg_if_attribute_typeshare/input.rs @@ -0,0 +1,33 @@ +/// 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(redacted))] +pub struct TestStruct3 { + field_1: String, +} 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..7d9acbb6 --- /dev/null +++ b/core/data/tests/cfg_if_attribute_typeshare/output.kt @@ -0,0 +1,30 @@ +package com.agilebits.onepassword + +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerialName + +typealias Bytes = List + +/// 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 TestStruct3 ( + val field_1: String +) { + override fun toString(): String = "TestStruct3" +} + 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..b922260c --- /dev/null +++ b/core/data/tests/cfg_if_attribute_typeshare/output.swift @@ -0,0 +1,34 @@ +import Foundation + +public typealias Bytes = [UInt8] + +/// 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 TestStruct3: Codable { + public let field_1: String + + public init(field_1: String) { + self.field_1 = field_1 + } +} 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..f64640ec --- /dev/null +++ b/core/data/tests/cfg_if_attribute_typeshare/output.ts @@ -0,0 +1,22 @@ +export type Bytes = number[]; + +/** + * 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 TestStruct3 { + field_1: string; +} + diff --git a/core/src/parser.rs b/core/src/parser.rs index 6cb9868b..322ee0d1 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -151,7 +151,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 +560,21 @@ 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 { + 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() - .flat_map(|attr| attr.path().segments.clone()) - .any(|segment| segment.ident == TYPESHARE) + .any(|attr| attr.path().is_ident(TYPESHARE) || check_cfg_attr(attr)) } pub(crate) fn serde_rename_all(attrs: &[syn::Attribute]) -> Option { @@ -681,9 +690,21 @@ fn is_skipped(attrs: &[syn::Attribute], target_os: &[String]) -> bool { // `#[typeshare(redacted)]` fn is_redacted(attrs: &[syn::Attribute]) -> bool { + let check_cfg_attr = |attr| { + 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(|parsed| parsed.iter().any(|ident| ident == "redacted")) + .unwrap_or(false), + + _ => false, + }) + }; + attrs.iter().any(|attr| { get_meta_items(attr, TYPESHARE) .any(|arg| matches!(arg, Meta::Path(path) if path.is_ident("redacted"))) + || check_cfg_attr(attr) }) } @@ -814,6 +835,12 @@ fn literal_to_string(lit: &syn::Lit) -> Option { fn get_decorators(attrs: &[syn::Attribute]) -> DecoratorMap { let mut decorator_map: DecoratorMap = DecoratorMap::new(); + let decorator_kinds = [ + DecoratorKind::Swift, + DecoratorKind::SwiftGenericConstraints, + DecoratorKind::Kotlin, + ]; + for decorator_kind in [ DecoratorKind::Swift, DecoratorKind::SwiftGenericConstraints, @@ -827,6 +854,31 @@ fn get_decorators(attrs: &[syn::Attribute]) -> DecoratorMap { } } + // Check cfg_attr. + let nvps = attrs + .iter() + .flat_map(|attr| get_meta_items(attr, "cfg_attr")) + .flat_map(|meta| match meta { + Meta::List(nvp) if nvp.path.is_ident(TYPESHARE) => nvp + .parse_args_with(Punctuated::::parse_terminated) + .ok(), + _ => None, + }) + .flat_map(|nvps| nvps.into_iter()); + + for nvp in nvps { + for kind in decorator_kinds { + if nvp.path.is_ident(kind.as_str()) { + if let Some(val) = expr_to_string(&nvp.value) { + decorator_map + .entry(kind) + .or_default() + .extend(val.split(',').map(|s| s.trim().to_string())); + } + } + } + } + decorator_map } @@ -844,26 +896,90 @@ 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, rename_all_to_case, DecoratorKind, + }; + use std::collections::BTreeSet; + use syn::Attribute; + + #[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]; - for test in tests { + 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"); + + assert_eq!( + swift_decorators, + &BTreeSet::from_iter(["Equatable".into(), "Hashable".into()]) + ); assert_eq!( - rename_all_to_case(test_word.to_string(), &Some(test.0.to_string())), - test.1 + 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)); + } } 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]; } From bc1968f0ba59d95645fdf5131d1e3f8b80bab38e Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Fri, 24 Oct 2025 11:04:08 -0400 Subject: [PATCH 2/9] update redacted parsing --- core/src/parser.rs | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/core/src/parser.rs b/core/src/parser.rs index 322ee0d1..349b0f35 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -693,8 +693,12 @@ fn is_redacted(attrs: &[syn::Attribute]) -> bool { let check_cfg_attr = |attr| { 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(|parsed| parsed.iter().any(|ident| ident == "redacted")) + .parse_args_with(Punctuated::::parse_terminated) + .map(|metas| { + metas + .into_iter() + .any(|meta| matches!(meta, Meta::Path(path) if path.is_ident("redacted"))) + }) .unwrap_or(false), _ => false, @@ -898,11 +902,15 @@ pub(crate) fn remove_dash_from_identifier(name: &str) -> String { #[cfg(test)] mod test { - use crate::parser::{ - get_decorators, has_typeshare_annotation, is_redacted, rename_all_to_case, DecoratorKind, + 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; + use syn::{Attribute, ItemStruct}; #[test] fn test_rename_all_to_case() { @@ -982,4 +990,19 @@ mod test { 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); + } } From 01cba75b7c4b4f5a51e1200944567685f64c7116 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Fri, 24 Oct 2025 11:15:03 -0400 Subject: [PATCH 3/9] cleanup --- core/src/parser.rs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/core/src/parser.rs b/core/src/parser.rs index 349b0f35..2067eacb 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -41,7 +41,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", @@ -845,17 +845,14 @@ fn get_decorators(attrs: &[syn::Attribute]) -> DecoratorMap { DecoratorKind::Kotlin, ]; - for decorator_kind in [ - 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())); } // Check cfg_attr. From 55fba14cc8c315e0ad54ec8cf81b58bb32e590da Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Fri, 24 Oct 2025 12:46:25 -0400 Subject: [PATCH 4/9] update decorator parsing --- .../tests/cfg_if_attribute_typeshare/input.rs | 9 +++-- .../cfg_if_attribute_typeshare/output.kt | 17 ++++---- .../cfg_if_attribute_typeshare/output.swift | 10 +---- .../cfg_if_attribute_typeshare/output.ts | 6 +-- core/src/parser.rs | 39 +++++++++++++++---- 5 files changed, 50 insertions(+), 31 deletions(-) diff --git a/core/data/tests/cfg_if_attribute_typeshare/input.rs b/core/data/tests/cfg_if_attribute_typeshare/input.rs index 2412104c..7fbe191e 100644 --- a/core/data/tests/cfg_if_attribute_typeshare/input.rs +++ b/core/data/tests/cfg_if_attribute_typeshare/input.rs @@ -27,7 +27,8 @@ pub struct TestStruct2 { field_2: R, } -#[cfg_attr(feature = "typeshare-support", typeshare(redacted))] -pub struct TestStruct3 { - field_1: String, -} +#[cfg_attr( + feature = "typeshare-support", + typeshare(kotlin = "JvmInline", redacted) +)] +pub struct TestStruct3(String); diff --git a/core/data/tests/cfg_if_attribute_typeshare/output.kt b/core/data/tests/cfg_if_attribute_typeshare/output.kt index 7d9acbb6..7d49bfd1 100644 --- a/core/data/tests/cfg_if_attribute_typeshare/output.kt +++ b/core/data/tests/cfg_if_attribute_typeshare/output.kt @@ -5,6 +5,16 @@ 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 @@ -21,10 +31,3 @@ data class TestStruct2 ( val field2: R ) -@Serializable -data class TestStruct3 ( - val field_1: String -) { - override fun toString(): String = "TestStruct3" -} - diff --git a/core/data/tests/cfg_if_attribute_typeshare/output.swift b/core/data/tests/cfg_if_attribute_typeshare/output.swift index b922260c..6d007799 100644 --- a/core/data/tests/cfg_if_attribute_typeshare/output.swift +++ b/core/data/tests/cfg_if_attribute_typeshare/output.swift @@ -2,6 +2,8 @@ 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 @@ -24,11 +26,3 @@ public struct TestStruct2: Codable, Equatable self.field2 = field2 } } - -public struct TestStruct3: Codable { - public let field_1: String - - public init(field_1: String) { - self.field_1 = field_1 - } -} diff --git a/core/data/tests/cfg_if_attribute_typeshare/output.ts b/core/data/tests/cfg_if_attribute_typeshare/output.ts index f64640ec..10e1303d 100644 --- a/core/data/tests/cfg_if_attribute_typeshare/output.ts +++ b/core/data/tests/cfg_if_attribute_typeshare/output.ts @@ -1,5 +1,7 @@ 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 @@ -16,7 +18,3 @@ export interface TestStruct2 { field2: R; } -export interface TestStruct3 { - field_1: string; -} - diff --git a/core/src/parser.rs b/core/src/parser.rs index 2067eacb..38f35863 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -861,21 +861,26 @@ fn get_decorators(attrs: &[syn::Attribute]) -> DecoratorMap { .flat_map(|attr| get_meta_items(attr, "cfg_attr")) .flat_map(|meta| match meta { Meta::List(nvp) if nvp.path.is_ident(TYPESHARE) => nvp - .parse_args_with(Punctuated::::parse_terminated) + .parse_args_with(Punctuated::::parse_terminated) .ok(), _ => None, }) .flat_map(|nvps| nvps.into_iter()); - for nvp in nvps { + for meta in nvps { for kind in decorator_kinds { - if nvp.path.is_ident(kind.as_str()) { - if let Some(val) = expr_to_string(&nvp.value) { - decorator_map - .entry(kind) - .or_default() - .extend(val.split(',').map(|s| s.trim().to_string())); + match &meta { + Meta::NameValue(meta_name_value) + if meta_name_value.path.is_ident(kind.as_str()) => + { + if let Some(val) = expr_to_string(&meta_name_value.value) { + decorator_map + .entry(kind) + .or_default() + .extend(val.split(',').map(|s| s.trim().to_string())); + } } + _ => {} } } } @@ -1002,4 +1007,22 @@ mod test { }; 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()])); + } } From 1b622c5fb8862fc5d135dabd6cf7a42df19bbc64 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Fri, 24 Oct 2025 13:05:43 -0400 Subject: [PATCH 5/9] Update parser.rs --- core/src/parser.rs | 76 ++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 40 deletions(-) diff --git a/core/src/parser.rs b/core/src/parser.rs index 38f35863..dc1dc2fe 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::{ @@ -589,7 +588,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, @@ -602,22 +601,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( @@ -855,36 +881,6 @@ fn get_decorators(attrs: &[syn::Attribute]) -> DecoratorMap { .extend(value.split(',').map(|s| s.trim().to_string())); } - // Check cfg_attr. - let nvps = attrs - .iter() - .flat_map(|attr| get_meta_items(attr, "cfg_attr")) - .flat_map(|meta| match meta { - Meta::List(nvp) if nvp.path.is_ident(TYPESHARE) => nvp - .parse_args_with(Punctuated::::parse_terminated) - .ok(), - _ => None, - }) - .flat_map(|nvps| nvps.into_iter()); - - for meta in nvps { - for kind in decorator_kinds { - match &meta { - Meta::NameValue(meta_name_value) - if meta_name_value.path.is_ident(kind.as_str()) => - { - if let Some(val) = expr_to_string(&meta_name_value.value) { - decorator_map - .entry(kind) - .or_default() - .extend(val.split(',').map(|s| s.trim().to_string())); - } - } - _ => {} - } - } - } - decorator_map } From 5335f8cbe1d8f0720fb3f7f914025090cfc7fcce Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Fri, 24 Oct 2025 13:32:11 -0400 Subject: [PATCH 6/9] refactor --- core/src/parser.rs | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/core/src/parser.rs b/core/src/parser.rs index dc1dc2fe..6f7e08c4 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -714,27 +714,29 @@ fn is_skipped(attrs: &[syn::Attribute], target_os: &[String]) -> bool { typeshare_skip || !accept_target_os(attrs, target_os) } -// `#[typeshare(redacted)]` -fn is_redacted(attrs: &[syn::Attribute]) -> bool { - let check_cfg_attr = |attr| { - 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("redacted"))) - }) - .unwrap_or(false), +/// 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, - }) - }; + _ => 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"))) - || check_cfg_attr(attr) + || has_typeshare_ident_within_cfg_attr(attr, "redacted") }) } From f17d94b6b1d425a57fbe18988729e372d5d1c93e Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Tue, 28 Oct 2025 11:21:26 -0400 Subject: [PATCH 7/9] update for field level cfg_attr(, typeshare) attributes. --- annotation/src/lib.rs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) 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) { From 1a706caac2caa94dee492f3f8cba584e94c57f0f Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Tue, 28 Oct 2025 11:28:07 -0400 Subject: [PATCH 8/9] update snapshot tests for cfg_attr --- core/data/tests/cfg_if_attribute_typeshare/input.rs | 6 ++++++ core/data/tests/cfg_if_attribute_typeshare/output.kt | 5 +++++ core/data/tests/cfg_if_attribute_typeshare/output.swift | 8 ++++++++ core/data/tests/cfg_if_attribute_typeshare/output.ts | 4 ++++ 4 files changed, 23 insertions(+) diff --git a/core/data/tests/cfg_if_attribute_typeshare/input.rs b/core/data/tests/cfg_if_attribute_typeshare/input.rs index 7fbe191e..71208b1a 100644 --- a/core/data/tests/cfg_if_attribute_typeshare/input.rs +++ b/core/data/tests/cfg_if_attribute_typeshare/input.rs @@ -32,3 +32,9 @@ pub struct TestStruct2 { 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 index 7d49bfd1..be62ab47 100644 --- a/core/data/tests/cfg_if_attribute_typeshare/output.kt +++ b/core/data/tests/cfg_if_attribute_typeshare/output.kt @@ -31,3 +31,8 @@ data class TestStruct2 ( 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 index 6d007799..10bd5e98 100644 --- a/core/data/tests/cfg_if_attribute_typeshare/output.swift +++ b/core/data/tests/cfg_if_attribute_typeshare/output.swift @@ -26,3 +26,11 @@ public struct TestStruct2: Codable, Equatable 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 index 10e1303d..fb2c6aed 100644 --- a/core/data/tests/cfg_if_attribute_typeshare/output.ts +++ b/core/data/tests/cfg_if_attribute_typeshare/output.ts @@ -18,3 +18,7 @@ export interface TestStruct2 { field2: R; } +export interface TestStruct4 { + field: number; +} + From c762c72a61dd3b4e279c656f695ab617c6833480 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Mon, 3 Nov 2025 12:06:48 -0500 Subject: [PATCH 9/9] update for fully qualified typeshare annotation. add test. --- core/src/parser.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/core/src/parser.rs b/core/src/parser.rs index 6f7e08c4..a87f1414 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -571,9 +571,13 @@ pub(crate) fn has_typeshare_annotation(attrs: &[syn::Attribute]) -> bool { Meta::NameValue(_meta_name_value) => false, }) }; - attrs - .iter() - .any(|attr| attr.path().is_ident(TYPESHARE) || check_cfg_attr(attr)) + 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 { @@ -1023,4 +1027,16 @@ mod test { .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)); + } }