diff --git a/Sources/SwiftJava/Optional+JavaOptional.swift b/Sources/SwiftJava/Optional+JavaOptional.swift index e93afb12..f6dd423e 100644 --- a/Sources/SwiftJava/Optional+JavaOptional.swift +++ b/Sources/SwiftJava/Optional+JavaOptional.swift @@ -14,7 +14,7 @@ extension Optional where Wrapped: AnyJavaObject { public func toJavaOptional() -> JavaOptional { - try! JavaClass>().ofNullable(self?.as(JavaObject.self)).as(JavaOptional.self)! + try! JavaClass>().ofNullable(self) } public init(javaOptional: JavaOptional?) { @@ -29,7 +29,7 @@ extension Optional where Wrapped: AnyJavaObject { extension Optional where Wrapped == String { public func toJavaOptional() -> JavaOptional { if let self { - return try! JavaClass>().of(JavaString(self).as(JavaObject.self)).as(JavaOptional.self)! + return try! JavaClass>().of(JavaString(self)) } else { return try! JavaClass>().empty().as(JavaOptional.self)! } diff --git a/Sources/SwiftJava/generated/JavaOptional.swift b/Sources/SwiftJava/generated/JavaOptional.swift index 6ec584af..1726fbc5 100644 --- a/Sources/SwiftJava/generated/JavaOptional.swift +++ b/Sources/SwiftJava/generated/JavaOptional.swift @@ -3,51 +3,115 @@ import SwiftJavaJNICore @JavaClass("java.util.Optional") open class JavaOptional: JavaObject { - @JavaMethod(typeErasedResult: "T") + /// Java method `get`. + /// + /// ### Java method signature + /// ```java + /// public T java.util.Optional.get() + /// ``` + @JavaMethod(typeErasedResult: "T!") open func get() -> T! + /// Java method `equals`. + /// + /// ### Java method signature + /// ```java + /// public boolean java.util.Optional.equals(java.lang.Object) + /// ``` @JavaMethod open override func equals(_ arg0: JavaObject?) -> Bool + /// Java method `toString`. + /// + /// ### Java method signature + /// ```java + /// public java.lang.String java.util.Optional.toString() + /// ``` @JavaMethod open override func toString() -> String + /// Java method `hashCode`. + /// + /// ### Java method signature + /// ```java + /// public int java.util.Optional.hashCode() + /// ``` @JavaMethod open override func hashCode() -> Int32 + /// Java method `isEmpty`. + /// + /// ### Java method signature + /// ```java + /// public boolean java.util.Optional.isEmpty() + /// ``` @JavaMethod open func isEmpty() -> Bool - @JavaMethod - open func isPresent() -> Bool + /// Java method `orElse`. + /// + /// ### Java method signature + /// ```java + /// public T java.util.Optional.orElse(T) + /// ``` + @JavaMethod(typeErasedResult: "T!") + open func orElse(_ arg0: T?) -> T! + /// Java method `isPresent`. + /// + /// ### Java method signature + /// ```java + /// public boolean java.util.Optional.isPresent() + /// ``` @JavaMethod - open func orElse(_ arg0: JavaObject?) -> JavaObject! + open func isPresent() -> Bool - @JavaMethod - open func orElseThrow() -> JavaObject! + /// Java method `orElseThrow`. + /// + /// ### Java method signature + /// ```java + /// public T java.util.Optional.orElseThrow() + /// ``` + @JavaMethod(typeErasedResult: "T!") + open func orElseThrow() -> T! } extension JavaClass { + /// Java method `of`. + /// + /// ### Java method signature + /// ```java + /// public static java.util.Optional java.util.Optional.of(T) + /// ``` @JavaStaticMethod - public func of(_ arg0: JavaObject?) -> JavaOptional! where ObjectType == JavaOptional + public func of(_ arg0: T?) -> JavaOptional! where ObjectType == JavaOptional - public func ofOptional(_ arg0: JavaObject?) -> JavaObject? where ObjectType == JavaOptional { + public func ofOptional(_ arg0: T?) -> T? where ObjectType == JavaOptional { Optional(javaOptional: of(arg0)) } + /// Java method `empty`. + /// + /// ### Java method signature + /// ```java + /// public static java.util.Optional java.util.Optional.empty() + /// ``` @JavaStaticMethod - public func empty() -> JavaOptional! where ObjectType == JavaOptional + public func empty() -> JavaOptional! where ObjectType == JavaOptional - public func emptyOptional() -> JavaObject? where ObjectType == JavaOptional { + public func emptyOptional() -> T? where ObjectType == JavaOptional { Optional(javaOptional: empty()) } + /// Java method `ofNullable`. + /// + /// ### Java method signature + /// ```java + /// public static java.util.Optional java.util.Optional.ofNullable(T) + /// ``` @JavaStaticMethod - public func ofNullable(_ arg0: JavaObject?) -> JavaOptional! - where ObjectType == JavaOptional + public func ofNullable(_ arg0: T?) -> JavaOptional! where ObjectType == JavaOptional - public func ofNullableOptional(_ arg0: JavaObject?) -> JavaObject? - where ObjectType == JavaOptional { + public func ofNullableOptional(_ arg0: T?) -> T? where ObjectType == JavaOptional { Optional(javaOptional: ofNullable(arg0)) } } diff --git a/Sources/SwiftJava/generated/List.swift b/Sources/SwiftJava/generated/List.swift index 9fa12e82..07d4c063 100644 --- a/Sources/SwiftJava/generated/List.swift +++ b/Sources/SwiftJava/generated/List.swift @@ -33,11 +33,23 @@ public struct List { @JavaMethod public func isEmpty() -> Bool - @JavaMethod - public func add(_ arg0: JavaObject?) -> Bool - - @JavaMethod - public func add(_ arg0: Int32, _ arg1: JavaObject?) + /// Java method `add`. + /// + /// ### Java method signature + /// ```java + /// public abstract void java.util.List.add(int,E) + /// ``` + @JavaMethod + public func add(_ arg0: Int32, _ arg1: E?) + + /// Java method `add`. + /// + /// ### Java method signature + /// ```java + /// public abstract boolean java.util.List.add(E) + /// ``` + @JavaMethod + public func add(_ arg0: E?) -> Bool @JavaMethod public func subList(_ arg0: Int32, _ arg1: Int32) -> List! diff --git a/Sources/SwiftJavaMacros/JavaMethodMacro.swift b/Sources/SwiftJavaMacros/JavaMethodMacro.swift index 9dd2c636..9996127d 100644 --- a/Sources/SwiftJavaMacros/JavaMethodMacro.swift +++ b/Sources/SwiftJavaMacros/JavaMethodMacro.swift @@ -50,6 +50,8 @@ extension JavaMethodMacro: BodyMacro { fatalError("not a function: \(declaration)") } + var resultStatements: [CodeBlockItemSyntax] = [] + let funcName = if case .argumentList(let arguments) = node.arguments, let argument = arguments.first, @@ -65,7 +67,28 @@ extension JavaMethodMacro: BodyMacro { let isStatic = node.attributeName.trimmedDescription == "JavaStaticMethod" let params = funcDecl.signature.parameterClause.parameters - let paramNames = params.map { param in param.parameterName?.text ?? "" }.joined(separator: ", ") + + var paramNames: [String] = [] + for param in params { + guard let name = param.parameterName else { + throw MacroErrors.parameterMustHaveName(method: funcName, paramSyntax: param.trimmedDescription) + } + if isJNIGenericParameter(param.type, funcDecl: funcDecl, in: context) { + let erasedName: TokenSyntax = "\(name)$erased" + if param.type.optionalUnwrappedType() != nil { + resultStatements.append( + "let \(erasedName) = \(name).map { JavaObject(javaHolder: $0.javaHolder) }" + ) + } else { + resultStatements.append( + "let \(erasedName) = JavaObject(javaHolder: \(name).javaHolder)" + ) + } + paramNames.append(erasedName.text) + } else { + paramNames.append(name.text) + } + } let genericResultType: String? = if case let .argumentList(arguments) = node.arguments, @@ -106,7 +129,7 @@ extension JavaMethodMacro: BodyMacro { if paramNames.isEmpty { parametersAsArgs = "" } else { - parametersAsArgs = ", arguments: \(paramNames)" + parametersAsArgs = ", arguments: \(paramNames.joined(separator: ", "))" } let canRethrowError = funcDecl.signature.effectSpecifiers?.throwsClause != nil @@ -137,23 +160,60 @@ extension JavaMethodMacro: BodyMacro { """ if let genericResultType { - return [ + resultStatements.append( """ /* convert erased return value to \(raw: genericResultType) */ let result$ = \(resultSyntax) + """ + ) + resultStatements.append( + """ if let result$ { return \(raw: genericResultType)(javaThis: result$.javaThis, environment: try! JavaVirtualMachine.shared().environment()) } else { return nil } """ - ] + ) + } else { + // no return type conversions + resultStatements.append("return \(resultSyntax)") } - // no return type conversions - return [ - "return \(resultSyntax)" - ] + return resultStatements + } + + /// Determines whether an argument is generic in heuristic way. + /// Since Optional does not appear in JNI signatures, it is removed before checking. + /// FIXME: It might be preferable to explicitly specify the type from JavaClass, similar to `typeErasedResult`. + private static func isJNIGenericParameter( + _ type: TypeSyntax, + funcDecl: FunctionDeclSyntax, + in context: some MacroExpansionContext + ) -> Bool { + let baseType = type.optionalUnwrappedType() ?? type + guard let identifier = baseType.as(IdentifierTypeSyntax.self) else { + return false + } + let typeName = identifier.name.text + + if let genericParams = funcDecl.genericParameterClause?.parameters { + if genericParams.contains(where: { $0.name.text == typeName }) { + return true + } + } + + for contextNode in context.lexicalContext { + if let decl = contextNode.asProtocol(WithGenericParametersSyntax.self) { + if decl.genericParameterClause?.parameters.contains(where: { + $0.name.text == typeName + }) == true { + return true + } + } + } + + return false } /// Bridge an initializer into a call to Java. @@ -238,4 +298,23 @@ extension TypeSyntaxProtocol { var typeReferenceString: String { typeReference.description } + + func optionalUnwrappedType() -> TypeSyntax? { + if let optionalType = self.as(OptionalTypeSyntax.self) { + return optionalType.wrappedType + } + + if let implicitlyUnwrappedType = self.as(ImplicitlyUnwrappedOptionalTypeSyntax.self) { + return implicitlyUnwrappedType.wrappedType + } + + if let identifierType = self.as(IdentifierTypeSyntax.self), + identifierType.name.text == "Optional", + let genericArgumentClause = identifierType.genericArgumentClause + { + return genericArgumentClause.arguments.first?.argument.as(TypeSyntax.self) + } + + return nil + } } diff --git a/Sources/SwiftJavaMacros/MacroErrors.swift b/Sources/SwiftJavaMacros/MacroErrors.swift index 2fcbfa91..97859c6e 100644 --- a/Sources/SwiftJavaMacros/MacroErrors.swift +++ b/Sources/SwiftJavaMacros/MacroErrors.swift @@ -21,4 +21,5 @@ enum MacroErrors: Error { case methodNotOnFunction case missingEnvironment case macroOutOfContext(String) + case parameterMustHaveName(method: String, paramSyntax: String) } diff --git a/Tests/SwiftJavaMacrosTests/JavaClassMacroTests.swift b/Tests/SwiftJavaMacrosTests/JavaClassMacroTests.swift index d79b6bd4..130c38be 100644 --- a/Tests/SwiftJavaMacrosTests/JavaClassMacroTests.swift +++ b/Tests/SwiftJavaMacrosTests/JavaClassMacroTests.swift @@ -24,6 +24,7 @@ class JavaKitMacroTests: XCTestCase { "JavaClass": JavaClassMacro.self, "JavaMethod": JavaMethodMacro.self, "JavaField": JavaFieldMacro.self, + "JavaStaticMethod": JavaMethodMacro.self, "JavaStaticField": JavaFieldMacro.self, ] @@ -99,7 +100,7 @@ class JavaKitMacroTests: XCTestCase { public init(_ value: Int32, environment: JNIEnvironment? = nil) @JavaMethod - public func isBigEnough(_: Int32) -> Bool + public func isBigEnough(_ v: Int32) -> Bool @JavaField public var myField: Int64 @@ -130,10 +131,10 @@ class JavaKitMacroTests: XCTestCase { } self = try! Self.dynamicJavaNewObject(in: _environment, arguments: value.self) } - public func isBigEnough(_: Int32) -> Bool { + public func isBigEnough(_ v: Int32) -> Bool { return { do { - return try dynamicJavaMethodCall(methodName: "isBigEnough", resultType: Bool.self) + return try dynamicJavaMethodCall(methodName: "isBigEnough", arguments: v, resultType: Bool.self) } catch { if let throwable = error as? Throwable { let sw = StringWriter() @@ -206,7 +207,7 @@ class JavaKitMacroTests: XCTestCase { public init(_ value: Int32, environment: JNIEnvironment? = nil) @JavaMethod - public func isBigEnough(_: Int32) -> Bool + public func isBigEnough(_ v: Int32) -> Bool @JavaField public var myField: Int64 @@ -239,10 +240,10 @@ class JavaKitMacroTests: XCTestCase { let javaThis = try! Self.dynamicJavaNewObjectInstance(in: _environment, arguments: value.self) self.init(javaThis: javaThis, environment: _environment) } - public func isBigEnough(_: Int32) -> Bool { + public func isBigEnough(_ v: Int32) -> Bool { return { do { - return try dynamicJavaMethodCall(methodName: "isBigEnough", resultType: Bool.self) + return try dynamicJavaMethodCall(methodName: "isBigEnough", arguments: v, resultType: Bool.self) } catch { if let throwable = error as? Throwable { let sw = StringWriter() @@ -303,7 +304,7 @@ class JavaKitMacroTests: XCTestCase { public init(environment: JNIEnvironment? = nil) @JavaMethod - public func isBigEnough(_: Int32) -> Bool + public func isBigEnough(_ v: Int32) -> Bool } """, expandedSource: """ @@ -318,10 +319,10 @@ class JavaKitMacroTests: XCTestCase { let javaThis = try! Self.dynamicJavaNewObjectInstance(in: _environment) self.init(javaThis: javaThis, environment: _environment) } - public func isBigEnough(_: Int32) -> Bool { + public func isBigEnough(_ v: Int32) -> Bool { return { do { - return try dynamicJavaMethodCall(methodName: "isBigEnough", resultType: Bool.self) + return try dynamicJavaMethodCall(methodName: "isBigEnough", arguments: v, resultType: Bool.self) } catch { if let throwable = error as? Throwable { let sw = StringWriter() @@ -405,4 +406,113 @@ class JavaKitMacroTests: XCTestCase { macros: Self.javaKitMacros ) } + + func testJavaGenericMethodParameter() throws { + assertMacroExpansion( + """ + extension JavaClass { + @JavaStaticMethod + public func ofNullable(_ arg0: T?) -> JavaOptional! + where ObjectType == JavaOptional + + @JavaStaticMethod + public func ofNullable2(arg0: T!, arg1: Optional, arg2: T, arg3: Int) + } + """, + expandedSource: #""" + extension JavaClass { + public func ofNullable(_ arg0: T?) -> JavaOptional! + where ObjectType == JavaOptional { + let arg0$erased = arg0.map { + JavaObject(javaHolder: $0.javaHolder) + } + return { + do { + return try dynamicJavaStaticMethodCall(methodName: "ofNullable", arguments: arg0$erased, resultType: JavaOptional?.self) + } catch { + if let throwable = error as? Throwable { + let sw = StringWriter() + let pw = PrintWriter(sw) + throwable.printStackTrace(pw) + fatalError("Java call threw unhandled exception: \(error)\n\(sw.toString())") + } + fatalError("Java call threw unhandled exception: \(error)") + } + }() + } + public func ofNullable2(arg0: T!, arg1: Optional, arg2: T, arg3: Int) { + let arg0$erased = arg0.map { + JavaObject(javaHolder: $0.javaHolder) + } + let arg1$erased = arg1.map { + JavaObject(javaHolder: $0.javaHolder) + } + let arg2$erased = JavaObject(javaHolder: arg2.javaHolder) + return { + do { + return try dynamicJavaStaticMethodCall(methodName: "ofNullable2", arguments: arg0$erased, arg1$erased, arg2$erased, arg3) + } catch { + if let throwable = error as? Throwable { + let sw = StringWriter() + let pw = PrintWriter(sw) + throwable.printStackTrace(pw) + fatalError("Java call threw unhandled exception: \(error)\n\(sw.toString())") + } + fatalError("Java call threw unhandled exception: \(error)") + } + }() + } + } + """#, + macros: Self.javaKitMacros + ) + } + + func testJavaGenericClassGenericMethodParameter() throws { + assertMacroExpansion( + """ + @JavaClass("java.util.ArrayList") + open class ArrayList: JavaObject { + @JavaMethod + open func add(_ arg0: E?) -> Bool + } + """, + expandedSource: #""" + open class ArrayList: JavaObject { + open func add(_ arg0: E?) -> Bool { + let arg0$erased = arg0.map { + JavaObject(javaHolder: $0.javaHolder) + } + return { + do { + return try dynamicJavaMethodCall(methodName: "add", arguments: arg0$erased, resultType: Bool.self) + } catch { + if let throwable = error as? Throwable { + let sw = StringWriter() + let pw = PrintWriter(sw) + throwable.printStackTrace(pw) + fatalError("Java call threw unhandled exception: \(error)\n\(sw.toString())") + } + fatalError("Java call threw unhandled exception: \(error)") + } + }() + } + + /// The full Java class name for this Swift type. + open override class var fullJavaClassName: String { + #if os(Android) && AndroidCoreLibraryDesugaring + AndroidSupport.androidDesugarClassNameConversion(for: "java.util.ArrayList") + #else + "java.util.ArrayList" + #endif + } + + public required init(javaHolder: JavaObjectHolder) { + super.init(javaHolder: javaHolder) + } + } + """#, + macros: Self.javaKitMacros + ) + } } diff --git a/Tests/SwiftJavaTests/BasicRuntimeTests.swift b/Tests/SwiftJavaTests/BasicRuntimeTests.swift index 0a139ba2..1dace398 100644 --- a/Tests/SwiftJavaTests/BasicRuntimeTests.swift +++ b/Tests/SwiftJavaTests/BasicRuntimeTests.swift @@ -108,6 +108,18 @@ class BasicRuntimeTests: XCTestCase { XCTAssertEqual(javaList.map { $0.intValue() }, [0, 1, 2]) } + + func testJavaOptional() throws { + let environment = try jvm.environment() + + let value = JavaInteger(42, environment: environment) + let javaOptional = Optional.some(value).toJavaOptional() + if javaOptional.isPresent() { + XCTAssertEqual(javaOptional.get().intValue(), 42) + } else { + XCTFail("javaOptional is empty") + } + } } @JavaClass("org.swift.javakit.Nonexistent")