Repository: 1Password/typeshare (rust)

Issue:
Typeshare ignores type annotations in embedded modules
Hi,

I have code like this:

```rust
mod my_module {
    // Types with Typeshare annotations
}
```

...which results in nothing being generated.

If I remove the module around the annotations, everything works.

Pull request description:
Typeshare can now detect types within embedded modules
Say, for instance, you have this situation:

```
mod inline_module {
  #[typeshare]
  pub struct Inline;
}
```

Previously, `Inline` would not be detected since our parser was not scanning the contents of inline/embedded modules. This has been fixed.

Resolves #40 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8857a4a..edee0a9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,22 +1,3 @@
-# Version 1.5.0
-
-This release brings support for fixed-length arrays and fixes some premature changes made in 1.4.0 involving the
-representation of unit types and enum variants in Typescript.
-
-* `typeshare-core`
-  * Fixed-length arrays in the form of `[T; N]` are now supported.
-  * Reverted changes made to the representation of unit types in Typescript.<sup>1</sup>
-
-<sup>1</sup> We apologize for the premature changes to the representation of unit types. This was done to improve
-correctness and compatibility with various JSON libraries, however it ended up causing a regression by invalidating
-certain usages. For now, we've reverted the changes so your projects can get back on track, and we are working to bring
-these improvements while mitigating the issues discovered. Thank you for bringing this to our attention.
-
-### Community contributors
-
-Thank you to the following community contributors for your work on this release:
-* [ccouzens](https://github.com/ccouzens)
-
 # Version 1.4.0
 
 This release brings topological sorting of types based on dependencies to generated files, as well as fixes several bugs.
diff --git a/Cargo.lock b/Cargo.lock
index b0a526c..b9f8779 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -601,7 +601,7 @@ dependencies = [
 
 [[package]]
 name = "typeshare-cli"
-version = "1.5.0"
+version = "1.4.0"
 dependencies = [
  "clap",
  "clap_complete_command",
@@ -615,7 +615,7 @@ dependencies = [
 
 [[package]]
 name = "typeshare-core"
-version = "1.5.0"
+version = "1.4.0"
 dependencies = [
  "anyhow",
  "expect-test",
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index e096873..1b466a7 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "typeshare-cli"
-version = "1.5.0"
+version = "1.4.0"
 rust-version = "1.57"
 edition = "2021"
 description = "Command Line Tool for generating language files with typeshare"
diff --git a/core/Cargo.toml b/core/Cargo.toml
index e57e241..39d10cb 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "typeshare-core"
-version = "1.5.0"
+version = "1.4.0"
 rust-version = "1.57"
 license = "MIT OR Apache-2.0"
 edition = "2021"
diff --git a/core/src/language/go.rs b/core/src/language/go.rs
index 9cd3a9d..a5936d4 100644
--- a/core/src/language/go.rs
+++ b/core/src/language/go.rs
@@ -81,7 +81,7 @@ impl Language for Go {
         Ok(match special_ty {
             SpecialRustType::Vec(rtype) => format!("[]{}", self.format_type(rtype, generic_types)?),
             SpecialRustType::Array(rtype, len) => {
-                format!("[{}]{}", len, self.format_type(rtype, generic_types)?)
+                format!("[{len}]{}", self.format_type(rtype, generic_types)?)
             }
             SpecialRustType::Option(rtype) => {
                 format!("*{}", self.format_type(rtype, generic_types)?)
diff --git a/core/src/language/typescript.rs b/core/src/language/typescript.rs
index 32bf7ae..74879d7 100644
--- a/core/src/language/typescript.rs
+++ b/core/src/language/typescript.rs
@@ -52,7 +52,7 @@ impl Language for TypeScript {
                 },
                 self.format_type(rtype2, generic_types)?
             )),
-            SpecialRustType::Unit => Ok("undefined".into()),
+            SpecialRustType::Unit => Ok("null".into()),
             SpecialRustType::String => Ok("string".into()),
             SpecialRustType::I8
             | SpecialRustType::U8
@@ -188,7 +188,7 @@ impl TypeScript {
                 match v {
                     RustEnumVariant::Unit(shared) => write!(
                         w,
-                        "\t| {{ {}: {:?}, {}?: undefined }}",
+                        "\t| {{ {}: {:?}, {}: null }}",
                         tag_key, shared.id.renamed, content_key
                     ),
                     RustEnumVariant::Tuple { ty, shared } => {
diff --git a/core/src/parser.rs b/core/src/parser.rs
index d3e6bf6..971f0d2 100644
--- a/core/src/parser.rs
+++ b/core/src/parser.rs
@@ -88,7 +88,8 @@ pub fn parse(input: &str) -> Result<ParsedData, ParseError> {
     // Parse and process the input, ensuring we parse only items marked with
     // `#[typeshare]
     let source = syn::parse_file(input)?;
-    for item in &source.items {
+
+    for item in flatten_items(source.items.iter()) {
         match item {
             syn::Item::Struct(s) if has_typeshare_annotation(&s.attrs) => {
                 parsed_data.push_rust_thing(parse_struct(s)?);
@@ -106,6 +107,23 @@ pub fn parse(input: &str) -> Result<ParsedData, ParseError> {
     Ok(parsed_data)
 }
 
+/// Given an iterator over items, will return an iterator that flattens the contents of embedded
+/// module items into the iterator.
+fn flatten_items<'a>(
+    items: impl Iterator<Item = &'a syn::Item>,
+) -> impl Iterator<Item = &'a syn::Item> {
+    items.flat_map(|item| {
+        match item {
+            syn::Item::Mod(syn::ItemMod {
+                content: Some((_, items)),
+                ..
+            }) => flatten_items(items.iter()).collect(),
+            item => vec![item],
+        }
+        .into_iter()
+    })
+}
+
 /// Parses a struct into a definition that more succinctly represents what
 /// typeshare needs to generate code for other languages.
 ///


Test patch:
diff --git a/core/data/tests/boxed_value/output.ts b/core/data/tests/boxed_value/output.ts
index 7c8a223..0b45e70 100644
--- a/core/data/tests/boxed_value/output.ts
+++ b/core/data/tests/boxed_value/output.ts
@@ -1,6 +1,6 @@
 /** This is a comment. */
 export type Colors = 
-	| { type: "Red", content?: undefined }
-	| { type: "Blue", content?: undefined }
+	| { type: "Red", content: null }
+	| { type: "Blue", content: null }
 	| { type: "Green", content: string };
 
diff --git a/core/data/tests/can_generate_algebraic_enum_with_skipped_variants/output.ts b/core/data/tests/can_generate_algebraic_enum_with_skipped_variants/output.ts
index 3b48f75..0ef9293 100644
--- a/core/data/tests/can_generate_algebraic_enum_with_skipped_variants/output.ts
+++ b/core/data/tests/can_generate_algebraic_enum_with_skipped_variants/output.ts
@@ -1,4 +1,4 @@
 export type SomeEnum = 
-	| { type: "A", content?: undefined }
+	| { type: "A", content: null }
 	| { type: "C", content: number };
 
diff --git a/core/data/tests/can_generate_empty_algebraic_enum/output.ts b/core/data/tests/can_generate_empty_algebraic_enum/output.ts
index 10b3ce7..3d2273f 100644
--- a/core/data/tests/can_generate_empty_algebraic_enum/output.ts
+++ b/core/data/tests/can_generate_empty_algebraic_enum/output.ts
@@ -3,5 +3,5 @@ export interface AddressDetails {
 
 export type Address = 
 	| { type: "FixedAddress", content: AddressDetails }
-	| { type: "NoFixedAddress", content?: undefined };
+	| { type: "NoFixedAddress", content: null };
 
diff --git a/core/data/tests/can_generate_generic_struct/output.ts b/core/data/tests/can_generate_generic_struct/output.ts
index 38a129b..0b6bc2c 100644
--- a/core/data/tests/can_generate_generic_struct/output.ts
+++ b/core/data/tests/can_generate_generic_struct/output.ts
@@ -13,5 +13,5 @@ export type EnumUsingGenericStruct =
 	| { type: "VariantA", content: GenericStruct<string, number> }
 	| { type: "VariantB", content: GenericStruct<string, number> }
 	| { type: "VariantC", content: GenericStruct<string, boolean> }
-	| { type: "VariantD", content: GenericStructUsingGenericStruct<undefined> };
+	| { type: "VariantD", content: GenericStructUsingGenericStruct<null> };
 
diff --git a/core/data/tests/can_handle_anonymous_struct/output.ts b/core/data/tests/can_handle_anonymous_struct/output.ts
index 4417ec5..b33267f 100644
--- a/core/data/tests/can_handle_anonymous_struct/output.ts
+++ b/core/data/tests/can_handle_anonymous_struct/output.ts
@@ -15,13 +15,13 @@ export type AutofilledBy =
 
 /** This is a comment (yareek sameek wuz here) */
 export type EnumWithManyVariants = 
-	| { type: "UnitVariant", content?: undefined }
+	| { type: "UnitVariant", content: null }
 	| { type: "TupleVariantString", content: string }
 	| { type: "AnonVariant", content: {
 	uuid: string;
 }}
 	| { type: "TupleVariantInt", content: number }
-	| { type: "AnotherUnitVariant", content?: undefined }
+	| { type: "AnotherUnitVariant", content: null }
 	| { type: "AnotherAnonVariant", content: {
 	uuid: string;
 	thing: number;
diff --git a/core/data/tests/can_handle_unit_type/output.ts b/core/data/tests/can_handle_unit_type/output.ts
index 430935d..d7508d1 100644
--- a/core/data/tests/can_handle_unit_type/output.ts
+++ b/core/data/tests/can_handle_unit_type/output.ts
@@ -1,9 +1,9 @@
 /** This struct has a unit field */
 export interface StructHasVoidType {
-	thisIsAUnit: undefined;
+	thisIsAUnit: null;
 }
 
 /** This enum has a variant associated with unit data */
 export type EnumHasVoidType = 
-	| { type: "hasAUnit", content: undefined };
+	| { type: "hasAUnit", content: null };
 
diff --git a/core/data/tests/can_recognize_types_inside_modules/input.rs b/core/data/tests/can_recognize_types_inside_modules/input.rs
new file mode 100644
index 0000000..222d5ee
--- /dev/null
+++ b/core/data/tests/can_recognize_types_inside_modules/input.rs
@@ -0,0 +1,23 @@
+mod a {
+    #[typeshare]
+    pub struct A {
+        field: u32
+    }
+    mod b {
+        mod c {
+            #[typeshare]
+            pub struct ABC {
+                field: u32
+            }
+        }
+        #[typeshare]
+        pub struct AB {
+            field: u32
+        }
+    }
+}
+
+#[typeshare]
+pub struct OutsideOfModules {
+    field: u32
+}
\ No newline at end of file
diff --git a/core/data/tests/can_recognize_types_inside_modules/output.go b/core/data/tests/can_recognize_types_inside_modules/output.go
new file mode 100644
index 0000000..f08ec77
--- /dev/null
+++ b/core/data/tests/can_recognize_types_inside_modules/output.go
@@ -0,0 +1,16 @@
+package proto
+
+import "encoding/json"
+
+type A struct {
+	Field uint32 `json:"field"`
+}
+type ABC struct {
+	Field uint32 `json:"field"`
+}
+type AB struct {
+	Field uint32 `json:"field"`
+}
+type OutsideOfModules struct {
+	Field uint32 `json:"field"`
+}
diff --git a/core/data/tests/can_recognize_types_inside_modules/output.kt b/core/data/tests/can_recognize_types_inside_modules/output.kt
new file mode 100644
index 0000000..2de6a52
--- /dev/null
+++ b/core/data/tests/can_recognize_types_inside_modules/output.kt
@@ -0,0 +1,27 @@
+@file:NoLiveLiterals
+
+package com.agilebits.onepassword
+
+import androidx.compose.runtime.NoLiveLiterals
+import kotlinx.serialization.*
+
+@Serializable
+data class A (
+	val field: UInt
+)
+
+@Serializable
+data class ABC (
+	val field: UInt
+)
+
+@Serializable
+data class AB (
+	val field: UInt
+)
+
+@Serializable
+data class OutsideOfModules (
+	val field: UInt
+)
+
diff --git a/core/data/tests/can_recognize_types_inside_modules/output.scala b/core/data/tests/can_recognize_types_inside_modules/output.scala
new file mode 100644
index 0000000..5a2e40b
--- /dev/null
+++ b/core/data/tests/can_recognize_types_inside_modules/output.scala
@@ -0,0 +1,29 @@
+package com.agilebits
+
+package object onepassword {
+
+type UByte = Byte
+type UShort = Short
+type UInt = Int
+type ULong = Int
+
+}
+package onepassword {
+
+case class A (
+	field: UInt
+)
+
+case class ABC (
+	field: UInt
+)
+
+case class AB (
+	field: UInt
+)
+
+case class OutsideOfModules (
+	field: UInt
+)
+
+}
diff --git a/core/data/tests/can_recognize_types_inside_modules/output.swift b/core/data/tests/can_recognize_types_inside_modules/output.swift
new file mode 100644
index 0000000..ffaebb3
--- /dev/null
+++ b/core/data/tests/can_recognize_types_inside_modules/output.swift
@@ -0,0 +1,33 @@
+import Foundation
+
+public struct A: Codable {
+	public let field: UInt32
+
+	public init(field: UInt32) {
+		self.field = field
+	}
+}
+
+public struct ABC: Codable {
+	public let field: UInt32
+
+	public init(field: UInt32) {
+		self.field = field
+	}
+}
+
+public struct AB: Codable {
+	public let field: UInt32
+
+	public init(field: UInt32) {
+		self.field = field
+	}
+}
+
+public struct OutsideOfModules: Codable {
+	public let field: UInt32
+
+	public init(field: UInt32) {
+		self.field = field
+	}
+}
diff --git a/core/data/tests/can_recognize_types_inside_modules/output.ts b/core/data/tests/can_recognize_types_inside_modules/output.ts
new file mode 100644
index 0000000..00c101e
--- /dev/null
+++ b/core/data/tests/can_recognize_types_inside_modules/output.ts
@@ -0,0 +1,16 @@
+export interface A {
+	field: number;
+}
+
+export interface ABC {
+	field: number;
+}
+
+export interface AB {
+	field: number;
+}
+
+export interface OutsideOfModules {
+	field: number;
+}
+
diff --git a/core/tests/snapshot_tests.rs b/core/tests/snapshot_tests.rs
index 117868c..9a1826a 100644
--- a/core/tests/snapshot_tests.rs
+++ b/core/tests/snapshot_tests.rs
@@ -406,6 +406,9 @@ tests! {
     can_generate_double_option_pattern: [
         typescript
     ];
+    can_recognize_types_inside_modules: [
+        swift, kotlin, scala, typescript, go
+    ];
     test_simple_enum_case_name_support: [swift, kotlin, scala, typescript, go ];
     test_algebraic_enum_case_name_support: [
         swift {