Skip to content

Normalize implicitly-unwrapped optional types before splicing into expressions#23

Merged
tsapeta merged 1 commit into
mainfrom
fix-iuo-expression-type
Jun 20, 2026
Merged

Normalize implicitly-unwrapped optional types before splicing into expressions#23
tsapeta merged 1 commit into
mainfrom
fix-iuo-expression-type

Conversation

@tsapeta

@tsapeta tsapeta commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Why

The @ExpoModule/@JS and @Record macros splice a member's type (type.trimmedDescription) into expression position to reach the dynamic-type converter, e.g.:

let arg0 = try \(type).getDynamicType().cast(jsValue: arguments[0], appContext: appContext) as! \(type)

For an implicitly-unwrapped optional, the spliced string is SomeType!, which is invalid Swift in expression position:

error: using '!' is not allowed here

So a @JS func/var or @Record property declared with an IUO type (var x: SomeType!, func f() -> SomeType!) generated code that doesn't compile. Plain optionals (T?) and Optional<T> were unaffected; only the T! spelling broke. The existing snapshot tests didn't catch it because none exercised an IUO type through a splice, and assertMacroExpansion checks the generated string, not that it compiles.

How

Add expressionType(_:), which rewrites a trailing ! to ? (T! and T? are both Optional<T>, treated identically by the dynamic-type / cast layer, and ? is valid in expression position). Apply it at every site that splices a type into expression position:

  • function argument decode and result encode,
  • property get/set accessors,
  • @Record from(object:) / from(dictionary:) / toObject() conversions.

Type-annotation positions (where T! is valid, e.g. the emitted var x: T! and init(x: T! = nil)) keep the original spelling.

Test Plan

swift test in apple/ (109 tests). Added IUO coverage for a module property, a function return type, and a @Record property, each asserting the cast/convert expressions use T? while annotations stay T!.

…pressions

The `@ExpoModule`/`@JS` and `@Record` macros splice a member's type
(`type.trimmedDescription`) into expression position to reach the dynamic-type
converter, e.g. `\(type).getDynamicType().cast(...) as! \(type)`. For an
implicitly-unwrapped optional (`var x: SomeType!`, `func f() -> SomeType!`, a
`@Record` property of type `T!`) the spliced string is `SomeType!`, which is
invalid Swift in expression position ("using '!' is not allowed here") and
produces code that doesn't compile. Plain optionals (`T?`) and `Optional<T>`
were unaffected; only the IUO spelling broke.

Add `expressionType(_:)`, which rewrites a trailing `!` to `?` (`T!` and `T?`
are both `Optional<T>`, treated identically by the dynamic-type / cast layer),
and apply it at every site that splices a type into expression position: the
function argument decode and result encode, the property get/set accessors, and
the `@Record` `from(object:)` / `from(dictionary:)` / `toObject()` conversions.
Type-annotation positions (where `T!` is valid) keep the original spelling.

The existing snapshot tests didn't catch this because none exercised an IUO
type through a splice, and `assertMacroExpansion` checks the generated string,
not that it compiles. Add IUO coverage for a module property, a function return
type, and a `@Record` property, asserting the cast/convert expressions use `T?`
while annotations stay `T!`.
@tsapeta tsapeta requested a review from Kudo June 19, 2026 21:59
@tsapeta tsapeta marked this pull request as ready for review June 19, 2026 22:00
@tsapeta tsapeta merged commit 5d086b6 into main Jun 20, 2026
1 check passed
@tsapeta tsapeta deleted the fix-iuo-expression-type branch June 20, 2026 18:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants