diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e9bceb8..f494aec6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: pull_request: branches: - main + - typeshare2 name: CI @@ -24,7 +25,6 @@ jobs: toolchain: ${{ matrix.rust }} - run: rustup run ${{ matrix.rust }} cargo check - test: name: Test runs-on: ubuntu-latest @@ -39,8 +39,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 +60,6 @@ jobs: toolchain: ${{ matrix.rust }} - run: rustup toolchain install ${{ matrix.rust }} - run: just --justfile tests/justfile determinism - fmt: name: Rustfmt @@ -78,7 +77,6 @@ jobs: components: rustfmt - run: rustup run ${{ matrix.rust }} cargo fmt --all -- --check - clippy: name: Clippy runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index e3c0e6f4..dacba07c 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,25 @@ 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.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e5335674a3a259527f97e9176a3767dcc9b220b8e29d643daeb2d6c72caf8b" +dependencies = [ + "chrono", + "log", + "nu-ansi-term", + "regex", + "thiserror", +] + [[package]] name = "globset" version = "0.4.15" @@ -222,6 +295,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 +378,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 +418,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 +480,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 +522,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 +584,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" @@ -553,6 +702,7 @@ name = "typeshare-driver" version = "1.3.0" dependencies = [ "anyhow", + "log", "typeshare-engine", ] @@ -564,10 +714,12 @@ dependencies = [ "clap", "clap_complete", "cool_asserts", + "flexi_logger", "ignore", "indent_write", "itertools", "lazy_format", + "log", "proc-macro2", "quote", "rayon", @@ -643,6 +795,7 @@ dependencies = [ name = "typeshare2-cli" version = "2.0.0" dependencies = [ + "log", "typeshare-driver", "typeshare-kotlin", "typeshare-swift", @@ -683,6 +836,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 +904,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/Cargo.toml b/Cargo.toml index b4c8b949..0c7e3d8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,4 +49,5 @@ inherits = "release" [workspace.dependencies] log = "0.4" -flexi_logger = "0.28" +flexi_logger = "0.31" +anyhow = "1" 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/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..7fbe191e --- /dev/null +++ b/app/cli/snapshot-tests/test_cfg_attr_typeshare/input.rs @@ -0,0 +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 new file mode 100644 index 00000000..10e1303d --- /dev/null +++ b/app/cli/snapshot-tests/test_cfg_attr_typeshare/output.ts @@ -0,0 +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; +} + 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/driver/Cargo.toml b/app/driver/Cargo.toml index bb934f34..bd92a581 100644 --- a/app/driver/Cargo.toml +++ b/app/driver/Cargo.toml @@ -9,6 +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/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/Cargo.toml b/app/engine/Cargo.toml index cdc213ff..d0754ee2 100644 --- a/app/engine/Cargo.toml +++ b/app/engine/Cargo.toml @@ -9,28 +9,37 @@ 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" 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/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 ed3461ee..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,12 +59,12 @@ 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, - config: &T, + _language: &str, + _config: &T, ) -> anyhow::Result<()> { todo!() // self.raw_data.insert( @@ -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 c6826133..130bb9d7 100644 --- a/app/engine/src/driver.rs +++ b/app/engine/src/driver.rs @@ -1,13 +1,4 @@ -use std::{collections::HashMap, io}; - -use anyhow::Context as _; -use clap::{CommandFactory as _, FromArgMatches as _}; -use clap_complete::generate as generate_completions; -use ignore::{overrides::OverrideBuilder, types::TypesBuilder, WalkBuilder}; -use itertools::Itertools; -use lazy_format::lazy_format; -use typeshare_model::prelude::{CrateName, FilesMode, Language}; - +//! Program runner. use crate::{ args::{ self, add_lang_argument, add_language_params_to_clap, add_personalizations, Command, @@ -19,8 +10,20 @@ 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> { + /// Language meta-data. type LanguageMetas: 'static; /// Each language has a set of configuration metadata, describing all @@ -40,6 +43,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, @@ -161,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(()) } @@ -174,16 +179,23 @@ 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, { + flexi_logger::Logger::try_with_env_or_str("info")? + .adaptive_format_for_stderr(AdaptiveFormat::Default) + .adaptive_format_for_stdout(AdaptiveFormat::Default) + .start()?; + let language_metas = Helper::LanguageSet::compute_language_metas()?; let command = StandardArgs::command(); let command = add_personalizations(command, personalizations); @@ -223,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 @@ -280,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/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 c480af35..03789888 100644 --- a/app/engine/src/lib.rs +++ b/app/engine/src/lib.rs @@ -1,6 +1,8 @@ +//! Typeshare parser and writer pub mod args; pub mod config; pub mod driver; +mod iter_util; pub mod parser; mod rename; mod serde; @@ -10,14 +12,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 +29,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, @@ -59,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 @@ -72,19 +77,20 @@ 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, } 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 +125,7 @@ impl Display for ParseErrorSet { } } +/// A file parsing error. #[derive(Debug, Error)] #[error("at {}:{}..{}:{}: {kind}", .start.line, @@ -127,12 +134,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 6661c323..e937e1a7 100644 --- a/app/engine/src/parser.rs +++ b/app/engine/src/parser.rs @@ -1,6 +1,15 @@ //! 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}, + visitors::TypeShareVisitor, + FileParseErrors, ParseError, ParseErrorKind, ParseErrorSet, +}; use ignore::Walk; use itertools::Itertools; +use log::debug; use proc_macro2::{Delimiter, Group}; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use std::{ @@ -15,22 +24,14 @@ 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"; /// An enum that encapsulates units of code generation for Typeshare. /// Analogous to `syn::Item`, even though our variants are more limited. @@ -67,6 +68,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); @@ -75,6 +77,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), @@ -84,6 +87,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); @@ -94,6 +98,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)); @@ -108,6 +113,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. @@ -185,6 +195,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| { @@ -268,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() @@ -285,7 +296,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 +308,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) } @@ -464,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), @@ -690,12 +708,21 @@ 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).any(|item| match item { + Meta::Path(path) => path + .segments + .iter() + .any(|segment| segment.ident == TYPESHARE), + Meta::List(meta_list) => meta_list.path.is_ident(TYPESHARE), + Meta::NameValue(_meta_name_value) => false, + }) + }; attrs .iter() - .flat_map(|attr| attr.path().segments.clone()) - .any(|segment| segment.ident == TYPESHARE) + .any(|attr| attr.path().is_ident(TYPESHARE) || check_cfg_attr(attr)) } pub(crate) fn serde_rename_all(attrs: &[syn::Attribute]) -> Option { @@ -717,7 +744,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) @@ -729,16 +755,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 { @@ -797,7 +823,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"))) }) @@ -806,7 +831,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))) }) } @@ -825,13 +849,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() } @@ -956,24 +994,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]); @@ -982,7 +1010,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]); @@ -996,7 +1024,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]); @@ -1008,10 +1036,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]); @@ -1025,10 +1053,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!( @@ -1044,11 +1072,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); @@ -1061,7 +1089,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); @@ -1077,15 +1105,13 @@ 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); - eprintln!("{decorators:#?}"); - assert_eq!( decorators.type_override_for_lang("swift").unwrap(), "NSString" @@ -1096,4 +1122,115 @@ mod test_get_decorators { ); assert_eq!(decorators.type_override_for_lang("kotlin"), None); } + + #[test] + fn test_cfg_attr() { + let attr: Vec = syn::parse_quote! { + #[cfg_attr(feature = "typeshare-support", typeshare)] + }; + assert!(has_typeshare_annotation(&attr)); + } + + #[test] + fn test_cfg_attr_with_nvps() { + let attrs: Vec = syn::parse_quote! { + #[cfg_attr( + feature = "typeshare-support", + typeshare( + swift = "Equatable, Hashable", + swiftGenericConstraints = "R: Equatable & Hashable" + ) + )] + }; + + assert!(has_typeshare_annotation(&attrs)); + let decorators = get_decorators(&attrs); + eprintln!("{decorators:#?}"); + + assert_eq!( + decorators.get_all("swift"), + &[Value::String("Equatable, Hashable".into())] + ); + + assert_eq!( + decorators.get_all("swiftGenericConstraints"), + &[Value::String("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)); + let decorators = get_decorators(&attrs); + assert!(decorators.is_redacted()); + } + + #[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"); + }; + + assert!(rust_struct.decorators.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); + + assert_eq!( + decorators.get_all("kotlin"), + &[Value::String("JvmInline".into())] + ); + + 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/app/engine/src/serde/args.rs b/app/engine/src/serde/args.rs index f1de5d6b..bb880e90 100644 --- a/app/engine/src/serde/args.rs +++ b/app/engine/src/serde/args.rs @@ -1,10 +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, @@ -26,6 +26,7 @@ pub struct CliArgsSet { } impl CliArgsSet { + /// Iterator of ArgSpec pub fn iter(&self) -> impl Iterator> + '_ { self.args .iter() @@ -36,6 +37,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/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/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 9318c8f7..d39512c7 100644 --- a/app/engine/src/writer.rs +++ b/app/engine/src/writer.rs @@ -1,15 +1,16 @@ +//! 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>, crate_parsed_data: HashMap, ParsedData>, @@ -115,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(()); } _ => {} @@ -131,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(()) } @@ -151,6 +153,7 @@ pub enum BorrowedRustItem<'a> { } impl BorrowedRustItem<'_> { + /// Typename identifier pub fn id(&self) -> &TypeName { &match *self { BorrowedRustItem::Struct(item) => &item.id, @@ -221,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, @@ -244,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:?}"); 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/kotlin/src/lib.rs b/app/langs/kotlin/src/lib.rs index 83e57c3c..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, @@ -146,9 +147,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 +164,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 +210,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 +413,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 +448,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 +520,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/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/swift/src/lib.rs b/app/langs/swift/src/lib.rs index c0a83a48..28432167 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, @@ -612,9 +613,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 +648,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 +691,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 +729,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 +800,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 )?; @@ -878,14 +889,14 @@ 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"); - 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/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/langs/typescript/src/lib.rs b/app/langs/typescript/src/lib.rs index 2a1b3cd2..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, } @@ -162,14 +168,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 +190,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 +209,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 +314,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/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/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 b1038adf..f2bf95e6 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 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] 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,11 +29,13 @@ impl FilesMode { } } + /// Is multi-file mode enabled. pub fn is_multi(&self) -> bool { matches!(*self, Self::Multi(_)) } } +#[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 +66,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 +85,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)?; @@ -119,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. @@ -128,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 @@ -139,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. @@ -170,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. */ @@ -181,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 @@ -327,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. */ @@ -558,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/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 9a97977c..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) } @@ -30,7 +32,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 +152,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`. @@ -508,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/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) { diff --git a/run-snapshot-tests.sh b/run-snapshot-tests.sh new file mode 100755 index 00000000..5dc8696b --- /dev/null +++ b/run-snapshot-tests.sh @@ -0,0 +1,48 @@ +#!/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 +# 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. + +# Check bash version. +/usr/bin/env 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. +TEST_FOLDER="app/cli/snapshot-tests" +# Precompiled typeshare binary +TYPESHARE="target/release/typeshare2" +# 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 --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 + +printf "All snapshot tests have passed\n" 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/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 96b0986f..d46c262a 100644 --- a/typeshare-snapshot-test/src/main.rs +++ b/typeshare-snapshot-test/src/main.rs @@ -1,7 +1,17 @@ +//! Binary to generate or run snapshot tests. 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}, @@ -14,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 @@ -62,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, @@ -172,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 @@ -237,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, @@ -457,6 +458,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, @@ -714,10 +716,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(|| { 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 {