From e27fbec3c12717da7174234a64256d7c180e0718 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Thu, 23 Oct 2025 17:46:54 -0400 Subject: [PATCH 01/14] Allow typeshare annotation using cfg_attr --- Cargo.lock | 304 +++++++++++++++++- .../test_cfg_attr_typeshare/input.rs | 4 + .../test_cfg_attr_typeshare/output.ts | 4 + app/engine/Cargo.toml | 19 +- app/engine/src/driver.rs | 6 + app/engine/src/parser.rs | 22 +- 6 files changed, 345 insertions(+), 14 deletions(-) create mode 100644 app/cli/snapshot-tests/test_cfg_attr_typeshare/input.rs create mode 100644 app/cli/snapshot-tests/test_cfg_attr_typeshare/output.ts diff --git a/Cargo.lock b/Cargo.lock index e3c0e6f4..04b7d450 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.18" @@ -67,6 +76,12 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "bitflags" version = "2.9.0" @@ -83,6 +98,39 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "cc" +version = "1.2.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + [[package]] name = "clap" version = "4.5.35" @@ -150,6 +198,12 @@ dependencies = [ "indent_write", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -197,6 +251,32 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "flexi_logger" +version = "0.28.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cca927478b3747ba47f98af6ba0ac0daea4f12d12f55e9104071b3dc00276310" +dependencies = [ + "chrono", + "glob", + "log", + "nu-ansi-term", + "regex", + "thiserror 1.0.69", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "globset" version = "0.4.15" @@ -222,6 +302,30 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ignore" version = "0.4.23" @@ -281,6 +385,16 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57d8bde02bbf44a562cf068a8ff4a68842df387e302a03a4de4a57fcf82ec377" +[[package]] +name = "js-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "lazy_format" version = "2.0.3" @@ -311,6 +425,24 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.20.2" @@ -355,6 +487,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.8" @@ -385,6 +529,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" version = "1.0.18" @@ -441,6 +591,12 @@ dependencies = [ "serde", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "similar" version = "2.7.0" @@ -477,13 +633,33 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -564,16 +740,18 @@ dependencies = [ "clap", "clap_complete", "cool_asserts", + "flexi_logger", "ignore", "indent_write", "itertools", "lazy_format", + "log", "proc-macro2", "quote", "rayon", "serde", "syn", - "thiserror", + "thiserror 2.0.12", "toml", "typeshare-model", ] @@ -588,7 +766,7 @@ dependencies = [ "joinery", "lazy_format", "serde", - "thiserror", + "thiserror 2.0.12", "typeshare-model", ] @@ -635,7 +813,7 @@ dependencies = [ "itertools", "joinery", "serde", - "thiserror", + "thiserror 2.0.12", "typeshare-model", ] @@ -683,6 +861,65 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] + [[package]] name = "winapi-util" version = "0.1.9" @@ -692,6 +929,65 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/app/cli/snapshot-tests/test_cfg_attr_typeshare/input.rs b/app/cli/snapshot-tests/test_cfg_attr_typeshare/input.rs new file mode 100644 index 00000000..ce810442 --- /dev/null +++ b/app/cli/snapshot-tests/test_cfg_attr_typeshare/input.rs @@ -0,0 +1,4 @@ +#[cfg_attr(feature = "typeshare-support", typeshare)] +pub struct TestStruct1 { + field: String, +} diff --git a/app/cli/snapshot-tests/test_cfg_attr_typeshare/output.ts b/app/cli/snapshot-tests/test_cfg_attr_typeshare/output.ts new file mode 100644 index 00000000..0fa1ac57 --- /dev/null +++ b/app/cli/snapshot-tests/test_cfg_attr_typeshare/output.ts @@ -0,0 +1,4 @@ +export interface TestStruct1 { + field: string; +} + diff --git a/app/engine/Cargo.toml b/app/engine/Cargo.toml index cdc213ff..0b25c253 100644 --- a/app/engine/Cargo.toml +++ b/app/engine/Cargo.toml @@ -16,21 +16,30 @@ ignore = "0.4.23" indent_write = "2.2.0" itertools = "0.14.0" proc-macro2 = { version = "1.0.94", default-features = false, features = [ - "span-locations", + "span-locations", ] } lazy_format = "2" quote = { version = "1.0.38", default-features = false } rayon = "1.10.0" serde = { version = "1.0.217", default-features = false, features = ["derive"] } syn = { version = "2.0.96", features = [ - "full", - "visit", - "printing", - "parsing", + "full", + "visit", + "printing", + "parsing", ] } thiserror = "2.0.11" toml = "0.8.19" typeshare-model = { path = "../model", version = "2.0" } +flexi_logger.workspace = true +log.workspace = true [dev-dependencies] cool_asserts = "2.0.3" +syn = { version = "2.0.96", features = [ + "full", + "visit", + "printing", + "parsing", + "extra-traits", +] } diff --git a/app/engine/src/driver.rs b/app/engine/src/driver.rs index c6826133..2545b1c4 100644 --- a/app/engine/src/driver.rs +++ b/app/engine/src/driver.rs @@ -3,6 +3,7 @@ use std::{collections::HashMap, io}; use anyhow::Context as _; use clap::{CommandFactory as _, FromArgMatches as _}; use clap_complete::generate as generate_completions; +use flexi_logger::AdaptiveFormat; use ignore::{overrides::OverrideBuilder, types::TypesBuilder, WalkBuilder}; use itertools::Itertools; use lazy_format::lazy_format; @@ -184,6 +185,11 @@ pub fn main_body(personalizations: args::PersonalizeClap) -> anyhow::Res where Helper: LanguageHelper, { + flexi_logger::Logger::try_with_env_or_str("info")? + .adaptive_format_for_stderr(AdaptiveFormat::Opt) + .adaptive_format_for_stdout(AdaptiveFormat::Opt) + .start()?; + let language_metas = Helper::LanguageSet::compute_language_metas()?; let command = StandardArgs::command(); let command = add_personalizations(command, personalizations); diff --git a/app/engine/src/parser.rs b/app/engine/src/parser.rs index 6661c323..3a24c9b6 100644 --- a/app/engine/src/parser.rs +++ b/app/engine/src/parser.rs @@ -1,6 +1,7 @@ //! Source file parsing. use ignore::Walk; use itertools::Itertools; +use log::debug; use proc_macro2::{Delimiter, Group}; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use std::{ @@ -185,6 +186,7 @@ pub fn parse_input( inputs .into_par_iter() .map(|parser_input| { + debug!("Parsing file {:?}", parser_input.file_path); // Performance nit: we don't need to clone in the error case; // map_err is taking unconditional ownership unnecessarily let content = std::fs::read_to_string(&parser_input.file_path).map_err(|err| { @@ -285,7 +287,8 @@ pub fn parse( ) -> Result, ParseErrorSet> { // We will only produce output for files that contain the `#[typeshare]` // attribute, so this is a quick and easy performance win - if !source_code.contains("#[typeshare") { + if !source_code.contains("typeshare") { + debug!("No typeshare found in file"); return Ok(None); } @@ -296,7 +299,6 @@ pub fn parse( .map_err(|err| ParseError::new(&err.span(), ParseErrorKind::SynError(err)))?; import_visitor.visit_file(&file_contents); - import_visitor.parsed_data().map(Some) } @@ -690,12 +692,16 @@ fn parse_const_expr(e: &Expr) -> Result { // Helpers -/// Checks the given attrs for `#[typeshare]` +/// Checks the given attrs for `#[typeshare]` or `#[cfg_attr(, typeshare)]` pub(crate) fn has_typeshare_annotation(attrs: &[syn::Attribute]) -> bool { + let check_cfg_attr = |attr| { + get_meta_items(attr, "cfg_attr").iter().any(|item| { + matches!(item, Meta::Path(path) if path.segments.iter().any(|segment| segment.ident == TYPESHARE)) + }) + }; 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 { @@ -1096,4 +1102,10 @@ mod test_get_decorators { ); assert_eq!(decorators.type_override_for_lang("kotlin"), None); } + + #[test] + fn test_cfg_attr() { + let attr = parse_attr(r#"#[cfg_attr(feature = "typeshare", typeshare)]"#); + assert!(has_typeshare_annotation(&attr)); + } } From a64b1be7540d2d96c94409586b36d2d9b424f2ca Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Fri, 24 Oct 2025 15:11:57 -0400 Subject: [PATCH 02/14] port changes from main for cfg_attr support --- app/engine/src/parser.rs | 152 +++++++++++++++++++++++++++++++++------ 1 file changed, 131 insertions(+), 21 deletions(-) diff --git a/app/engine/src/parser.rs b/app/engine/src/parser.rs index 3a24c9b6..f1555096 100644 --- a/app/engine/src/parser.rs +++ b/app/engine/src/parser.rs @@ -695,8 +695,13 @@ fn parse_const_expr(e: &Expr) -> Result { /// Checks the given attrs for `#[typeshare]` or `#[cfg_attr(, typeshare)]` pub(crate) fn has_typeshare_annotation(attrs: &[syn::Attribute]) -> bool { let check_cfg_attr = |attr| { - get_meta_items(attr, "cfg_attr").iter().any(|item| { - matches!(item, Meta::Path(path) if path.segments.iter().any(|segment| segment.ident == TYPESHARE)) + 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 @@ -723,7 +728,6 @@ pub(crate) fn get_name_value_meta_items<'a>( ) -> impl Iterator + 'a { attrs.iter().flat_map(move |attr| { get_meta_items(attr, ident) - .iter() .filter_map(|arg| match arg { Meta::NameValue(name_value) if name_value.path.is_ident(name) => { expr_to_string(&name_value.value) @@ -735,16 +739,16 @@ pub(crate) fn get_name_value_meta_items<'a>( } /// Returns all arguments passed into `#[{ident}(...)]` where `{ident}` can be `serde` or `typeshare` attributes -fn get_meta_items(attr: &syn::Attribute, ident: &str) -> Vec { - if attr.path().is_ident(ident) { - attr.parse_args_with(Punctuated::::parse_terminated) - .iter() - .flat_map(|meta| meta.iter()) - .cloned() - .collect() - } else { - Vec::default() - } +fn get_meta_items(attr: &syn::Attribute, ident: &str) -> impl Iterator { + attr.path() + .is_ident(ident) + .then(|| { + attr.parse_args_with(Punctuated::::parse_terminated) + .into_iter() + .flat_map(|punctuated| punctuated.into_iter()) + }) + .into_iter() + .flatten() } fn get_ident(ident: Option<&Ident>, attrs: &[syn::Attribute], rename_all: Option<&str>) -> Id { @@ -803,7 +807,6 @@ fn parse_comment_attrs(attrs: &[Attribute]) -> Vec { fn is_skipped(attrs: &[syn::Attribute]) -> bool { attrs.iter().any(|attr| { get_meta_items(attr, SERDE) - .into_iter() .chain(get_meta_items(attr, TYPESHARE)) .any(|arg| matches!(arg, Meta::Path(path) if path.is_ident("skip"))) }) @@ -812,7 +815,6 @@ fn is_skipped(attrs: &[syn::Attribute]) -> bool { fn serde_attr(attrs: &[syn::Attribute], ident: &str) -> bool { attrs.iter().any(|attr| { get_meta_items(attr, SERDE) - .iter() .any(|arg| matches!(arg, Meta::Path(path) if path.is_ident(ident))) }) } @@ -831,13 +833,27 @@ fn serde_flatten(attrs: &[syn::Attribute]) -> bool { fn get_decorators(attrs: &[Attribute]) -> DecoratorSet { attrs .iter() - .flat_map(|attr| match attr.meta { - Meta::List(ref meta) => Some(meta), + .filter_map(|attr| match attr.meta { + Meta::List(ref meta_list) => Some(meta_list), Meta::Path(_) | Meta::NameValue(_) => None, }) - .filter(|meta| meta.path.is_ident(TYPESHARE)) - .filter_map(|meta| meta.parse_args_with(KeyValueSeq::parse_terminated).ok()) + .filter(|meta_list| meta_list.path.is_ident(TYPESHARE)) + .filter_map(|meta_list| { + meta_list + .parse_args_with(KeyValueSeq::parse_terminated) + .ok() + }) .flatten() + .chain(attrs.iter().flat_map(move |attr| { + get_meta_items(attr, "cfg_attr") + .filter_map(|meta| match meta { + Meta::List(meta_list) if meta_list.path.is_ident(TYPESHARE) => meta_list + .parse_args_with(KeyValueSeq::parse_terminated) + .ok(), + _ => None, + }) + .flatten() + })) .map(|pair| (pair.key, pair.value)) .collect() } @@ -1089,6 +1105,7 @@ mod test_get_decorators { ); let decorators = get_decorators(&attr); + dbg!(&decorators); eprintln!("{decorators:#?}"); @@ -1105,7 +1122,100 @@ mod test_get_decorators { #[test] fn test_cfg_attr() { - let attr = parse_attr(r#"#[cfg_attr(feature = "typeshare", typeshare)]"#); - assert!(has_typeshare_annotation(&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); + dbg!(&decorators); + + assert_eq!( + decorators + .type_override_for_lang("swift") + .expect("No swift decorators"), + "Equatable, Hashable" + ); + + // 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!( + // 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, None).expect("Failed to parse struct") + else { + panic!("Not a struct"); + }; + + dbg!(rust_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); + dbg!(decorators); + // let kotlin_decorator = decorators + // .get(&DecoratorKind::Kotlin) + // .expect("No kotlin decorator"); + // assert_eq!(kotlin_decorator, &BTreeSet::from_iter(["JvmInline".into()])); } } From cea3d990b179b3bf1251f9205290463eecfce284 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Fri, 24 Oct 2025 17:31:47 -0400 Subject: [PATCH 03/14] update tests for cfg_attr attribute parsing --- app/engine/src/parser.rs | 110 +++++++++++++++------------------------ 1 file changed, 42 insertions(+), 68 deletions(-) diff --git a/app/engine/src/parser.rs b/app/engine/src/parser.rs index f1555096..aa638184 100644 --- a/app/engine/src/parser.rs +++ b/app/engine/src/parser.rs @@ -978,24 +978,14 @@ fn test_rename_all_to_case() { #[cfg(test)] mod test_get_decorators { - use std::str::FromStr; - use cool_asserts::assert_matches; - use proc_macro2::TokenStream; - use syn::parse::Parser; use typeshare_model::decorator::Value; use super::*; - fn parse_attr(input: &str) -> Vec { - let tokens = TokenStream::from_str(input).expect("failed to create token stream"); - - Parser::parse2(Attribute::parse_outer, tokens).expect("failed to parse attribute") - } - #[test] fn basic() { - let attr = parse_attr("#[typeshare(foo)]"); + let attr: Vec = syn::parse_quote!(#[typeshare(foo)]); let decorators = get_decorators(&attr); assert_eq!(decorators.get_all("foo"), &[Value::None]); @@ -1004,7 +994,7 @@ mod test_get_decorators { #[test] fn several() { - let attr = parse_attr("#[typeshare(foo, int=10, string=\"foo\")]"); + let attr: Vec = syn::parse_quote!(#[typeshare(foo, int=10, string="foo")]); let decorators = get_decorators(&attr); assert_eq!(decorators.get_all("foo"), &[Value::None]); @@ -1018,7 +1008,7 @@ mod test_get_decorators { #[test] fn multi_key() { - let attr = parse_attr("#[typeshare(thing=10, foo, thing=\"hello\")]"); + let attr: Vec = syn::parse_quote!(#[typeshare(thing=10, foo, thing="hello")]); let decorators = get_decorators(&attr); assert_eq!(decorators.get_all("foo"), &[Value::None]); @@ -1030,10 +1020,10 @@ mod test_get_decorators { #[test] fn multiple_attributes() { - let attr = parse_attr( - "#[typeshare(foo, bar = \"baz\")] - #[typeshare(baz = 42, qux)]", - ); + let attr: Vec = syn::parse_quote! { + #[typeshare(foo, bar = "baz")] + #[typeshare(baz = 42, qux)] + }; let decorators = get_decorators(&attr); assert_eq!(decorators.get_all("foo"), &[Value::None]); @@ -1047,10 +1037,10 @@ mod test_get_decorators { #[test] fn duplicate_keys_in_multiple_attributes() { - let attr = parse_attr( - "#[typeshare(foo = \"bar\", foo = 42)] - #[typeshare(foo)]", - ); + let attr: Vec = syn::parse_quote! { + #[typeshare(foo = "bar", foo = 42)] + #[typeshare(foo)] + }; let decorators = get_decorators(&attr); assert_eq!( @@ -1066,11 +1056,11 @@ mod test_get_decorators { // Regression test for an earlier breakage #[test] fn jvm_inline() { - let attr = parse_attr( - "#[typeshare(kotlin =\"JvmInline\", redacted)] - #[derive(Serialize, Debug, Clone, PartialEq, Eq, Hash)] - #[serde(rename_all = \"camelCase\")]", - ); + let attr: Vec = syn::parse_quote! { + #[typeshare(kotlin ="JvmInline", redacted)] + #[derive(Serialize, Debug, Clone, PartialEq, Eq, Hash)] + #[serde(rename_all = "camelCase")] + }; let decorators = get_decorators(&attr); @@ -1083,7 +1073,7 @@ mod test_get_decorators { #[test] fn nested() { - let attr = parse_attr("#[typeshare(a, b(c=1, d=2, d=3))]"); + let attr: Vec = syn::parse_quote!(#[typeshare(a, b(c=1, d=2, d=3))]); let decorators = get_decorators(&attr); @@ -1099,15 +1089,12 @@ mod test_get_decorators { #[test] fn type_override() { - let attr = parse_attr( - "#[typeshare(typescript(type = \"string\"))] - #[typeshare(swift = \"Foo\", swift(type=\"NSString\"))]", - ); + let attr: Vec = syn::parse_quote! { + #[typeshare(typescript(type = "string"))] + #[typeshare(swift = "Foo", swift(type="NSString"))] + }; let decorators = get_decorators(&attr); - dbg!(&decorators); - - eprintln!("{decorators:#?}"); assert_eq!( decorators.type_override_for_lang("swift").unwrap(), @@ -1122,15 +1109,15 @@ mod test_get_decorators { #[test] fn test_cfg_attr() { - let attr: Attribute = syn::parse_quote! { + let attr: Vec = syn::parse_quote! { #[cfg_attr(feature = "typeshare-support", typeshare)] }; - assert!(has_typeshare_annotation(&[attr])); + assert!(has_typeshare_annotation(&attr)); } #[test] fn test_cfg_attr_with_nvps() { - let attr: Attribute = syn::parse_quote! { + let attrs: Vec = syn::parse_quote! { #[cfg_attr( feature = "typeshare-support", typeshare( @@ -1138,37 +1125,22 @@ mod test_get_decorators { swiftGenericConstraints = "R: Equatable & Hashable" ) )] - }; - let attrs = [attr]; + }; assert!(has_typeshare_annotation(&attrs)); - let decorators = get_decorators(&attrs); - dbg!(&decorators); + eprintln!("{decorators:#?}"); assert_eq!( - decorators - .type_override_for_lang("swift") - .expect("No swift decorators"), - "Equatable, Hashable" + decorators.get_all("swift"), + &[Value::String("Equatable, Hashable".into())] ); - // 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!( - // swift_constraints, - // &BTreeSet::from_iter(["R: Equatable & Hashable".into()]) - // ); + assert_eq!( + decorators.get_all("swiftGenericConstraints"), + &[Value::String("R: Equatable & Hashable".into())] + ); } #[test] @@ -1180,7 +1152,8 @@ mod test_get_decorators { let attrs = [attr]; assert!(has_typeshare_annotation(&attrs)); - // assert!(is_redacted(&attrs)); + let decorators = get_decorators(&attrs); + assert!(decorators.is_redacted()); } #[test] @@ -1196,8 +1169,7 @@ mod test_get_decorators { panic!("Not a struct"); }; - dbg!(rust_struct); - // assert!(rust_struct.is_redacted); + assert!(rust_struct.decorators.is_redacted()); } #[test] @@ -1212,10 +1184,12 @@ mod test_get_decorators { let attrs = [attr]; assert!(has_typeshare_annotation(&attrs)); let decorators = get_decorators(&attrs); - dbg!(decorators); - // let kotlin_decorator = decorators - // .get(&DecoratorKind::Kotlin) - // .expect("No kotlin decorator"); - // assert_eq!(kotlin_decorator, &BTreeSet::from_iter(["JvmInline".into()])); + + assert_eq!( + decorators.get_all("kotlin"), + &[Value::String("JvmInline".into())] + ); + + assert!(decorators.is_redacted()); } } From e5a29c91d50127b123d539bd667bf429cf9401d4 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Fri, 24 Oct 2025 17:57:44 -0400 Subject: [PATCH 04/14] add snapshot test --- .../test_cfg_attr_typeshare/input.rs | 30 +++++++++++++++++ .../test_cfg_attr_typeshare/output.kt | 33 +++++++++++++++++++ .../test_cfg_attr_typeshare/output.swift | 28 ++++++++++++++++ .../test_cfg_attr_typeshare/output.ts | 16 +++++++++ 4 files changed, 107 insertions(+) create mode 100644 app/cli/snapshot-tests/test_cfg_attr_typeshare/output.kt create mode 100644 app/cli/snapshot-tests/test_cfg_attr_typeshare/output.swift diff --git a/app/cli/snapshot-tests/test_cfg_attr_typeshare/input.rs b/app/cli/snapshot-tests/test_cfg_attr_typeshare/input.rs index ce810442..7fbe191e 100644 --- a/app/cli/snapshot-tests/test_cfg_attr_typeshare/input.rs +++ b/app/cli/snapshot-tests/test_cfg_attr_typeshare/input.rs @@ -1,4 +1,34 @@ +/// 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); diff --git a/app/cli/snapshot-tests/test_cfg_attr_typeshare/output.kt b/app/cli/snapshot-tests/test_cfg_attr_typeshare/output.kt new file mode 100644 index 00000000..7d49bfd1 --- /dev/null +++ b/app/cli/snapshot-tests/test_cfg_attr_typeshare/output.kt @@ -0,0 +1,33 @@ +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 +) + diff --git a/app/cli/snapshot-tests/test_cfg_attr_typeshare/output.swift b/app/cli/snapshot-tests/test_cfg_attr_typeshare/output.swift new file mode 100644 index 00000000..6d007799 --- /dev/null +++ b/app/cli/snapshot-tests/test_cfg_attr_typeshare/output.swift @@ -0,0 +1,28 @@ +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 + } +} diff --git a/app/cli/snapshot-tests/test_cfg_attr_typeshare/output.ts b/app/cli/snapshot-tests/test_cfg_attr_typeshare/output.ts index 0fa1ac57..10e1303d 100644 --- a/app/cli/snapshot-tests/test_cfg_attr_typeshare/output.ts +++ b/app/cli/snapshot-tests/test_cfg_attr_typeshare/output.ts @@ -1,4 +1,20 @@ +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; +} + From f4690cddac95d7c819911dea501758d4c4cfd9d1 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Fri, 24 Oct 2025 22:02:57 -0400 Subject: [PATCH 05/14] run snapshot tests --- .github/workflows/ci.yml | 5 +---- app/engine/src/parser.rs | 6 +++--- run-snapshot-tests.sh | 12 ++++++++++++ typeshare-snapshot-test/src/main.rs | 8 ++++---- 4 files changed, 20 insertions(+), 11 deletions(-) create mode 100755 run-snapshot-tests.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e9bceb8..da1c7047 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,6 @@ jobs: toolchain: ${{ matrix.rust }} - run: rustup run ${{ matrix.rust }} cargo check - test: name: Test runs-on: ubuntu-latest @@ -39,8 +38,8 @@ jobs: profile: minimal toolchain: ${{ matrix.rust }} - run: rustup run ${{ matrix.rust }} cargo test --all-features + - run: ./run-snapshot-tests.sh - check-deterministic: name: Ensure Deterministic Output runs-on: ubuntu-latest @@ -60,7 +59,6 @@ jobs: toolchain: ${{ matrix.rust }} - run: rustup toolchain install ${{ matrix.rust }} - run: just --justfile tests/justfile determinism - fmt: name: Rustfmt @@ -78,7 +76,6 @@ jobs: components: rustfmt - run: rustup run ${{ matrix.rust }} cargo fmt --all -- --check - clippy: name: Clippy runs-on: ubuntu-latest diff --git a/app/engine/src/parser.rs b/app/engine/src/parser.rs index aa638184..8d1519b8 100644 --- a/app/engine/src/parser.rs +++ b/app/engine/src/parser.rs @@ -32,6 +32,7 @@ use crate::{ const SERDE: &str = "serde"; const TYPESHARE: &str = "typeshare"; +const CFG_ATTR: &str = "cfg_attr"; /// An enum that encapsulates units of code generation for Typeshare. /// Analogous to `syn::Item`, even though our variants are more limited. @@ -695,7 +696,7 @@ fn parse_const_expr(e: &Expr) -> Result { /// Checks the given attrs for `#[typeshare]` or `#[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 { + get_meta_items(attr, CFG_ATTR).any(|item| match item { Meta::Path(path) => path .segments .iter() @@ -845,7 +846,7 @@ fn get_decorators(attrs: &[Attribute]) -> DecoratorSet { }) .flatten() .chain(attrs.iter().flat_map(move |attr| { - get_meta_items(attr, "cfg_attr") + get_meta_items(attr, CFG_ATTR) .filter_map(|meta| match meta { Meta::List(meta_list) if meta_list.path.is_ident(TYPESHARE) => meta_list .parse_args_with(KeyValueSeq::parse_terminated) @@ -1125,7 +1126,6 @@ mod test_get_decorators { swiftGenericConstraints = "R: Equatable & Hashable" ) )] - }; assert!(has_typeshare_annotation(&attrs)); diff --git a/run-snapshot-tests.sh b/run-snapshot-tests.sh new file mode 100755 index 00000000..10be41be --- /dev/null +++ b/run-snapshot-tests.sh @@ -0,0 +1,12 @@ +#!/bin/bash +TEST="cargo run --release --bin typeshare-snapshot-test --" +TEST_FOLDER="app/cli/snapshot-tests" +TYPESHARE="target/release/typeshare2" +declare -A languages=(["swift"]=".swift" ["typescript"]=".ts" ["kotlin"]=".kt") + +cargo build --release --all-targets && \ +for lang in "@{!languages[@]}" +do + echo "Running $lang tests" + $TEST -t $TYPESHARE --language "$lang" --mode test --suffix "${languages[$lang]}" $TEST_FOLDER +done diff --git a/typeshare-snapshot-test/src/main.rs b/typeshare-snapshot-test/src/main.rs index 96b0986f..9da7df06 100644 --- a/typeshare-snapshot-test/src/main.rs +++ b/typeshare-snapshot-test/src/main.rs @@ -714,10 +714,10 @@ fn main() -> anyhow::Result<()> { let entry_name = entry_name.to_string_lossy(); let entry_name = entry_name.into_owned(); - if let Some(names) = include_names { - if !names.contains(entry_name.as_str()) { - return Ok((entry_name, Report::Skip)); - } + if let Some(names) = include_names + && !names.contains(entry_name.as_str()) + { + return Ok((entry_name, Report::Skip)); } let meta = entry_path.metadata().with_context(|| { From 3291bf115b7eb52ee9585a913c170b790bcfbcbb Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Fri, 24 Oct 2025 22:08:46 -0400 Subject: [PATCH 06/14] run ci on PR against typeshare2 --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da1c7047..f494aec6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: pull_request: branches: - main + - typeshare2 name: CI From ff991e797f20d87563c0023b6d0db130133cb072 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Fri, 24 Oct 2025 22:34:33 -0400 Subject: [PATCH 07/14] clippy lints --- app/engine/src/config.rs | 4 +- app/langs/kotlin/src/lib.rs | 48 ++++++++++++++-------- app/langs/swift/src/lib.rs | 48 +++++++++++++--------- app/langs/typescript/src/lib.rs | 39 +++++++++++------- app/model/src/language.rs | 10 ++--- app/model/src/parsed_data.rs | 3 +- typeshare-snapshot-test/src/main.rs | 1 + typeshare-snapshot-test/src/sorted_iter.rs | 5 +-- 8 files changed, 95 insertions(+), 63 deletions(-) diff --git a/app/engine/src/config.rs b/app/engine/src/config.rs index ed3461ee..68c65f6b 100644 --- a/app/engine/src/config.rs +++ b/app/engine/src/config.rs @@ -63,8 +63,8 @@ impl Config { // serializing the config type into a toml table pub fn store_config_for_language( &mut self, - language: &str, - config: &T, + _language: &str, + _config: &T, ) -> anyhow::Result<()> { todo!() // self.raw_data.insert( diff --git a/app/langs/kotlin/src/lib.rs b/app/langs/kotlin/src/lib.rs index 83e57c3c..18c5605c 100644 --- a/app/langs/kotlin/src/lib.rs +++ b/app/langs/kotlin/src/lib.rs @@ -146,9 +146,11 @@ impl Kotlin<'_> { w, "data class {}{}(", variant_name, - (!e.shared().generic_types.is_empty()) - .then(|| format!("<{}>", e.shared().generic_types.join(", "))) - .unwrap_or_default() + if !e.shared().generic_types.is_empty() { + format!("<{}>", e.shared().generic_types.join(", ")) + } else { + String::new() + }, )?; let variant_type = self .format_type(ty, e.shared().generic_types.as_slice()) @@ -161,9 +163,11 @@ impl Kotlin<'_> { w, "data class {}{}(", variant_name, - (!e.shared().generic_types.is_empty()) - .then(|| format!("<{}>", e.shared().generic_types.join(", "))) - .unwrap_or_default() + if !e.shared().generic_types.is_empty() { + format!("<{}>", e.shared().generic_types.join(", ")) + } else { + String::new() + } )?; // Builds the list of generic types (e.g [T, U, V]), by digging @@ -205,9 +209,11 @@ impl Kotlin<'_> { ": {}{}{}()", self.prefix, e.shared().id.original, - (!e.shared().generic_types.is_empty()) - .then(|| format!("<{}>", e.shared().generic_types.join(", "))) - .unwrap_or_default() + if !e.shared().generic_types.is_empty() { + format!("<{}>", e.shared().generic_types.join(", ")) + } else { + String::new() + } )?; } } @@ -406,9 +412,11 @@ impl<'config> Language<'config> for Kotlin<'config> { w, "typealias {}{} = {}\n", type_name, - (!alias.generic_types.is_empty()) - .then(|| format!("<{}>", alias.generic_types.join(", "))) - .unwrap_or_default(), + if !alias.generic_types.is_empty() { + format!("<{}>", alias.generic_types.join(", ")) + } else { + String::new() + }, self.format_type(&alias.ty, alias.generic_types.as_slice()) .map_err(std::io::Error::other)? )?; @@ -439,9 +447,11 @@ impl<'config> Language<'config> for Kotlin<'config> { "data class {}{}{} (", self.prefix, rs.id.original, - (!rs.generic_types.is_empty()) - .then(|| format!("<{}>", rs.generic_types.join(", "))) - .unwrap_or_default() + if !rs.generic_types.is_empty() { + format!("<{}>", rs.generic_types.join(", ")) + } else { + String::new() + } )?; { @@ -509,9 +519,11 @@ impl<'config> Language<'config> for Kotlin<'config> { writeln!(w, "@Serializable")?; } - let generic_parameters = (!e.shared().generic_types.is_empty()) - .then(|| format!("<{}>", e.shared().generic_types.join(", "))) - .unwrap_or_default(); + let generic_parameters = if !e.shared().generic_types.is_empty() { + format!("<{}>", e.shared().generic_types.join(", ")) + } else { + String::new() + }; match e { RustEnum::Unit { .. } => { diff --git a/app/langs/swift/src/lib.rs b/app/langs/swift/src/lib.rs index c0a83a48..30edc096 100644 --- a/app/langs/swift/src/lib.rs +++ b/app/langs/swift/src/lib.rs @@ -612,9 +612,11 @@ impl<'config> Language<'config> for Swift<'config> { w, "public typealias {}{} = {}", type_name, - (!alias.generic_types.is_empty()) - .then(|| format!("<{}>", alias.generic_types.join(", "))) - .unwrap_or_default(), + if !alias.generic_types.is_empty() { + format!("<{}>", alias.generic_types.join(", ")) + } else { + String::new() + }, self.format_type(&alias.ty, alias.generic_types.as_slice()) .context("failed to format type")?, ) @@ -645,9 +647,11 @@ impl<'config> Language<'config> for Swift<'config> { writeln!( w, "public struct {type_name}{}: {} {{", - (!rs.generic_types.is_empty()) - .then(|| format!("<{generic_names_and_constraints}>",)) - .unwrap_or_default(), + if !rs.generic_types.is_empty() { + format!("<{generic_names_and_constraints}>") + } else { + String::new() + }, decs )?; @@ -686,9 +690,11 @@ impl<'config> Language<'config> for Swift<'config> { writeln!( w, "public let {fixed_name}: {ty}{}", - (f.has_default && !f.ty.is_optional()) - .then_some("?") - .unwrap_or_default() + if f.has_default && !f.ty.is_optional() { + "?" + } else { + Default::default() + } )?; } @@ -722,9 +728,11 @@ impl<'config> Language<'config> for Swift<'config> { "{}: {}{}", remove_dash_from_identifier(f.id.renamed.as_str()), ty, - (f.has_default && !f.ty.is_optional()) - .then_some("?") - .unwrap_or_default() + if f.has_default && !f.ty.is_optional() { + "?" + } else { + Default::default() + } )); } @@ -791,9 +799,11 @@ impl<'config> Language<'config> for Swift<'config> { writeln!( w, "public {indirect}enum {enum_name}{}: {} {{", - (!e.shared().generic_types.is_empty()) - .then(|| format!("<{generic_names_and_constraints}>",)) - .unwrap_or_default(), + if !e.shared().generic_types.is_empty() { + format!("<{generic_names_and_constraints}>") + } else { + String::new() + }, decs )?; @@ -882,10 +892,10 @@ impl<'config> Language<'config> for Swift<'config> { let path = output_folder.join("Codable.swift"); - if let Ok(old_content) = fs::read(&path) { - if content == old_content { - return Ok(()); - } + if let Ok(old_content) = fs::read(&path) + && content == old_content + { + return Ok(()); } let mut w = fs::File::create(&path)?; diff --git a/app/langs/typescript/src/lib.rs b/app/langs/typescript/src/lib.rs index 2a1b3cd2..077d0f57 100644 --- a/app/langs/typescript/src/lib.rs +++ b/app/langs/typescript/src/lib.rs @@ -162,14 +162,17 @@ impl Language<'_> for TypeScript { w, "export type {}{} = {}{};\n", ty.id.original, - (!ty.generic_types.is_empty()) - .then(|| format!("<{}>", ty.generic_types.iter().join_with(", "))) - .unwrap_or_default(), + if !ty.generic_types.is_empty() { + format!("<{}>", ty.generic_types.iter().join_with(", ")) + } else { + String::new() + }, r#type, - ty.ty - .is_optional() - .then_some(" | undefined") - .unwrap_or_default(), + if ty.ty.is_optional() { + " | undefined" + } else { + Default::default() + }, )?; Ok(()) @@ -181,9 +184,11 @@ impl Language<'_> for TypeScript { w, "export interface {}{} {{", rs.id.original, - (!rs.generic_types.is_empty()) - .then(|| format!("<{}>", rs.generic_types.iter().join_with(", "))) - .unwrap_or_default() + if !rs.generic_types.is_empty() { + format!("<{}>", rs.generic_types.iter().join_with(", ")) + } else { + String::new() + } )?; rs.fields @@ -198,9 +203,11 @@ impl Language<'_> for TypeScript { fn write_enum(&self, w: &mut impl Write, e: &RustEnum) -> anyhow::Result<()> { self.write_comments(w, 0, &e.shared().comments)?; - let generic_parameters = (!e.shared().generic_types.is_empty()) - .then(|| format!("<{}>", e.shared().generic_types.iter().join_with(", "))) - .unwrap_or_default(); + let generic_parameters = if !e.shared().generic_types.is_empty() { + format!("<{}>", e.shared().generic_types.iter().join_with(", ")) + } else { + String::new() + }; match e { RustEnum::Unit { shared, .. } => { @@ -301,7 +308,11 @@ impl TypeScript { tag_key, shared.id.renamed.as_str(), content_key, - ty.is_optional().then_some("?").unwrap_or_default(), + if ty.is_optional() { + "?" + } else { + Default::default() + }, r#type ) .with_context(|| { diff --git a/app/model/src/language.rs b/app/model/src/language.rs index b1038adf..e400672f 100644 --- a/app/model/src/language.rs +++ b/app/model/src/language.rs @@ -8,7 +8,7 @@ use crate::parsed_data::{ SpecialRustType, TypeName, }; -/// If we're in multifile mode, this enum contains the crate name for the +/// If we're in multi-file mode, this enum contains the crate name for the /// specific file #[derive(Debug, Clone, Copy)] #[non_exhaustive] @@ -32,6 +32,7 @@ impl FilesMode { } } +#[expect(clippy::doc_lazy_continuation)] /** *The* trait you need to implement in order to have your own implementation of typeshare. The whole world revolves around this trait. @@ -62,8 +63,8 @@ they're defaulted. It's also very common to implement: -- `mapped_type`, to define certain types as having specialied handling in your - lanugage. +- `mapped_type`, to define certain types as having specialized handling in your + language. - `begin_file`, `end_file`, and `write_additional_files`, to add additional per-file or per-directory content to your output. @@ -81,8 +82,7 @@ in what order. For these examples, we're assuming a hypothetical implementation for Kotlin, which means that there must be `impl Language<'_> for Kotlin` somewhere. -1. The language's config is loaded from the config file and command line -arguments: +1. The language's config is loaded from the config file and command line arguments: ```ignore let config = Kotlin::Config::deserialize(config_file)?; diff --git a/app/model/src/parsed_data.rs b/app/model/src/parsed_data.rs index 9a97977c..03868174 100644 --- a/app/model/src/parsed_data.rs +++ b/app/model/src/parsed_data.rs @@ -30,7 +30,7 @@ impl CrateName { /// Extract the crate name from a give path to a rust source file. This is /// defined as the name of the directory one level above the `src` directory - /// that cotains this source file, with any `-` replaced with `_`. + /// that contains this source file, with any `-` replaced with `_`. pub fn find_crate_name(path: &Path) -> Option { path.ancestors() // Only consider paths that contain normal stuff. If there's a @@ -150,6 +150,7 @@ pub enum RustType { /// - `SomeStruct` /// - `SomeEnum` /// - `SomeTypeAlias<(), &str>` + /// /// However, there are some generic types that are considered to be _special_. These /// include `Vec` `HashMap`, and `Option`, which are part of `SpecialRustType` instead /// of `RustType::Generic`. diff --git a/typeshare-snapshot-test/src/main.rs b/typeshare-snapshot-test/src/main.rs index 9da7df06..914a6e54 100644 --- a/typeshare-snapshot-test/src/main.rs +++ b/typeshare-snapshot-test/src/main.rs @@ -457,6 +457,7 @@ fn clear_item(path: &Path) { let _ = fs::remove_dir_all(path); } +#[expect(clippy::too_many_arguments)] fn snapshot_test( snapshot_directory: &Path, mode: Mode, diff --git a/typeshare-snapshot-test/src/sorted_iter.rs b/typeshare-snapshot-test/src/sorted_iter.rs index 4644b392..ff80ac3b 100644 --- a/typeshare-snapshot-test/src/sorted_iter.rs +++ b/typeshare-snapshot-test/src/sorted_iter.rs @@ -1,7 +1,4 @@ -use std::{ - cmp::{self, Ordering}, - usize, -}; +use std::cmp::{self, Ordering}; #[derive(Debug, Clone)] pub enum EitherOrBoth { From 481cb4bb1ba6027268398bd53f631bd83f35e571 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Sat, 25 Oct 2025 10:19:25 -0400 Subject: [PATCH 08/14] Add missing docs. Fix deterministic test. --- app/cli/src/main.rs | 2 +- app/engine/src/args.rs | 24 ++++++++++++++++++++---- app/engine/src/config.rs | 23 ++++++++++++++--------- app/engine/src/driver.rs | 8 +++++++- app/engine/src/lib.rs | 16 +++++++++++++--- app/engine/src/parser.rs | 20 +++++++++++--------- app/engine/src/serde/args.rs | 3 +++ app/engine/src/writer.rs | 3 +++ app/langs/kotlin/src/lib.rs | 15 ++++++++------- app/langs/swift/src/lib.rs | 19 ++++++++++--------- app/langs/typescript/src/lib.rs | 16 +++++++++++----- app/model/src/decorator.rs | 5 +++++ app/model/src/language.rs | 13 ++++++++----- app/model/src/lib.rs | 1 + app/model/src/parsed_data.rs | 5 +++++ tests/justfile | 4 ++-- typeshare-snapshot-test/src/main.rs | 1 + 17 files changed, 123 insertions(+), 55 deletions(-) diff --git a/app/cli/src/main.rs b/app/cli/src/main.rs index 58d09517..7446eaa8 100644 --- a/app/cli/src/main.rs +++ b/app/cli/src/main.rs @@ -1,5 +1,5 @@ +//! Typeshare binary runner. use typeshare_driver::typeshare_binary; - use typeshare_kotlin::Kotlin; use typeshare_swift::Swift; use typeshare_typescript::TypeScript; diff --git a/app/engine/src/args.rs b/app/engine/src/args.rs index 8deea060..8edc245a 100644 --- a/app/engine/src/args.rs +++ b/app/engine/src/args.rs @@ -1,15 +1,18 @@ -use std::path::{Path, PathBuf}; - -use clap::builder::PossibleValuesParser; - +//! Command line arguments use crate::serde::args::{ArgType, CliArgsSet}; +use clap::builder::PossibleValuesParser; +use std::path::{Path, PathBuf}; +/// Generated typeshared file output location. #[derive(Debug, Clone, Copy)] pub enum OutputLocation<'a> { + /// Output to a single file. File(&'a Path), + /// Output multiple files to folder. Folder(&'a Path), } +/// Output to folder or single file. #[derive(clap::Args, Debug)] #[group(multiple = false, required = true)] pub struct Output { @@ -25,6 +28,7 @@ pub struct Output { } impl Output { + /// Get the output location pub fn location(&self) -> OutputLocation<'_> { match (&self.directory, &self.file) { (Some(dir), None) => OutputLocation::Folder(dir), @@ -37,9 +41,11 @@ impl Output { } } +/// Command line arguments. #[derive(clap::Parser, Debug)] #[command(args_conflicts_with_subcommands = true, subcommand_negates_reqs = true)] pub struct StandardArgs { + /// Sub command. #[command(subcommand)] pub subcommand: Option, @@ -51,9 +57,11 @@ pub struct StandardArgs { #[arg(num_args(1..), required=true)] pub directories: Vec, + /// Command line completions #[arg(long, exclusive(true))] pub completions: Option, + /// Output folder or file. #[command(flatten)] pub output: Output, @@ -72,6 +80,7 @@ pub struct StandardArgs { pub target_os: Option>, } +/// Command #[derive(Debug, Clone, Copy, clap::Subcommand)] pub enum Command { /// Generate shell completions @@ -81,6 +90,7 @@ pub enum Command { }, } +/// Personalize clap. #[derive(Debug, Clone, Default)] #[non_exhaustive] pub struct PersonalizeClap { @@ -91,6 +101,7 @@ pub struct PersonalizeClap { } impl PersonalizeClap { + /// New personalized clap. pub const fn new() -> Self { Self { name: None, @@ -100,6 +111,7 @@ impl PersonalizeClap { } } + /// App name pub const fn name(self, name: &'static str) -> Self { Self { name: Some(name), @@ -107,6 +119,7 @@ impl PersonalizeClap { } } + /// App version pub const fn version(self, version: &'static str) -> Self { Self { version: Some(version), @@ -114,6 +127,7 @@ impl PersonalizeClap { } } + /// App author pub const fn author(self, author: &'static str) -> Self { Self { author: Some(author), @@ -121,6 +135,7 @@ impl PersonalizeClap { } } + /// About app pub const fn about(self, about: &'static str) -> Self { Self { about: Some(about), @@ -129,6 +144,7 @@ impl PersonalizeClap { } } +/// Add clap personalizations. pub fn add_personalizations( command: clap::Command, personalizations: PersonalizeClap, diff --git a/app/engine/src/config.rs b/app/engine/src/config.rs index 68c65f6b..80432c34 100644 --- a/app/engine/src/config.rs +++ b/app/engine/src/config.rs @@ -1,19 +1,19 @@ +//! Configuration data. +pub use crate::serde::args::CliArgsSet; +use crate::serde::{args::ArgsSetSerializer, config::ConfigDeserializer, empty::EmptyDeserializer}; +use anyhow::Context; +use serde::{ser, Deserialize, Serialize}; use std::{ collections::BTreeMap, env, fs, path::{Path, PathBuf}, }; - -use anyhow::Context; -use serde::{ser, Deserialize, Serialize}; use typeshare_model::Language; -use crate::serde::{args::ArgsSetSerializer, config::ConfigDeserializer, empty::EmptyDeserializer}; - -pub use crate::serde::args::CliArgsSet; - +/// Default configuration file name. const DEFAULT_CONFIG_FILE_NAME: &str = "typeshare.toml"; +/// Global application configuration. #[derive(Debug, Clone, Default, Deserialize)] pub struct GlobalConfig { /// If present, only fields / variants / items that are accepted by at @@ -59,8 +59,8 @@ impl Config { self.raw_data.get(language).unwrap_or(&self.empty) } - // Store a config for a language, overriding the existing one, by - // serializing the config type into a toml table + /// Store a config for a language, overriding the existing one, by + /// serializing the config type into a toml table pub fn store_config_for_language( &mut self, _language: &str, @@ -77,6 +77,7 @@ impl Config { // Ok(()) } + /// Get global configuration pub fn global_config(&self) -> &GlobalConfig { &self.typeshare } @@ -91,6 +92,7 @@ impl Serialize for Config { } } +/// Parse and compute command line arguments. pub fn compute_args_set<'a, L: Language<'a>>() -> anyhow::Result { let empty_config = L::Config::deserialize(EmptyDeserializer).context( "failed to create empty config; \ @@ -119,6 +121,7 @@ pub fn compute_args_set<'a, L: Language<'a>>() -> anyhow::Result { // Ok(()) // } +/// Load configuration from disk. pub fn load_config(file_path: Option<&Path>) -> anyhow::Result { let file_path_buf; @@ -164,6 +167,7 @@ fn find_configuration_file() -> Option { } } +/// Load a language configuration. pub fn load_language_config<'a, 'config, L: Language<'config>>( config_file_entry: &'config toml::Table, cli_matches: &'config clap::ArgMatches, @@ -177,6 +181,7 @@ pub fn load_language_config<'a, 'config, L: Language<'config>>( .context("error deserializing config") } +/// Load a language configuration using file and arguments. pub fn load_language_config_from_file_and_args<'a, 'config, L: Language<'config>>( config: &'config Config, cli_matches: &'config clap::ArgMatches, diff --git a/app/engine/src/driver.rs b/app/engine/src/driver.rs index 2545b1c4..f013dae0 100644 --- a/app/engine/src/driver.rs +++ b/app/engine/src/driver.rs @@ -1,3 +1,4 @@ +//! Program runner. use std::{collections::HashMap, io}; use anyhow::Context as _; @@ -21,7 +22,9 @@ use crate::{ writer::write_output, }; +/// Language set. pub trait LanguageSet<'config> { + /// Language meta-data. type LanguageMetas: 'static; /// Each language has a set of configuration metadata, describing all @@ -41,6 +44,7 @@ pub trait LanguageSet<'config> { metas: &Self::LanguageMetas, ) -> clap::Command; + /// Run typeshare for provided language output. fn execute_typeshare_for_language( language: &str, config: &'config config::Config, @@ -175,12 +179,14 @@ language_set_for! { } /// This trait is used by the driver macro to unify the 'config lifetime -/// across all of the language types. I'm open to suggesstions for getting +/// across all of the language types. I'm open to suggestions for getting /// rid of this. pub trait LanguageHelper { + /// Language set for this language helper. type LanguageSet<'config>: LanguageSet<'config>; } +/// The "main" function for running typeshare. pub fn main_body(personalizations: args::PersonalizeClap) -> anyhow::Result<()> where Helper: LanguageHelper, diff --git a/app/engine/src/lib.rs b/app/engine/src/lib.rs index c480af35..0374c278 100644 --- a/app/engine/src/lib.rs +++ b/app/engine/src/lib.rs @@ -1,3 +1,4 @@ +//! Typeshare parser and writer pub mod args; pub mod config; pub mod driver; @@ -10,14 +11,13 @@ mod type_parser; mod visitors; pub mod writer; +use indent_write::fmt::IndentWriter; +use proc_macro2::LineColumn; use std::{ fmt::{self, Display, Write}, io, path::PathBuf, }; - -use indent_write::fmt::IndentWriter; -use proc_macro2::LineColumn; use syn::spanned::Spanned; use thiserror::Error; use typeshare_model::prelude::{CrateName, TypeName}; @@ -28,12 +28,16 @@ pub use typeshare_model::prelude::FilesMode; /// A set of parse errors from a specific file #[derive(Debug, Error)] pub struct FileParseErrors { + /// File path pub path: PathBuf, + /// Name of crate being parsed pub crate_name: Option, + /// Error kind pub kind: FileErrorKind, } impl FileParseErrors { + /// Create a new file parse errors set. pub fn new(path: PathBuf, crate_name: Option, kind: FileErrorKind) -> Self { Self { path, @@ -85,6 +89,7 @@ pub struct ParseErrorSet { } impl ParseErrorSet { + /// Collect parse errors into a parse error set. pub fn collect(errors: impl IntoIterator) -> Result<(), Self> { let mut errors = errors.into_iter().peekable(); @@ -119,6 +124,7 @@ impl Display for ParseErrorSet { } } +/// A file parsing error. #[derive(Debug, Error)] #[error("at {}:{}..{}:{}: {kind}", .start.line, @@ -127,12 +133,16 @@ impl Display for ParseErrorSet { .end.column, )] pub struct ParseError { + /// Line column start start: LineColumn, + /// Line column end end: LineColumn, + /// Error kind kind: ParseErrorKind, } impl ParseError { + /// Create a new parse error. pub fn new(span: &impl Spanned, kind: ParseErrorKind) -> Self { let span = span.span(); Self { diff --git a/app/engine/src/parser.rs b/app/engine/src/parser.rs index 8d1519b8..26087ad1 100644 --- a/app/engine/src/parser.rs +++ b/app/engine/src/parser.rs @@ -1,4 +1,11 @@ //! Source file parsing. +use crate::{ + rename::RenameExt, + target_os, + type_parser::{parse_rust_type, parse_rust_type_from_string, type_name}, + visitors::TypeShareVisitor, + FileParseErrors, ParseError, ParseErrorKind, ParseErrorSet, +}; use ignore::Walk; use itertools::Itertools; use log::debug; @@ -16,20 +23,11 @@ use syn::{ Attribute, Expr, ExprGroup, ExprLit, ExprParen, Fields, GenericParam, Ident, ItemConst, ItemEnum, ItemStruct, ItemType, Lit, Meta, Token, }; - use typeshare_model::{ decorator::{self, DecoratorSet}, prelude::*, }; -use crate::{ - rename::RenameExt, - target_os, - type_parser::{parse_rust_type, parse_rust_type_from_string, type_name}, - visitors::TypeShareVisitor, - FileParseErrors, ParseError, ParseErrorKind, ParseErrorSet, -}; - const SERDE: &str = "serde"; const TYPESHARE: &str = "typeshare"; const CFG_ATTR: &str = "cfg_attr"; @@ -69,6 +67,7 @@ pub struct ParsedData { } impl ParsedData { + /// Merge one parsed data with this one. pub fn merge(&mut self, other: Self) { self.structs.extend(other.structs); self.enums.extend(other.enums); @@ -77,6 +76,7 @@ impl ParsedData { self.import_types.extend(other.import_types); } + /// Add a rust item. pub fn add(&mut self, item: RustItem) { match item { RustItem::Struct(rust_struct) => self.structs.push(rust_struct), @@ -86,6 +86,7 @@ impl ParsedData { } } + /// Yield TypeName's pub fn all_type_names(&self) -> impl Iterator + use<'_> { let s = self.structs.iter().map(|s| &s.id.renamed); let e = self.enums.iter().map(|e| &e.shared().id.renamed); @@ -96,6 +97,7 @@ impl ParsedData { s.chain(e).chain(a) } + /// Sort parsed data. pub fn sort_contents(&mut self) { self.structs .sort_unstable_by(|lhs, rhs| Ord::cmp(&lhs.id.original, &rhs.id.original)); diff --git a/app/engine/src/serde/args.rs b/app/engine/src/serde/args.rs index f1de5d6b..2dacb97e 100644 --- a/app/engine/src/serde/args.rs +++ b/app/engine/src/serde/args.rs @@ -1,3 +1,4 @@ +//! Command line argument parsing. use std::{ collections::HashMap, fmt::{self, Display, Write}, @@ -26,6 +27,7 @@ pub struct CliArgsSet { } impl CliArgsSet { + /// Iterator of ArgSpec pub fn iter(&self) -> impl Iterator> + '_ { self.args .iter() @@ -36,6 +38,7 @@ impl CliArgsSet { }) } + /// Checks if arguments has provided key. pub fn contains_key(&self, key: &str) -> bool { self.args.contains_key(key) } diff --git a/app/engine/src/writer.rs b/app/engine/src/writer.rs index 9318c8f7..93aaf6e9 100644 --- a/app/engine/src/writer.rs +++ b/app/engine/src/writer.rs @@ -1,3 +1,4 @@ +//! Single or multi-file writer for generated typeshared types. use std::{ collections::{BTreeMap, BTreeSet, HashMap, HashSet}, fs, @@ -10,6 +11,7 @@ use typeshare_model::prelude::*; use crate::{args::OutputLocation, parser::ParsedData, topsort::topsort}; +/// Writes the generated typeshared types to the file system. pub fn write_output<'c>( lang: &impl Language<'c>, crate_parsed_data: HashMap, ParsedData>, @@ -151,6 +153,7 @@ pub enum BorrowedRustItem<'a> { } impl BorrowedRustItem<'_> { + /// Typename identifier pub fn id(&self) -> &TypeName { &match *self { BorrowedRustItem::Struct(item) => &item.id, diff --git a/app/langs/kotlin/src/lib.rs b/app/langs/kotlin/src/lib.rs index 18c5605c..3bacc979 100644 --- a/app/langs/kotlin/src/lib.rs +++ b/app/langs/kotlin/src/lib.rs @@ -1,16 +1,15 @@ -use std::{ - borrow::Cow, - collections::HashMap, - io::{self, Write as _}, -}; - +//! Code generation for Kotlin use anyhow::Context; use indent_write::io::IndentWriter; use itertools::Itertools as _; use joinery::JoinableIterator as _; use lazy_format::lazy_format; use serde::{Deserialize, Serialize}; - +use std::{ + borrow::Cow, + collections::HashMap, + io::{self, Write as _}, +}; use typeshare_model::{ decorator::{DecoratorSet, Value}, prelude::*, @@ -21,6 +20,7 @@ enum Visibility { Private, } +/// Kotlin config #[derive(Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct Config<'config> { @@ -42,6 +42,7 @@ pub struct Config<'config> { no_version_header: bool, } +/// Kotlin language #[derive(Debug)] pub struct Kotlin<'config> { package: &'config str, diff --git a/app/langs/swift/src/lib.rs b/app/langs/swift/src/lib.rs index 30edc096..8ab1975f 100644 --- a/app/langs/swift/src/lib.rs +++ b/app/langs/swift/src/lib.rs @@ -1,3 +1,10 @@ +//! Code generation for Swift +use anyhow::Context; +use indent_write::io::IndentWriter; +use itertools::Itertools; +use joinery::{Joinable, JoinableIterator}; +use lazy_format::lazy_format; +use serde::{Deserialize, Serialize}; use std::{ borrow::Cow, collections::{BTreeSet, HashMap}, @@ -6,20 +13,12 @@ use std::{ path::Path, sync::atomic::{AtomicBool, Ordering}, }; - -use anyhow::Context; -use indent_write::io::IndentWriter; -use itertools::Itertools; -use joinery::{Joinable, JoinableIterator}; -use lazy_format::lazy_format; -use serde::{Deserialize, Serialize}; - use typeshare_model::{ decorator::{DecoratorSet, Value}, prelude::*, }; -// Keywords taken from https://docs.swift.org/swift-book/ReferenceManual/LexicalStructure.html +/// Keywords taken from https://docs.swift.org/swift-book/ReferenceManual/LexicalStructure.html const SWIFT_KEYWORDS: &[&str] = &[ "associatedtype", "class", @@ -86,6 +85,7 @@ struct CodingKeysInfo { coding_keys: Vec, } +/// Configuration #[derive(Debug, Serialize, Deserialize)] pub struct Config<'a> { /// The prefix to apply to all swift types @@ -115,6 +115,7 @@ pub struct Config<'a> { no_version_header: bool, } +/// Swift language #[derive(Debug)] pub struct Swift<'a> { prefix: &'a str, diff --git a/app/langs/typescript/src/lib.rs b/app/langs/typescript/src/lib.rs index 077d0f57..7f0955ce 100644 --- a/app/langs/typescript/src/lib.rs +++ b/app/langs/typescript/src/lib.rs @@ -1,16 +1,17 @@ +//! Code generation for Typescript +use anyhow::Context; +use itertools::Itertools; +use joinery::JoinableIterator; +use serde::{Deserialize, Serialize}; use std::{ borrow::Cow, collections::HashMap, io::{self, Write}, }; - -use anyhow::Context; -use itertools::Itertools; -use joinery::JoinableIterator; -use serde::{Deserialize, Serialize}; use thiserror::Error; use typeshare_model::{decorator::Value, prelude::*}; +/// Typescript language config #[derive(Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct Config { @@ -27,15 +28,20 @@ pub struct Config { /// All information needed to generate Typescript type-code #[derive(Default, Debug)] pub struct TypeScript { + /// Type mappings pub type_mappings: HashMap, + /// No version header pub no_version_header: bool, } +/// Format type error #[derive(Debug, Clone, Error)] pub enum FormatTypeError { + /// Large integer #[error("Can't have 64-bit or larger integers in typescript")] LargeInteger, + /// Generic key map #[error("Can't have generic types as map keys in typescript")] GenericMapKey, } diff --git a/app/model/src/decorator.rs b/app/model/src/decorator.rs index 1c246fed..76ef58a0 100644 --- a/app/model/src/decorator.rs +++ b/app/model/src/decorator.rs @@ -1,3 +1,4 @@ +//! Module for parsing and representing attribute decorators. use std::collections::HashMap; /// A decorator value can either be any literal (`ident = "foo"`), or absent @@ -7,9 +8,13 @@ use std::collections::HashMap; pub enum Value { /// The key was present as an attribute without an associated value. None, + /// A bool Bool(bool), + /// A String String(String), + /// An unsigned 32-bit integer Int(u32), + /// A nested decorator set. Nested(DecoratorSet), } diff --git a/app/model/src/language.rs b/app/model/src/language.rs index e400672f..18729d18 100644 --- a/app/model/src/language.rs +++ b/app/model/src/language.rs @@ -1,25 +1,27 @@ -use std::{borrow::Cow, fmt::Debug, io::Write, path::Path}; - -use anyhow::Context; -use itertools::Itertools; - +//! Module for language generation. use crate::parsed_data::{ CrateName, Id, RustConst, RustEnum, RustEnumVariant, RustStruct, RustType, RustTypeAlias, SpecialRustType, TypeName, }; +use anyhow::Context; +use itertools::Itertools; +use std::{borrow::Cow, fmt::Debug, io::Write, path::Path}; /// If we're in multi-file mode, this enum contains the crate name for the /// specific file #[derive(Debug, Clone, Copy)] #[non_exhaustive] pub enum FilesMode { + /// Single file mode Single, + /// Multi-file mode Multi(T), // We've had requests for java support, which means we'll need a // 1-file-per-type mode } impl FilesMode { + /// Apply function. pub fn map(self, op: impl FnOnce(T) -> U) -> FilesMode { match self { FilesMode::Single => FilesMode::Single, @@ -27,6 +29,7 @@ impl FilesMode { } } + /// Is multi-file mode enabled. pub fn is_multi(&self) -> bool { matches!(*self, Self::Multi(_)) } diff --git a/app/model/src/lib.rs b/app/model/src/lib.rs index 7b790138..e837e66e 100644 --- a/app/model/src/lib.rs +++ b/app/model/src/lib.rs @@ -11,6 +11,7 @@ pub mod parsed_data; pub use language::*; pub mod prelude { + //! Default exports when importing from prelude. pub use crate::language::{FilesMode, Language}; pub use crate::parsed_data::{ CrateName, Id, ImportedType, RustConst, RustConstExpr, RustEnum, RustEnumShared, diff --git a/app/model/src/parsed_data.rs b/app/model/src/parsed_data.rs index 03868174..14df720a 100644 --- a/app/model/src/parsed_data.rs +++ b/app/model/src/parsed_data.rs @@ -1,3 +1,4 @@ +//! Data model representation of parsed typeshare types and meta-data. use std::{ borrow::{Borrow, Cow}, cmp::Ord, @@ -19,6 +20,7 @@ impl Display for CrateName { } impl CrateName { + /// Create a new CrateName. pub const fn new(name: String) -> Self { Self(name) } @@ -509,18 +511,21 @@ pub struct TypeName(Cow<'static, str>); impl TypeName { #[inline] #[must_use] + /// View as a string slice pub fn as_str(&self) -> &str { self.0.as_ref() } #[inline] #[must_use] + /// Create a TypeName from an String pub fn new_string(ident: String) -> Self { Self(Cow::Owned(ident)) } #[inline] #[must_use] + /// Create a TypeName from an String slice pub const fn new_static(ident: &'static str) -> Self { Self(Cow::Borrowed(ident)) } diff --git a/tests/justfile b/tests/justfile index 65335df9..a1a4f1d0 100644 --- a/tests/justfile +++ b/tests/justfile @@ -10,8 +10,8 @@ generate-determinism-files: done determinism: clean generate-determinism-files mkdir -p out - cargo run -p typeshare-cli -- -l typescript -o "out/output1.ts" input - cargo run -p typeshare-cli -- -l typescript -o "out/output2.ts" input + cargo run --bin typeshare2 -- -l typescript -o "out/output1.ts" input + cargo run --bin typeshare2 -- -l typescript -o "out/output2.ts" input diff -q out/*.ts clean: diff --git a/typeshare-snapshot-test/src/main.rs b/typeshare-snapshot-test/src/main.rs index 914a6e54..50a1fda4 100644 --- a/typeshare-snapshot-test/src/main.rs +++ b/typeshare-snapshot-test/src/main.rs @@ -1,3 +1,4 @@ +//! Binary to generate or run snapshot tests. mod config; mod sorted_iter; From 8f79df92494574825b5876cc3adfad191c424955 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Sat, 25 Oct 2025 11:11:50 -0400 Subject: [PATCH 09/14] add back info and error logging --- Cargo.lock | 43 ++++++++------------------------------ Cargo.toml | 30 +++++++++++++------------- app/cli/Cargo.toml | 1 + app/driver/Cargo.toml | 1 + app/driver/src/lib.rs | 9 +++++--- app/engine/src/driver.rs | 43 +++++++++++++++++++++++++------------- app/engine/src/parser.rs | 5 +++++ app/engine/src/visitors.rs | 15 ++++++------- app/engine/src/writer.rs | 16 +++++++------- 9 files changed, 80 insertions(+), 83 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04b7d450..dacba07c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -259,24 +259,17 @@ checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" [[package]] name = "flexi_logger" -version = "0.28.5" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cca927478b3747ba47f98af6ba0ac0daea4f12d12f55e9104071b3dc00276310" +checksum = "31e5335674a3a259527f97e9176a3767dcc9b220b8e29d643daeb2d6c72caf8b" dependencies = [ "chrono", - "glob", "log", "nu-ansi-term", "regex", - "thiserror 1.0.69", + "thiserror", ] -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - [[package]] name = "globset" version = "0.4.15" @@ -633,33 +626,13 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.12", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -729,6 +702,7 @@ name = "typeshare-driver" version = "1.3.0" dependencies = [ "anyhow", + "log", "typeshare-engine", ] @@ -751,7 +725,7 @@ dependencies = [ "rayon", "serde", "syn", - "thiserror 2.0.12", + "thiserror", "toml", "typeshare-model", ] @@ -766,7 +740,7 @@ dependencies = [ "joinery", "lazy_format", "serde", - "thiserror 2.0.12", + "thiserror", "typeshare-model", ] @@ -813,7 +787,7 @@ dependencies = [ "itertools", "joinery", "serde", - "thiserror 2.0.12", + "thiserror", "typeshare-model", ] @@ -821,6 +795,7 @@ dependencies = [ name = "typeshare2-cli" version = "2.0.0" dependencies = [ + "log", "typeshare-driver", "typeshare-kotlin", "typeshare-swift", diff --git a/Cargo.toml b/Cargo.toml index b4c8b949..e1bd22ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,18 @@ [workspace] members = [ - "lib/annotation", - "lib", + "lib/annotation", + "lib", - "app/langs/swift", - "app/langs/typescript", - "app/langs/kotlin", + "app/langs/swift", + "app/langs/typescript", + "app/langs/kotlin", - "app/model", - "app/engine", - "app/driver", - "app/cli", + "app/model", + "app/engine", + "app/driver", + "app/cli", - "typeshare-snapshot-test", + "typeshare-snapshot-test", ] resolver = "2" @@ -27,10 +27,10 @@ ci = ["github"] installers = ["shell", "powershell"] # Target platforms to build apps for (Rust target-triple syntax) targets = [ - "x86_64-unknown-linux-gnu", - "x86_64-apple-darwin", - "x86_64-pc-windows-msvc", - "aarch64-apple-darwin", + "x86_64-unknown-linux-gnu", + "x86_64-apple-darwin", + "x86_64-pc-windows-msvc", + "aarch64-apple-darwin", ] [profile.test] @@ -49,4 +49,4 @@ inherits = "release" [workspace.dependencies] log = "0.4" -flexi_logger = "0.28" +flexi_logger = "0.31" diff --git a/app/cli/Cargo.toml b/app/cli/Cargo.toml index f4c5330e..ce81f076 100644 --- a/app/cli/Cargo.toml +++ b/app/cli/Cargo.toml @@ -8,6 +8,7 @@ typeshare-typescript = { path = "../langs/typescript" } typeshare-kotlin = { path = "../langs/kotlin" } typeshare-swift = { path = "../langs/swift" } typeshare-driver = { path = "../driver" } +log.workspace = true [[bin]] name = "typeshare2" diff --git a/app/driver/Cargo.toml b/app/driver/Cargo.toml index bb934f34..acff3e2a 100644 --- a/app/driver/Cargo.toml +++ b/app/driver/Cargo.toml @@ -10,5 +10,6 @@ readme = "README.md" [dependencies] anyhow = "1.0.96" +log.workspace = true typeshare-engine = { version = "0.4.0", path = "../engine" } diff --git a/app/driver/src/lib.rs b/app/driver/src/lib.rs index 74d1b2b1..3166c7ab 100644 --- a/app/driver/src/lib.rs +++ b/app/driver/src/lib.rs @@ -64,7 +64,7 @@ details and an example. #[macro_export] macro_rules! typeshare_binary { ($($Language:ident $(< $config:lifetime >)?),+ $(,)?) => { - fn main() -> $crate::ඞ::anyhow::Result<()> { + fn main() { struct Local; impl $crate::ඞ::engine::driver::LanguageHelper for Local { @@ -73,11 +73,14 @@ macro_rules! typeshare_binary { )+); } - $crate::ඞ::engine::driver::main_body::( + if let Err(err) = $crate::ඞ::engine::driver::main_body::( $crate::ඞ::engine::args::PersonalizeClap::new() .name(env!("CARGO_PKG_NAME")) .version(env!("CARGO_PKG_VERSION")), - ) + ) { + log::error!("Typeshare failed: {err}"); + log::error!("{}", err.root_cause()); + } } }; } diff --git a/app/engine/src/driver.rs b/app/engine/src/driver.rs index f013dae0..130bb9d7 100644 --- a/app/engine/src/driver.rs +++ b/app/engine/src/driver.rs @@ -1,15 +1,4 @@ //! Program runner. -use std::{collections::HashMap, io}; - -use anyhow::Context as _; -use clap::{CommandFactory as _, FromArgMatches as _}; -use clap_complete::generate as generate_completions; -use flexi_logger::AdaptiveFormat; -use ignore::{overrides::OverrideBuilder, types::TypesBuilder, WalkBuilder}; -use itertools::Itertools; -use lazy_format::lazy_format; -use typeshare_model::prelude::{CrateName, FilesMode, Language}; - use crate::{ args::{ self, add_lang_argument, add_language_params_to_clap, add_personalizations, Command, @@ -21,6 +10,16 @@ use crate::{ parser::{parse_input, parser_inputs, ParsedData}, writer::write_output, }; +use anyhow::Context as _; +use clap::{CommandFactory as _, FromArgMatches as _}; +use clap_complete::generate as generate_completions; +use flexi_logger::AdaptiveFormat; +use ignore::{overrides::OverrideBuilder, types::TypesBuilder, WalkBuilder}; +use itertools::Itertools; +use lazy_format::lazy_format; +use log::{debug, info}; +use std::{collections::HashMap, io}; +use typeshare_model::prelude::{CrateName, FilesMode, Language}; /// Language set. pub trait LanguageSet<'config> { @@ -166,6 +165,7 @@ fn execute_typeshare_for_language<'config, 'a, L: Language<'config>>( write_output(&language_implementation, data, destination) .with_context(|| format!("failed to generate typeshared code for language {name}"))?; + info!("Finished writing generated types"); Ok(()) } @@ -192,8 +192,8 @@ where Helper: LanguageHelper, { flexi_logger::Logger::try_with_env_or_str("info")? - .adaptive_format_for_stderr(AdaptiveFormat::Opt) - .adaptive_format_for_stdout(AdaptiveFormat::Opt) + .adaptive_format_for_stderr(AdaptiveFormat::Default) + .adaptive_format_for_stdout(AdaptiveFormat::Default) .start()?; let language_metas = Helper::LanguageSet::compute_language_metas()?; @@ -235,7 +235,17 @@ where .or_else(|| config.global_config().target_os.as_ref()) .map(|targets| targets.iter().map(|target| target.as_str()).collect_vec()); - eprintln!("TARGET {target_os:?}"); + debug!("TARGET {target_os:?}"); + + info!( + "Running typeshare using directories: \"{}\"", + standard_args + .directories + .as_slice() + .iter() + .map(|p| p.to_string_lossy()) + .join(",") + ); // Construct the directory walker that will produce the list of // files to typeshare @@ -292,6 +302,11 @@ where }) .context("error parsing input files")?; + info!( + "Parsed {} typeshare types", + data.iter().map(|d| d.1.total_parsed_types()).sum::() + ); + let destination = standard_args.output.location(); let language: &String = args diff --git a/app/engine/src/parser.rs b/app/engine/src/parser.rs index 26087ad1..7beb93da 100644 --- a/app/engine/src/parser.rs +++ b/app/engine/src/parser.rs @@ -112,6 +112,11 @@ impl ParsedData { self.consts .sort_unstable_by(|lhs, rhs| Ord::cmp(&lhs.id.original, &rhs.id.original)); } + + /// Get the total amount of parsed types + pub fn total_parsed_types(&self) -> usize { + self.aliases.len() + self.consts.len() + self.structs.len() + self.enums.len() + } } /// Input data for parsing each source file. diff --git a/app/engine/src/visitors.rs b/app/engine/src/visitors.rs index 3b38b6bb..62d3ffb6 100644 --- a/app/engine/src/visitors.rs +++ b/app/engine/src/visitors.rs @@ -1,13 +1,4 @@ //! Visitors to collect various items from the AST. - -use std::collections::HashSet; -use std::{iter, mem}; -use syn::{visit::Visit, Attribute, ItemUse, UseTree}; -use typeshare_model::{ - parsed_data::ImportedType, - prelude::{CrateName, FilesMode, RustEnum, RustEnumVariant, RustType, TypeName}, -}; - use crate::{ parser::{ self, has_typeshare_annotation, parse_const, parse_enum, parse_struct, parse_type_alias, @@ -16,6 +7,12 @@ use crate::{ type_parser::type_name, ParseError, ParseErrorSet, }; +use std::{collections::HashSet, iter, mem}; +use syn::{visit::Visit, Attribute, ItemUse, UseTree}; +use typeshare_model::{ + parsed_data::ImportedType, + prelude::{CrateName, FilesMode, RustEnum, RustEnumVariant, RustType, TypeName}, +}; /// List of some popular crate names that we can ignore /// during import parsing. diff --git a/app/engine/src/writer.rs b/app/engine/src/writer.rs index 93aaf6e9..d39512c7 100644 --- a/app/engine/src/writer.rs +++ b/app/engine/src/writer.rs @@ -1,16 +1,15 @@ //! Single or multi-file writer for generated typeshared types. +use crate::{args::OutputLocation, parser::ParsedData, topsort::topsort}; +use anyhow::Context; +use itertools::Itertools; +use log::{info, warn}; use std::{ collections::{BTreeMap, BTreeSet, HashMap, HashSet}, fs, path::{Path, PathBuf}, }; - -use anyhow::Context; -use itertools::Itertools; use typeshare_model::prelude::*; -use crate::{args::OutputLocation, parser::ParsedData, topsort::topsort}; - /// Writes the generated typeshared types to the file system. pub fn write_output<'c>( lang: &impl Language<'c>, @@ -117,7 +116,7 @@ fn check_write_file(outfile: &PathBuf, output: Vec) -> anyhow::Result<()> { // avoid writing the file to leave the mtime intact // for tools which might use it to know when to // rebuild. - eprintln!("Skipping writing to {outfile:?} no changes"); + info!("Skipping writing to {outfile:?} no changes"); return Ok(()); } _ => {} @@ -133,6 +132,7 @@ fn check_write_file(outfile: &PathBuf, output: Vec) -> anyhow::Result<()> { } fs::write(outfile, output).context("failed to write output")?; + info!("Wrote to {}", outfile.to_string_lossy()); } Ok(()) } @@ -224,7 +224,7 @@ fn generate_types<'c>( .context("error writing file trailer") } -/// Lookup any refeferences to other typeshared types in order to build +/// Lookup any references to other typeshared types in order to build /// a list of imports for the generated module. fn used_imports<'a, 'b: 'a>( data: &'b ParsedData, @@ -247,7 +247,7 @@ fn used_imports<'a, 'b: 'a>( }) .next() { - println!("Warning: Using {crate_name} as module for {ty} which is not in referenced crate {}", referenced_import.base_crate); + warn!("Warning: Using {crate_name} as module for {ty} which is not in referenced crate {}", referenced_import.base_crate); used.entry(crate_name).or_default().insert(ty); } else { // println!("Could not lookup reference {referenced_import:?}"); From 2b5e06a7de26389dc5b0c438c935776f9acdaba1 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Sat, 25 Oct 2025 12:50:30 -0400 Subject: [PATCH 10/14] Update run-snapshot-tests.sh --- Cargo.toml | 29 ++++++++++++++------------- app/driver/Cargo.toml | 2 +- app/engine/Cargo.toml | 2 +- app/langs/kotlin/Cargo.toml | 2 +- app/langs/swift/Cargo.toml | 2 +- app/langs/typescript/Cargo.toml | 2 +- app/model/Cargo.toml | 2 +- run-snapshot-tests.sh | 31 ++++++++++++++++++++++++----- typeshare-snapshot-test/Cargo.toml | 2 +- typeshare-snapshot-test/src/main.rs | 24 +++++++++++----------- 10 files changed, 60 insertions(+), 38 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e1bd22ac..0c7e3d8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,18 @@ [workspace] members = [ - "lib/annotation", - "lib", + "lib/annotation", + "lib", - "app/langs/swift", - "app/langs/typescript", - "app/langs/kotlin", + "app/langs/swift", + "app/langs/typescript", + "app/langs/kotlin", - "app/model", - "app/engine", - "app/driver", - "app/cli", + "app/model", + "app/engine", + "app/driver", + "app/cli", - "typeshare-snapshot-test", + "typeshare-snapshot-test", ] resolver = "2" @@ -27,10 +27,10 @@ ci = ["github"] installers = ["shell", "powershell"] # Target platforms to build apps for (Rust target-triple syntax) targets = [ - "x86_64-unknown-linux-gnu", - "x86_64-apple-darwin", - "x86_64-pc-windows-msvc", - "aarch64-apple-darwin", + "x86_64-unknown-linux-gnu", + "x86_64-apple-darwin", + "x86_64-pc-windows-msvc", + "aarch64-apple-darwin", ] [profile.test] @@ -50,3 +50,4 @@ inherits = "release" [workspace.dependencies] log = "0.4" flexi_logger = "0.31" +anyhow = "1" diff --git a/app/driver/Cargo.toml b/app/driver/Cargo.toml index acff3e2a..bd92a581 100644 --- a/app/driver/Cargo.toml +++ b/app/driver/Cargo.toml @@ -9,7 +9,7 @@ repository = "https://github.com/1Password/typeshare" readme = "README.md" [dependencies] -anyhow = "1.0.96" +anyhow.workspace = true log.workspace = true typeshare-engine = { version = "0.4.0", path = "../engine" } diff --git a/app/engine/Cargo.toml b/app/engine/Cargo.toml index 0b25c253..d0754ee2 100644 --- a/app/engine/Cargo.toml +++ b/app/engine/Cargo.toml @@ -9,7 +9,7 @@ repository = "https://github.com/1Password/typeshare" readme = "README.md" [dependencies] -anyhow = "1.0.95" +anyhow.workspace = true clap = { version = "4.5", features = ["std", "derive", "string"] } clap_complete = "4.5.32" ignore = "0.4.23" diff --git a/app/langs/kotlin/Cargo.toml b/app/langs/kotlin/Cargo.toml index e63e6349..6d922280 100644 --- a/app/langs/kotlin/Cargo.toml +++ b/app/langs/kotlin/Cargo.toml @@ -4,7 +4,7 @@ version = "2.0.0" edition = "2021" [dependencies] -anyhow = "1.0.95" +anyhow.workspace = true indent_write = "2.2.0" itertools = { version = "0.14.0", default-features = false } joinery = "3.1.0" diff --git a/app/langs/swift/Cargo.toml b/app/langs/swift/Cargo.toml index 1b19d12b..f25e8105 100644 --- a/app/langs/swift/Cargo.toml +++ b/app/langs/swift/Cargo.toml @@ -4,7 +4,7 @@ version = "2.0.0" edition = "2024" [dependencies] -anyhow = "1.0.98" +anyhow.workspace = true indent_write = "2.2.0" itertools = { version = "0.14.0" } joinery = "3.1.0" diff --git a/app/langs/typescript/Cargo.toml b/app/langs/typescript/Cargo.toml index 11a84aba..a06bd5e5 100644 --- a/app/langs/typescript/Cargo.toml +++ b/app/langs/typescript/Cargo.toml @@ -4,7 +4,7 @@ version = "2.0.0" edition = "2021" [dependencies] -anyhow = "1.0.95" +anyhow.workspace = true itertools = "0.14.0" joinery = "3.1.0" serde = { version = "1.0.217", features = ["derive"] } diff --git a/app/model/Cargo.toml b/app/model/Cargo.toml index a7cb2e92..1f6ede6a 100644 --- a/app/model/Cargo.toml +++ b/app/model/Cargo.toml @@ -9,6 +9,6 @@ repository = "https://github.com/1Password/typeshare" readme = "README.md" [dependencies] -anyhow = { version = "1.0.95", default-features = false } +anyhow.workspace = true itertools = "0.14.0" serde = { version = "1.0.217", default-features = false } diff --git a/run-snapshot-tests.sh b/run-snapshot-tests.sh index 10be41be..cfa1bb7e 100755 --- a/run-snapshot-tests.sh +++ b/run-snapshot-tests.sh @@ -1,12 +1,33 @@ #!/bin/bash +# This bash script will run all our snapshot tests using the +# new snapshot test runner. The test runner only runs for a +# single output language so this script loops through all the +# supported languages. The test runner requires a pre-built +# typeshare binary to run, so this script starts by building +# a release profile of the binary. + +# Test runner. TEST="cargo run --release --bin typeshare-snapshot-test --" +# Location of our snapshot tests. TEST_FOLDER="app/cli/snapshot-tests" +# Precompiled typeshare binary TYPESHARE="target/release/typeshare2" -declare -A languages=(["swift"]=".swift" ["typescript"]=".ts" ["kotlin"]=".kt") +# Associative array of languages and filename extensions for each +# test runner iteration. +declare -A languages=( + ["swift"]=".swift" + ["typescript"]=".ts" + ["kotlin"]=".kt" +) -cargo build --release --all-targets && \ -for lang in "@{!languages[@]}" -do - echo "Running $lang tests" +cargo build --release --all-targets --bin typeshare2 && \ +for lang in "${!languages[@]}"; do + printf "Running snapshot tests for language %s\n" "$lang" $TEST -t $TYPESHARE --language "$lang" --mode test --suffix "${languages[$lang]}" $TEST_FOLDER + # Break on first failure and return the failed status to the caller + status=$? + if [ $status -ne 0 ]; then + printf "Test failed\n" + exit $status + fi done diff --git a/typeshare-snapshot-test/Cargo.toml b/typeshare-snapshot-test/Cargo.toml index e87a1340..0be01b63 100644 --- a/typeshare-snapshot-test/Cargo.toml +++ b/typeshare-snapshot-test/Cargo.toml @@ -9,7 +9,7 @@ repository = "https://github.com/1Password/typeshare" readme = "README.md" [dependencies] -anyhow = "1.0.97" +anyhow.workspace = true clap = { version = "4.5.35", features = ["derive", "unicode", "wrap_help"] } indent_write = "2.2.0" lazy_format = "2.0.3" diff --git a/typeshare-snapshot-test/src/main.rs b/typeshare-snapshot-test/src/main.rs index 50a1fda4..d46c262a 100644 --- a/typeshare-snapshot-test/src/main.rs +++ b/typeshare-snapshot-test/src/main.rs @@ -2,7 +2,16 @@ mod config; mod sorted_iter; +use crate::{ + config::read_toml, + sorted_iter::{EitherOrBoth, SortedPairsIter}, +}; +use anyhow::Context; +use clap::Parser; use core::str; +use indent_write::{indentable::Indentable, io::IndentWriter}; +use lazy_format::lazy_format; +use similar::TextDiff; use std::{ borrow::Cow, collections::{BTreeMap, BTreeSet, HashSet}, @@ -15,15 +24,6 @@ use std::{ thread, }; -use anyhow::Context; -use clap::Parser; -use indent_write::{indentable::Indentable, io::IndentWriter}; -use lazy_format::lazy_format; -use similar::TextDiff; - -use crate::config::read_toml; -use crate::sorted_iter::{EitherOrBoth, SortedPairsIter}; - /** Utility for capturing and running snapshot tests for your implementation of typeshare. See the README.md for a tutorial on how to use this; this usage @@ -63,7 +63,7 @@ struct Args { /// identified by the name of that directory. snapshots: PathBuf, - /// name or path to your typeshare binary. If ommitted, we assume the + /// name or path to your typeshare binary. If omitted, we assume the /// binary is called `typeshare-{LANGUAGE}`. #[arg(short, long)] typeshare: Option, @@ -173,7 +173,7 @@ enum Report { /// Usually Warning is preferable. Skip, - /// Something intereting happened that we should tell the user about, but + /// Something interesting happened that we should tell the user about, but /// not enough to cause a nonzero exit Warning { // Feel free to switch this to a String or Cow if need be @@ -238,7 +238,7 @@ impl Report { fn print_report(&self, name: &str, dest: &mut impl io::Write) -> io::Result<()> { match *self { Report::Success | Report::Skip => Ok(()), - Report::Warning { ref message } => writeln!(dest, "warning from {name}: {message}\n"), + Report::Warning { ref message } => writeln!(dest, "warning from {name}: {message}"), Report::CommandError { ref command, ref stdout, From a59412b50e4ef364dc6b2d57106907e14cd14f08 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Sat, 25 Oct 2025 16:29:28 -0400 Subject: [PATCH 11/14] Update parser.rs --- app/engine/src/iter_util.rs | 22 ++++++++++++++++++++++ app/engine/src/lib.rs | 7 ++++--- app/engine/src/parser.rs | 26 +++++++++++++++++--------- app/engine/src/serde/args.rs | 3 +-- app/engine/src/serde/config.rs | 9 ++++----- app/engine/src/serde/empty.rs | 1 + app/engine/src/serde/mod.rs | 1 + app/engine/src/serde/toml.rs | 4 ++-- app/engine/src/target_os.rs | 1 + app/engine/src/topsort.rs | 5 ++--- app/langs/swift/src/lib.rs | 2 +- app/model/src/language.rs | 16 ++++++++-------- run-snapshot-tests.sh | 2 ++ 13 files changed, 66 insertions(+), 33 deletions(-) create mode 100644 app/engine/src/iter_util.rs diff --git a/app/engine/src/iter_util.rs b/app/engine/src/iter_util.rs new file mode 100644 index 00000000..d47215d9 --- /dev/null +++ b/app/engine/src/iter_util.rs @@ -0,0 +1,22 @@ +//! Some extra iterator methods. + +/// Iterator extension trait. +pub trait IterExt: Iterator { + /// Fallible version of `any()`. Short circuits + /// on true or error. + #[inline] + fn try_any(&mut self, mut f: F) -> Result + where + Self: Sized, + F: FnMut(Self::Item) -> Result, + { + for x in self.by_ref() { + if f(x)? { + return Ok(true); + } + } + Ok(false) + } +} + +impl IterExt for std::slice::Iter<'_, T> {} diff --git a/app/engine/src/lib.rs b/app/engine/src/lib.rs index 0374c278..03789888 100644 --- a/app/engine/src/lib.rs +++ b/app/engine/src/lib.rs @@ -2,6 +2,7 @@ pub mod args; pub mod config; pub mod driver; +mod iter_util; pub mod parser; mod rename; mod serde; @@ -63,7 +64,7 @@ impl Display for FileParseErrors { #[non_exhaustive] pub enum FileErrorKind { /// We couldn't figure which crate this file belongs to, which we need in - /// mutli-file mode + /// multi-file mode UnknownCrate, /// There were parse errors @@ -76,13 +77,13 @@ pub enum FileErrorKind { impl Display for FileErrorKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - FileErrorKind::UnknownCrate => f.write_str("unknown crate in mutli-file mode"), + FileErrorKind::UnknownCrate => f.write_str("unknown crate in multi-file mode"), FileErrorKind::ParseErrors(parse_error_set) => parse_error_set.fmt(f), FileErrorKind::ReadError(error) => write!(f, "i/o error: {error}"), } } } -/// A group of parse errors from a single file. Guaranteed to be non-emtpy. +/// A group of parse errors from a single file. Guaranteed to be non-empty. #[derive(Debug)] pub struct ParseErrorSet { errors: Vec, diff --git a/app/engine/src/parser.rs b/app/engine/src/parser.rs index 7beb93da..54bce486 100644 --- a/app/engine/src/parser.rs +++ b/app/engine/src/parser.rs @@ -1,5 +1,6 @@ //! Source file parsing. use crate::{ + iter_util::IterExt as _, rename::RenameExt, target_os, type_parser::{parse_rust_type, parse_rust_type_from_string, type_name}, @@ -278,7 +279,7 @@ pub fn parse_input( ) } -/// Check if we have not parsed any relavent typehsared types. +/// Check if we have not parsed any relevant typeshared types. fn is_parsed_data_empty(parsed_data: &ParsedData) -> bool { parsed_data.enums.is_empty() && parsed_data.aliases.is_empty() @@ -474,14 +475,21 @@ pub(crate) fn parse_enum(e: &ItemEnum, valid_os: Option<&[&str]>) -> Result, _>>()?; // Check if the enum references itself recursively in any of its variants - let is_recursive = variants.iter().any(|v| match v { - RustEnumVariant::Unit(_) => false, - RustEnumVariant::Tuple { ty, .. } => ty.contains_type(&original_enum_ident), - RustEnumVariant::AnonymousStruct { fields, .. } => fields - .iter() - .any(|f| f.ty.contains_type(&original_enum_ident)), - _ => panic!("unrecgonized enum type"), - }); + let is_recursive = variants.iter().try_any(|v| { + Ok(match v { + RustEnumVariant::Unit(_) => false, + RustEnumVariant::Tuple { ty, .. } => ty.contains_type(&original_enum_ident), + RustEnumVariant::AnonymousStruct { fields, .. } => fields + .iter() + .any(|f| f.ty.contains_type(&original_enum_ident)), + _ => { + return Err(ParseError::new( + &e, + ParseErrorKind::UnsupportedType("Unsupported enum type".into()), + )) + } + }) + })?; let shared = RustEnumShared { id: get_ident(Some(&e.ident), &e.attrs, None), diff --git a/app/engine/src/serde/args.rs b/app/engine/src/serde/args.rs index 2dacb97e..bb880e90 100644 --- a/app/engine/src/serde/args.rs +++ b/app/engine/src/serde/args.rs @@ -1,11 +1,10 @@ //! Command line argument parsing. +use serde::ser; use std::{ collections::HashMap, fmt::{self, Display, Write}, }; -use serde::ser; - #[derive(Debug, Clone, Copy)] pub enum ArgType { Bool, diff --git a/app/engine/src/serde/config.rs b/app/engine/src/serde/config.rs index 30cbde7f..33237bea 100644 --- a/app/engine/src/serde/config.rs +++ b/app/engine/src/serde/config.rs @@ -1,13 +1,12 @@ +//! Deserialization for configuration. +use super::args::{ArgSpec, ArgType, CliArgsSet}; +use itertools::Itertools; +use serde::de::{self, value::BorrowedStrDeserializer}; use std::{ ffi::{OsStr, OsString}, str::FromStr, }; -use itertools::Itertools; -use serde::de::{self, value::BorrowedStrDeserializer}; - -use super::args::{ArgSpec, ArgType, CliArgsSet}; - #[derive(Debug, thiserror::Error)] pub enum ConfigDeserializeError { #[error("error from Deserialize type: {0}")] diff --git a/app/engine/src/serde/empty.rs b/app/engine/src/serde/empty.rs index 6460c3a1..b885c202 100644 --- a/app/engine/src/serde/empty.rs +++ b/app/engine/src/serde/empty.rs @@ -1,3 +1,4 @@ +//! Deserialization for empty use serde::de; use thiserror::Error; diff --git a/app/engine/src/serde/mod.rs b/app/engine/src/serde/mod.rs index d488700f..6cde6518 100644 --- a/app/engine/src/serde/mod.rs +++ b/app/engine/src/serde/mod.rs @@ -1,3 +1,4 @@ +//! Various serde deserializers pub mod args; pub mod config; pub mod empty; diff --git a/app/engine/src/serde/toml.rs b/app/engine/src/serde/toml.rs index d6b4f0cb..40fcdd9e 100644 --- a/app/engine/src/serde/toml.rs +++ b/app/engine/src/serde/toml.rs @@ -1,6 +1,6 @@ -use std::{marker::PhantomData, str}; - +//! Toml deserialization. use serde::de; +use std::{marker::PhantomData, str}; /// Borrowing deserializer for a `toml::Value` pub struct ValueDeserializer<'a, E> { diff --git a/app/engine/src/target_os.rs b/app/engine/src/target_os.rs index f823fd0f..51121db4 100644 --- a/app/engine/src/target_os.rs +++ b/app/engine/src/target_os.rs @@ -1,3 +1,4 @@ +//! Parsing of `target_os` attributes use proc_macro2::{Delimiter, Group, TokenStream}; use syn::{parse::Parser, punctuated::Punctuated, Ident, LitStr, Token}; diff --git a/app/engine/src/topsort.rs b/app/engine/src/topsort.rs index 46c21e86..9b7c1fe9 100644 --- a/app/engine/src/topsort.rs +++ b/app/engine/src/topsort.rs @@ -1,9 +1,8 @@ +//! Top sort of rust types. +use crate::writer::BorrowedRustItem; use std::collections::{HashMap, HashSet}; - use typeshare_model::prelude::*; -use crate::writer::BorrowedRustItem; - fn get_dependencies_from_type<'a>( tp: &'a RustType, types: &HashMap<&'a TypeName, BorrowedRustItem<'a>>, diff --git a/app/langs/swift/src/lib.rs b/app/langs/swift/src/lib.rs index 8ab1975f..28432167 100644 --- a/app/langs/swift/src/lib.rs +++ b/app/langs/swift/src/lib.rs @@ -889,7 +889,7 @@ impl<'config> Language<'config> for Swift<'config> { if self.should_emit_codable_void.load(Ordering::Relaxed) { let mut content = Vec::new(); self.write_codable_void(&mut content) - .expect("write to vec is infallbile"); + .expect("write to vec is infallible"); let path = output_folder.join("Codable.swift"); diff --git a/app/model/src/language.rs b/app/model/src/language.rs index 18729d18..f2bf95e6 100644 --- a/app/model/src/language.rs +++ b/app/model/src/language.rs @@ -122,7 +122,7 @@ name. language.begin_file(&mut file, mode) ``` -5. In mutli-file mode only, we call `write_imports` with a list of all the +5. In multi-file mode only, we call `write_imports` with a list of all the types that are being imported from other typeshare'd crates. This allows the language to emit appropriate import statements for its own language. @@ -131,7 +131,7 @@ language to emit appropriate import statements for its own language. language.write_imports(&mut file, crate_name, computed_imports) ``` -6. For EACE typeshared item in being typeshared, we call `write_enum`, +6. For EACH typeshared item in being typeshared, we call `write_enum`, `write_struct`, `write_type_alias`, or `write_const`, as appropriate. ```ignore @@ -142,12 +142,12 @@ language.write_enum(&mut file, parsed_enum); 6a. In your implementations of these methods, we recommend that you call `format_type` for the fields of these types. `format_type` will in turn call `format_simple_type`, `format_generic_type`, or `format_special_type`, as -appropriate; usually it is only necessary for you to implmenent +appropriate; usually it is only necessary for you to implement `format_special_type` yourself, and use the default implementations for the others. The `format_*` methods will otherwise never be called by typeshare. 6b. If your language doesn't natively support data-containing enums, we -recommand that you call `write_types_for_anonymous_structs` in your +recommend that you call `write_types_for_anonymous_structs` in your implementation of `write_enum`; this will call `write_struct` for each variant of the enum. @@ -173,7 +173,7 @@ algorithms that compute import sets are being rewritten. The API presented here is stable, but output might be buggy while issues with import detection are resolved. -In the future, we hope to make mutli-file mode multithreaded, capable of +In the future, we hope to make multi-file mode multithreaded, capable of writing multiple files concurrently from a shared `Language` instance. `Language` therefore has a `Sync` bound to keep this possibility available. */ @@ -184,7 +184,7 @@ pub trait Language<'config>: Sized + Sync + Debug { `serde`. It is important that this type include `#[serde(default)]` or something - equivelent, so that a config can be loaded with default setting even + equivalent, so that a config can be loaded with default setting even if this language isn't present in the config file. The `serialize` implementation for this type should NOT skip keys, if @@ -330,7 +330,7 @@ pub trait Language<'config>: Sized + Sync + Debug { /** Format a special type. This will handle things like arrays, primitives, - options, and so on. Every lanugage has different spellings for these types, + options, and so on. Every language has different spellings for these types, so this is one of the key methods that a language implementation needs to deal with. */ @@ -561,7 +561,7 @@ pub trait Language<'config>: Sized + Sync + Debug { unconditionally excluded from cross-file import analysis. Usually this will be the types in `mapped_types`, since those are types with special behavior (for instance, a datetime date provided as a standard type by your - langauge). + language). This is mostly a performance optimization. By default it returns `false` for all types. diff --git a/run-snapshot-tests.sh b/run-snapshot-tests.sh index cfa1bb7e..91636c3b 100755 --- a/run-snapshot-tests.sh +++ b/run-snapshot-tests.sh @@ -31,3 +31,5 @@ for lang in "${!languages[@]}"; do exit $status fi done + +printf "All snapshot tests have passed\n" From 3a316bbb0da31df215831ed47e700fcbd1dfbc46 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Mon, 27 Oct 2025 14:06:59 -0400 Subject: [PATCH 12/14] update proc macro to handle field level cfg_attr(, typeshare) --- lib/annotation/src/lib.rs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/annotation/src/lib.rs b/lib/annotation/src/lib.rs index 71d9a0d5..d80389dc 100644 --- a/lib/annotation/src/lib.rs +++ b/lib/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(|x| !is_typeshare_attribute(x)); } fn remove_configuration_from_fields(fields: &mut Fields) { From 60087a423f2976b766c4d678c43b046c85c17fc5 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Mon, 27 Oct 2025 14:32:14 -0400 Subject: [PATCH 13/14] Update run-snapshot-tests.sh --- app/engine/src/parser.rs | 26 ++++++++++++++++++++++++++ run-snapshot-tests.sh | 13 +++++++++++++ 2 files changed, 39 insertions(+) diff --git a/app/engine/src/parser.rs b/app/engine/src/parser.rs index 54bce486..e937e1a7 100644 --- a/app/engine/src/parser.rs +++ b/app/engine/src/parser.rs @@ -1207,4 +1207,30 @@ mod test_get_decorators { assert!(decorators.is_redacted()); } + + #[test] + fn test_field_decorator_cfg_attr() { + let item_struct: ItemStruct = syn::parse_quote! { + #[cfg_attr(feature = "typeshare-support", typeshare)] + pub struct Test { + #[cfg_attr(feature = "typeshare-support", typeshare(serialized_as = "i54"))] + pub field_1: i64 + } + }; + + let RustItem::Struct(rust_struct) = + parse_struct(&item_struct, None).expect("Failed to parse struct") + else { + panic!("Not a struct"); + }; + + let field_decorators = &rust_struct.fields[0].decorators; + + let value = field_decorators + .get_all("serialized_as") + .first() + .expect("No serialized_as decorator"); + + assert_eq!(value, &Value::String("i54".into())); + } } diff --git a/run-snapshot-tests.sh b/run-snapshot-tests.sh index 91636c3b..f8c274f9 100755 --- a/run-snapshot-tests.sh +++ b/run-snapshot-tests.sh @@ -1,4 +1,5 @@ #!/bin/bash + # This bash script will run all our snapshot tests using the # new snapshot test runner. The test runner only runs for a # single output language so this script loops through all the @@ -6,6 +7,18 @@ # typeshare binary to run, so this script starts by building # a release profile of the binary. +# Check bash version. +/bin/bash --version | head -n 1 | awk '{ + if ($4 < "4") { + print "Bash 4+ is required. Your version is", $4; + exit(1); + } +}' +if [ $? -ne 0 ]; then + printf "Not running snapshot tests\n" + exit 1 +fi + # Test runner. TEST="cargo run --release --bin typeshare-snapshot-test --" # Location of our snapshot tests. From 1c42bb69db399fd438854d26d19ef9002e8282de Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Tue, 28 Oct 2025 11:14:20 -0400 Subject: [PATCH 14/14] Use /usr/bin/env to source bash --- run-snapshot-tests.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run-snapshot-tests.sh b/run-snapshot-tests.sh index f8c274f9..5dc8696b 100755 --- a/run-snapshot-tests.sh +++ b/run-snapshot-tests.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # This bash script will run all our snapshot tests using the # new snapshot test runner. The test runner only runs for a @@ -8,7 +8,7 @@ # a release profile of the binary. # Check bash version. -/bin/bash --version | head -n 1 | awk '{ +/usr/bin/env bash --version | head -n 1 | awk '{ if ($4 < "4") { print "Bash 4+ is required. Your version is", $4; exit(1);