Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ internal struct JSFunction {
if let accessor = fastDecodeAccessor(for: type) {
return "let arg\(index) = try arguments.unownedValue(at: \(index)).\(accessor)()"
}
return "let arg\(index) = try \(type).getDynamicType().cast(jsValue: arguments[\(index)], appContext: appContext) as! \(type)"
let exprType = expressionType(type)
return "let arg\(index) = try \(exprType).getDynamicType().cast(jsValue: arguments[\(index)], appContext: appContext) as! \(exprType)"
}

/// The `self.<name>(...)` call for the given arity. Slots `0..<arity` are passed their decoded
Expand Down Expand Up @@ -184,7 +185,7 @@ internal struct JSFunction {
if fastDecodeAccessor(for: returnType) != nil {
return ["return result.toJavaScriptValue(in: runtime)"]
}
return ["return try \(returnType).getDynamicType().castToJS(result, appContext: appContext, in: runtime)"]
return ["return try \(expressionType(returnType)).getDynamicType().castToJS(result, appContext: appContext, in: runtime)"]
}

/// The `setProperty` statement that installs this function on the JS object. The decode-call-encode
Expand Down Expand Up @@ -295,7 +296,7 @@ internal struct JSProperty {
getEncode = "return self.\(swiftName).toJavaScriptValue(in: runtime)"
} else if let valueType {
getEncode =
"return try \(valueType).getDynamicType().castToJS(self.\(swiftName), appContext: appContext, in: runtime)"
"return try \(expressionType(valueType)).getDynamicType().castToJS(self.\(swiftName), appContext: appContext, in: runtime)"
} else {
// No known type: fall back to converting whatever `self.<name>` is. This only happens when the
// declaration has neither an annotation nor a literal default, which is rare for a stored var.
Expand All @@ -311,8 +312,9 @@ internal struct JSProperty {
if let accessor = fastDecodeAccessor(for: valueType) {
setDecode = "self.\(swiftName) = try arguments.unownedValue(at: 0).\(accessor)()"
} else {
let exprType = expressionType(valueType)
setDecode =
"self.\(swiftName) = try \(valueType).getDynamicType().cast(jsValue: arguments[0], appContext: appContext) as! \(valueType)"
"self.\(swiftName) = try \(exprType).getDynamicType().cast(jsValue: arguments[0], appContext: appContext) as! \(exprType)"
}
lines.append(
accessorClosure(descriptorName, "set", usesAppContext: usesAppContext, body: "\(setDecode)\nreturn .undefined"))
Expand Down
11 changes: 11 additions & 0 deletions apple/Sources/ExpoModulesMacros/MacroHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -245,3 +245,14 @@ extension AttributeListSyntax {
return nil
}
}

/// A type spelled so it's valid in expression position (before `.getDynamicType()` or after `as!`).
/// Implicitly-unwrapped optionals (`T!`) are only allowed in type-annotation position, so a trailing
/// `!` is rewritten to `?` (`T!` and `T?` are both `Optional<T>`, which the dynamic-type / cast layer
/// treats identically). Other type spellings pass through unchanged.
internal func expressionType(_ type: String) -> String {
guard type.hasSuffix("!") else {
return type
}
return type.dropLast() + "?"
}
8 changes: 5 additions & 3 deletions apple/Sources/ExpoModulesMacros/RecordMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,8 @@ private func jsObjectReadLines(properties: [RecordProperty]) -> [String] {
var lines: [String] = []
for property in properties {
let valueVar = "\(property.name)JSValue"
let cast = "try \(property.type).getDynamicType().cast(jsValue: \(valueVar), appContext: appContext) as! \(property.type)"
let exprType = expressionType(property.type)
let cast = "try \(exprType).getDynamicType().cast(jsValue: \(valueVar), appContext: appContext) as! \(exprType)"
lines.append(" let \(valueVar) = object.getProperty(\"\(property.name)\")")
if property.isRequired {
lines.append(" guard !\(valueVar).isUndefined() else {")
Expand All @@ -337,7 +338,8 @@ private func dictionaryReadLines(properties: [RecordProperty]) -> [String] {
var lines: [String] = []
for property in properties {
let valueVar = "\(property.name)Value"
let cast = "try \(property.type).getDynamicType().cast(\(valueVar), appContext: appContext) as! \(property.type)"
let exprType = expressionType(property.type)
let cast = "try \(exprType).getDynamicType().cast(\(valueVar), appContext: appContext) as! \(exprType)"
lines.append(" let \(valueVar) = dictionary[\"\(property.name)\"]")
if property.isRequired {
lines.append(" guard let \(valueVar) else {")
Expand Down Expand Up @@ -399,7 +401,7 @@ private func toObjectMethod(properties: [RecordProperty], inheritsRecord: Bool)
lines.append(" let object = try appContext.runtime.createObject()")
}
for property in properties {
lines.append(" object.setProperty(\"\(property.name)\", value: try \(property.type).getDynamicType().convertToJS(self.\(property.name), appContext: appContext))")
lines.append(" object.setProperty(\"\(property.name)\", value: try \(expressionType(property.type)).getDynamicType().convertToJS(self.\(property.name), appContext: appContext))")
}
lines.append(" return object")
let body = lines.joined(separator: "\n")
Expand Down
97 changes: 97 additions & 0 deletions apple/Tests/ExpoModulesMacrosTests/ExpoModuleMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,52 @@ struct ExpoModuleMacroTests {
)
}

@Test
func `Implicitly-unwrapped optional return normalizes to optional in the cast expression`() {
assertExpansion(
"""
@ExpoModule
final class MyModule: Module {
@JS
func make(count: Int) -> MyRecord! { nil }
}
""",
expandedSource: """
final class MyModule: Module {
@JavaScriptActor
func make(count: Int) -> MyRecord! { nil }

private func _assertTypesConformance_make() {
func make<T: AnyArgument>(_: T.Type) {
}
make(MyRecord.self)
}

public static let _jsName = "MyModule"

public func _synthesizedDefinition() -> [AnyDefinition] {
return []
}

@JavaScriptActor
public func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime, appContext: AppContext) throws {
object.setProperty("make") { [weak appContext, self] (this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer) in
guard let appContext else {
throw Exceptions.AppContextLost()
}
guard arguments.count == 1 else {
throw Exceptions.ArgumentsRangeMismatch((functionName: "make", received: arguments.count, required: 1, maximum: 1))
}
let arg0 = try arguments.unownedValue(at: 0).asInt()
let result = self.make(count: arg0)
return try MyRecord?.getDynamicType().castToJS(result, appContext: appContext, in: runtime)
}
}
}
"""
)
}

@Test
func `Static function emits a static conformance-assertion peer`() {
assertExpansion(
Expand Down Expand Up @@ -613,6 +659,57 @@ struct ExpoModuleMacroTests {
)
}

@Test
func `Implicitly-unwrapped optional property normalizes the type to optional in the cast expressions`() {
assertExpansion(
"""
@ExpoModule
final class MyModule: Module {
@JS
var config: MyRecord!
}
""",
expandedSource: """
final class MyModule: Module {
@JavaScriptActor
var config: MyRecord!

private func _assertTypesConformance_config() {
func config<T: AnyArgument>(_: T.Type) {
}
config(MyRecord.self)
}

public static let _jsName = "MyModule"

public func _synthesizedDefinition() -> [AnyDefinition] {
return []
}

@JavaScriptActor
public func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime, appContext: AppContext) throws {
let configDescriptor = runtime.createObject()
configDescriptor.setProperty("enumerable", value: true)
configDescriptor.setProperty("get") { [weak appContext, self] (this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer) in
guard let appContext else {
throw Exceptions.AppContextLost()
}
return try MyRecord?.getDynamicType().castToJS(self.config, appContext: appContext, in: runtime)
}
configDescriptor.setProperty("set") { [weak appContext, self] (this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer) in
guard let appContext else {
throw Exceptions.AppContextLost()
}
self.config = try MyRecord?.getDynamicType().cast(jsValue: arguments[0], appContext: appContext) as! MyRecord?
return .undefined
}
object.defineProperty("config", descriptor: configDescriptor)
}
}
"""
)
}

@Test
func `Mixed members: only @JS-marked ones are picked up`() {
assertExpansion(
Expand Down
59 changes: 59 additions & 0 deletions apple/Tests/ExpoModulesMacrosTests/RecordMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,65 @@ struct RecordMacroTests {
)
}

@Test
func `Implicitly-unwrapped optional property normalizes to optional in cast and convert expressions`() {
assertExpansion(
"""
@Record
struct Options {
var owner: MyRecord!
}
""",
expandedSource: """
struct Options {
var owner: MyRecord!

private func _assertTypesConformance() {
func owner<T: AnyArgument>(_: T.Type) {
}
owner(MyRecord.self)
}

public init() {
}

public init(owner: MyRecord! = nil) {
self.owner = owner
}

@JavaScriptActor
public static func from(object: borrowing JavaScriptObject, appContext: AppContext) throws -> Self {
let ownerJSValue = object.getProperty("owner")
let owner: MyRecord! = (ownerJSValue.isUndefined() || ownerJSValue.isNull()) ? nil : try MyRecord?.getDynamicType().cast(jsValue: ownerJSValue, appContext: appContext) as! MyRecord?
return Self(owner: owner)
}

public static func from(dictionary: [String: Any], appContext: AppContext) throws -> Self {
let ownerValue = dictionary["owner"]
let owner: MyRecord! = (ownerValue == nil || ownerValue! is NSNull) ? nil : try MyRecord?.getDynamicType().cast(ownerValue, appContext: appContext) as! MyRecord?
return Self(owner: owner)
}

public func toDictionary(appContext: AppContext? = nil) -> [String: Any] {
var dictionary: [String: Any] = [:]
dictionary["owner"] = self.owner
return dictionary
}

@JavaScriptActor
public func toObject(appContext: AppContext) throws -> JavaScriptObject {
let object = try appContext.runtime.createObject()
object.setProperty("owner", value: try MyRecord?.getDynamicType().convertToJS(self.owner, appContext: appContext))
return object
}
}

extension Options: Record {
}
"""
)
}

@Test
func `Non-primitive properties are checked in a single conformance-assertion peer`() {
assertExpansion(
Expand Down
Loading