diff --git a/Package.swift b/Package.swift index 7d872795a..b4c202082 100644 --- a/Package.swift +++ b/Package.swift @@ -209,6 +209,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), .package(url: "https://github.com/apple/swift-system", from: "1.4.0"), .package(url: "https://github.com/apple/swift-log", from: "1.2.0"), + .package(url: "https://github.com/apple/swift-collections", .upToNextMinor(from: "1.3.0")), // primarily for ordered collections // // FIXME: swift-subprocess stopped supporting 6.0 when it moved into a package; // // we'll need to drop 6.0 as well, but currently blocked on doing so by swiftpm plugin pending design questions @@ -400,6 +401,7 @@ let package = Package( name: "SwiftJavaToolLib", dependencies: [ .product(name: "Logging", package: "swift-log"), + .product(name: "OrderedCollections", package: "swift-collections"), .product(name: "SwiftBasicFormat", package: "swift-syntax"), .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), diff --git a/Samples/JavaKitSampleApp/Package.swift b/Samples/JavaKitSampleApp/Package.swift index 881b8d0ba..082d61ac9 100644 --- a/Samples/JavaKitSampleApp/Package.swift +++ b/Samples/JavaKitSampleApp/Package.swift @@ -78,5 +78,16 @@ let package = Package( .plugin(name: "SwiftJavaPlugin", package: "swift-java"), ] ), + + .testTarget( + name: "JavaKitExampleTests", + dependencies: [ + "JavaKitExample" + ], + swiftSettings: [ + .swiftLanguageMode(.v5), + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) + ] + ), ] ) diff --git a/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/ThreadSafeHelperClass.java b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/ThreadSafeHelperClass.java index 3b7793f07..38fe1a741 100644 --- a/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/ThreadSafeHelperClass.java +++ b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/ThreadSafeHelperClass.java @@ -23,7 +23,7 @@ public class ThreadSafeHelperClass { public ThreadSafeHelperClass() { } - public Optional text = Optional.of(""); + public Optional text = Optional.of("cool string"); public final OptionalDouble val = OptionalDouble.of(2); @@ -31,6 +31,20 @@ public String getValue(Optional name) { return name.orElse(""); } + + public String getOrElse(Optional name) { + return name.orElse("or else value"); + } + + public Optional getNil() { + return Optional.empty(); + } + + // @NonNull + // public Optional getNil() { + // return Optional.empty(); + // } + public Optional getText() { return text; } diff --git a/Samples/JavaKitSampleApp/Tests/JavaKitExampleTests/JavaKitOptionalTests.swift b/Samples/JavaKitSampleApp/Tests/JavaKitExampleTests/JavaKitOptionalTests.swift new file mode 100644 index 000000000..9ed14cd05 --- /dev/null +++ b/Samples/JavaKitSampleApp/Tests/JavaKitExampleTests/JavaKitOptionalTests.swift @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JavaKitExample + +import SwiftJava +import JavaUtilFunction +import Testing + +@Suite +struct ManglingTests { + + @Test + func methodMangling() throws { + let jvm = try! JavaVirtualMachine.shared( + classpath: [ + ".build/plugins/outputs/javakitsampleapp/JavaKitExample/destination/JavaCompilerPlugin/Java" + ] + ) + let env = try! jvm.environment() + + let helper = ThreadSafeHelperClass(environment: env) + + let text: JavaString? = helper.textOptional + #expect(#"Optional("cool string")"# == String(describing: Optional("cool string"))) + #expect(#"Optional("cool string")"# == String(describing: text)) + + // let defaultValue: String? = helper.getOrElse(JavaOptional.empty()) + // #expect(#"Optional("or else value")"# == String(describing: defaultValue)) + + let noneValue: JavaOptional = helper.getNil()! + #expect(noneValue.isPresent() == false) + #expect("\(noneValue)" == "SwiftJava.JavaOptional") + + let textFunc: JavaString? = helper.getTextOptional() + #expect(#"Optional("cool string")"# == String(describing: textFunc)) + + let doubleOpt: Double? = helper.valOptional + #expect(#"Optional(2.0)"# == String(describing: doubleOpt)) + + let longOpt: Int64? = helper.fromOptional(21 as Int32?) + #expect(#"Optional(21)"# == String(describing: longOpt)) + } + +} \ No newline at end of file diff --git a/Samples/JavaKitSampleApp/ci-validate.sh b/Samples/JavaKitSampleApp/ci-validate.sh index f453a00be..297f5c885 100755 --- a/Samples/JavaKitSampleApp/ci-validate.sh +++ b/Samples/JavaKitSampleApp/ci-validate.sh @@ -3,7 +3,9 @@ set -e set -x -swift build +swift build \ + --disable-experimental-prebuilts # FIXME: until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 + "$JAVA_HOME/bin/java" \ -cp .build/plugins/outputs/javakitsampleapp/JavaKitExample/destination/JavaCompilerPlugin/Java \ -Djava.library.path=.build/debug \ diff --git a/Sources/SwiftJava/AnyJavaObject.swift b/Sources/SwiftJava/AnyJavaObject.swift index bf749820f..fe77bdbd4 100644 --- a/Sources/SwiftJava/AnyJavaObject.swift +++ b/Sources/SwiftJava/AnyJavaObject.swift @@ -59,7 +59,11 @@ public protocol AnyJavaObjectWithCustomClassLoader: AnyJavaObject { extension AnyJavaObject { /// Retrieve the underlying Java object. public var javaThis: jobject { - javaHolder.object! + javaHolder.object! // FIXME: this is a bad idea, can be null + } + + public var javaThisOptional: jobject? { + javaHolder.object } /// Retrieve the environment in which this Java object resides. diff --git a/Sources/SwiftJava/JavaObject+Inheritance.swift b/Sources/SwiftJava/JavaObject+Inheritance.swift index f306b9c6f..0ddd79449 100644 --- a/Sources/SwiftJava/JavaObject+Inheritance.swift +++ b/Sources/SwiftJava/JavaObject+Inheritance.swift @@ -22,16 +22,16 @@ extension AnyJavaObject { private func isInstanceOf( _ otherClass: OtherClass.Type ) -> jclass? { - try? otherClass.withJNIClass(in: javaEnvironment) { otherJavaClass in - if javaEnvironment.interface.IsInstanceOf( - javaEnvironment, - javaThis, - otherJavaClass - ) == 0 { - return nil - } + guard let this: jobject = javaThisOptional else { + return nil + } + + return try? otherClass.withJNIClass(in: javaEnvironment) { otherJavaClass in + if javaEnvironment.interface.IsInstanceOf(javaEnvironment, this, otherJavaClass) == 0 { + return nil + } - return otherJavaClass + return otherJavaClass } } diff --git a/Sources/SwiftJava/JavaString+Extensions.swift b/Sources/SwiftJava/JavaString+Extensions.swift new file mode 100644 index 000000000..1836777d2 --- /dev/null +++ b/Sources/SwiftJava/JavaString+Extensions.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import CSwiftJavaJNI +import JavaTypes + +extension JavaString: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { + return toString() + } + public var debugDescription: String { + return "\"" + toString() + "\"" + } +} + +extension Optional where Wrapped == JavaString { + public var description: String { + switch self { + case .some(let value): "Optional(\(value.toString())" + case .none: "nil" + } + } +} \ No newline at end of file diff --git a/Sources/SwiftJava/Macros.swift b/Sources/SwiftJava/Macros.swift index 8e1bb86c5..eb9c43745 100644 --- a/Sources/SwiftJava/Macros.swift +++ b/Sources/SwiftJava/Macros.swift @@ -123,8 +123,26 @@ public macro JavaStaticField(_ javaFieldName: String? = nil, isFinal: Bool = fal /// ``` /// /// corresponds to the Java constructor `HelloSwift(String name)`. +/// +/// ### Generics and type-erasure +/// Swift and Java differ in how they represent generics at runtime. +/// In Java, generics are type-erased and the JVM representation of generic types is erased to `java.lang.Object`. +/// Swift on the other hand, reifies types which means a `Test` in practice will be a specific type with +/// the generic substituted `Test`. This means that at runtime, calling a generic @JavaMethod needs to know +/// which of the parameters (or result type) must be subjected to type-erasure as we form the call into the Java function. +/// +/// In order to mark a generic return type you must indicate it to the @JavaMethod macro like this: +/// ```swift +/// // Java: class Test { public get(); } +/// @JavaMethod(typeErasedResult: "T!") +/// func get() -> T! +/// ``` +/// This allows the macro to form a call into the get() method, which at runtime, will have an `java.lang.Object` +/// returning method signature, and then, convert the result to the expected `T` type on the Swift side. @attached(body) -public macro JavaMethod() = #externalMacro(module: "SwiftJavaMacros", type: "JavaMethodMacro") +public macro JavaMethod( + typeErasedResult: String? = nil +) = #externalMacro(module: "SwiftJavaMacros", type: "JavaMethodMacro") /// Attached macro that turns a Swift method on JavaClass into one that wraps /// a Java static method on the underlying Java class object. @@ -132,11 +150,13 @@ public macro JavaMethod() = #externalMacro(module: "SwiftJavaMacros", type: "Jav /// The macro must be used within a specific JavaClass instance. /// /// ```swift -/// @JavaMethod +/// @JavaStaticMethod /// func sayHelloBack(_ i: Int32) -> Double /// ``` @attached(body) -public macro JavaStaticMethod() = #externalMacro(module: "SwiftJavaMacros", type: "JavaMethodMacro") +public macro JavaStaticMethod( + typeErasedResult: String? = nil +) = #externalMacro(module: "SwiftJavaMacros", type: "JavaMethodMacro") /// Macro that marks extensions to specify that all of the @JavaMethod /// methods are implementations of Java methods spelled as `native`. diff --git a/Sources/SwiftJava/Optional+JavaOptional.swift b/Sources/SwiftJava/Optional+JavaOptional.swift index c9becd584..8e3924fb1 100644 --- a/Sources/SwiftJava/Optional+JavaOptional.swift +++ b/Sources/SwiftJava/Optional+JavaOptional.swift @@ -79,3 +79,17 @@ public extension Optional where Wrapped == Int64 { } } } + +extension JavaOptional { + public func empty(environment: JNIEnvironment? = nil) -> JavaOptional! { + guard let env = try? environment ?? JavaVirtualMachine.shared().environment() else { + return nil + } + + guard let opt = try? JavaClass>(environment: env).empty() else { + return nil + } + + return opt.as(JavaOptional.self) + } +} diff --git a/Sources/SwiftJava/String+Extensions.swift b/Sources/SwiftJava/String+Extensions.swift index 94fb19286..b45f60bfb 100644 --- a/Sources/SwiftJava/String+Extensions.swift +++ b/Sources/SwiftJava/String+Extensions.swift @@ -13,10 +13,6 @@ //===----------------------------------------------------------------------===// import Foundation -// import SwiftJavaToolLib -// import SwiftJava -// import JavaUtilJar -// import SwiftJavaConfigurationShared extension String { /// For a String that's of the form java.util.Vector, return the "Vector" diff --git a/Sources/SwiftJava/generated/JavaLong.swift b/Sources/SwiftJava/generated/JavaLong.swift index 7ea8fc09e..4e993d65e 100644 --- a/Sources/SwiftJava/generated/JavaLong.swift +++ b/Sources/SwiftJava/generated/JavaLong.swift @@ -140,9 +140,6 @@ extension JavaClass { @JavaStaticMethod public func decode(_ arg0: String) throws -> JavaLong! - @JavaStaticMethod - public func highestOneBit(_ arg0: Int64) -> Int64 - @JavaStaticMethod public func sum(_ arg0: Int64, _ arg1: Int64) -> Int64 @@ -158,6 +155,9 @@ extension JavaClass { @JavaStaticMethod public func toBinaryString(_ arg0: Int64) -> String + @JavaStaticMethod + public func highestOneBit(_ arg0: Int64) -> Int64 + @JavaStaticMethod public func lowestOneBit(_ arg0: Int64) -> Int64 diff --git a/Sources/SwiftJava/generated/JavaOptional.swift b/Sources/SwiftJava/generated/JavaOptional.swift index 5f10005fc..f3b7adcab 100644 --- a/Sources/SwiftJava/generated/JavaOptional.swift +++ b/Sources/SwiftJava/generated/JavaOptional.swift @@ -3,8 +3,8 @@ import CSwiftJavaJNI @JavaClass("java.util.Optional") open class JavaOptional: JavaObject { - @JavaMethod - open func get() -> JavaObject! // FIXME: Currently we do generate -> T https://github.com/swiftlang/swift-java/issues/439 + @JavaMethod(typeErasedResult: "T") + open func get() -> T! @JavaMethod open override func equals(_ arg0: JavaObject?) -> Bool diff --git a/Sources/SwiftJava/generated/JavaOptionalDouble.swift b/Sources/SwiftJava/generated/JavaOptionalDouble.swift index 0d0e2eaeb..58aa94419 100644 --- a/Sources/SwiftJava/generated/JavaOptionalDouble.swift +++ b/Sources/SwiftJava/generated/JavaOptionalDouble.swift @@ -16,10 +16,10 @@ open class JavaOptionalDouble: JavaObject { open func isEmpty() -> Bool @JavaMethod - open func isPresent() -> Bool + open func orElse(_ arg0: Double) -> Double @JavaMethod - open func orElse(_ arg0: Double) -> Double + open func isPresent() -> Bool @JavaMethod open func orElseThrow() -> Double diff --git a/Sources/SwiftJavaMacros/JavaMethodMacro.swift b/Sources/SwiftJavaMacros/JavaMethodMacro.swift index db8a3b367..f992ee760 100644 --- a/Sources/SwiftJavaMacros/JavaMethodMacro.swift +++ b/Sources/SwiftJavaMacros/JavaMethodMacro.swift @@ -47,18 +47,49 @@ extension JavaMethodMacro: BodyMacro { } guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else { - fatalError("not a function") + fatalError("not a function: \(declaration)") } let isStatic = node.attributeName.trimmedDescription == "JavaStaticMethod" let funcName = funcDecl.name.text let params = funcDecl.signature.parameterClause.parameters - let resultType: String = - funcDecl.signature.returnClause.map { result in - ", resultType: \(result.type.typeReferenceString).self" - } ?? "" let paramNames = params.map { param in param.parameterName?.text ?? "" }.joined(separator: ", ") + let genericResultType: String? = + if case let .argumentList(arguments) = node.arguments, + let firstElement = arguments.first, + let stringLiteral = firstElement.expression + .as(StringLiteralExprSyntax.self), + stringLiteral.segments.count == 1, + case let .stringSegment(wrapperName)? = stringLiteral.segments.first { + // TODO: Improve this unwrapping a bit; + // Trim the trailing ! and ? from the type for purposes + // of initializing the type wrapper in the method body + if "\(wrapperName)".hasSuffix("!") || + "\(wrapperName)".hasSuffix("?") { + String("\(wrapperName)".dropLast()) + } else { + "\(wrapperName)" + } + } else { + nil + } + + // Determine the result type + let resultType: String = + if let returnClause = funcDecl.signature.returnClause { + if let genericResultType { + // we need to type-erase the signature, because on JVM level generics are erased and we'd otherwise + // form a signature with the "concrete" type, which would not match the real byte-code level signature + // of the method we're trying to call -- which would result in a MethodNotFound exception. + ", resultType: /*type-erased:\(genericResultType)*/JavaObject?.self" + } else { + ", resultType: \(returnClause.type.typeReferenceString).self" + } + } else { + "" + } + let parametersAsArgs: String if paramNames.isEmpty { parametersAsArgs = "" @@ -70,8 +101,25 @@ extension JavaMethodMacro: BodyMacro { funcDecl.signature.effectSpecifiers?.throwsClause != nil ? "try" : "try!" + let resultSyntax: CodeBlockItemSyntax = + "\(raw: tryKeyword) dynamicJava\(raw: isStatic ? "Static" : "")MethodCall(methodName: \(literal: funcName)\(raw: parametersAsArgs)\(raw: resultType))" + + if let genericResultType { + return [ + """ + /* convert erased return value to \(raw: genericResultType) */ + if let result$ = \(resultSyntax) { + return \(raw: genericResultType)(javaThis: result$.javaThis, environment: try! JavaVirtualMachine.shared().environment()) + } else { + return nil + } + """ + ] + } + + // no return type conversions return [ - "return \(raw: tryKeyword) dynamicJava\(raw: isStatic ? "Static" : "")MethodCall(methodName: \(literal: funcName)\(raw: parametersAsArgs)\(raw: resultType))" + "return \(resultSyntax)" ] } diff --git a/Sources/SwiftJavaToolLib/JavaClassTranslator.swift b/Sources/SwiftJavaToolLib/JavaClassTranslator.swift index 795639e5a..0f7aa45d6 100644 --- a/Sources/SwiftJavaToolLib/JavaClassTranslator.swift +++ b/Sources/SwiftJavaToolLib/JavaClassTranslator.swift @@ -15,6 +15,7 @@ import SwiftJava import JavaLangReflect import SwiftSyntax +import OrderedCollections import SwiftJavaConfigurationShared import Logging @@ -606,6 +607,27 @@ extension JavaClassTranslator { return false } + // TODO: make it more precise with the "origin" of the generic parameter (outer class etc) + func collectMethodGenericParameters( + genericParameters: [String], + method: Method + ) -> OrderedSet { + var allGenericParameters = OrderedSet(genericParameters) + + let typeParameters = method.getTypeParameters() + for typeParameter in typeParameters { + guard let typeParameter else { continue } + + guard genericParameterIsUsedInSignature(typeParameter, in: method) else { + continue + } + + allGenericParameters.append("\(typeParameter.getTypeName()): AnyJavaObject") + } + + return allGenericParameters + } + /// Translates the given Java method into a Swift declaration. package func renderMethod( _ javaMethod: Method, @@ -614,17 +636,7 @@ extension JavaClassTranslator { whereClause: String = "" ) throws -> DeclSyntax { // Map the generic params on the method. - var allGenericParameters = genericParameters - let typeParameters = javaMethod.getTypeParameters() - if typeParameters.contains(where: {$0 != nil }) { - allGenericParameters += typeParameters.compactMap { typeParam in - guard let typeParam else { return nil } - guard genericParameterIsUsedInSignature(typeParam, in: javaMethod) else { - return nil - } - return "\(typeParam.getTypeName()): AnyJavaObject" - } - } + let allGenericParameters = collectMethodGenericParameters(genericParameters: genericParameters, method: javaMethod) let genericParameterClauseStr = if allGenericParameters.isEmpty { "" @@ -643,11 +655,8 @@ extension JavaClassTranslator { preferValueTypes: true, outerOptional: .implicitlyUnwrappedOptional ) - // let resultType = try translator.getSwiftTypeNameAsString( - // javaMethod.getGenericReturnType()!, - // preferValueTypes: true, - // outerOptional: .implicitlyUnwrappedOptional - // ) + let hasTypeEraseGenericResultType: Bool = + isTypeErased(javaMethod.getGenericReturnType()) // FIXME: cleanup the checking here if resultType != "Void" && resultType != "Swift.Void" { @@ -659,9 +668,34 @@ extension JavaClassTranslator { // --- Handle other effects let throwsStr = javaMethod.throwsCheckedException ? "throws" : "" let swiftMethodName = javaMethod.getName().escapedSwiftName - let methodAttribute: AttributeSyntax = implementedInSwift - ? "" - : javaMethod.isStatic ? "@JavaStaticMethod\n" : "@JavaMethod\n"; + + // Compute the parameters for '@...JavaMethod(...)' + let methodAttribute: AttributeSyntax + if implementedInSwift { + methodAttribute = "" + } else { + var methodAttributeStr = + if javaMethod.isStatic { + "@JavaStaticMethod" + } else { + "@JavaMethod" + } + // Do we need to record any generic information, in order to enable type-erasure for the upcalls? + var parameters: [String] = [] + if hasTypeEraseGenericResultType { + parameters.append("typeErasedResult: \"\(resultType)\"") + } + // TODO: generic parameters? + + if !parameters.isEmpty { + methodAttributeStr += "(" + methodAttributeStr.append(parameters.joined(separator: ", ")) + methodAttributeStr += ")" + } + methodAttributeStr += "\n" + methodAttribute = "\(raw: methodAttributeStr)" + } + let accessModifier = implementedInSwift ? "" : (javaMethod.isStatic || !translateAsClass) ? "public " : "open " @@ -669,6 +703,7 @@ extension JavaClassTranslator { ? "override " : "" + // FIXME: refactor this so we don't have to duplicate the method signatures if resultType.optionalWrappedType() != nil || parameters.contains(where: { $0.type.trimmedDescription.optionalWrappedType() != nil }) { let parameters = parameters.map { param -> (clause: FunctionParameterSyntax, passedArg: String) in let name = param.secondName!.trimmedDescription @@ -690,7 +725,8 @@ extension JavaClassTranslator { } - return """ + return + """ \(methodAttribute)\(raw: accessModifier)\(raw: overrideOpt)func \(raw: swiftMethodName)\(raw: genericParameterClauseStr)(\(raw: parametersStr))\(raw: throwsStr)\(raw: resultTypeStr)\(raw: whereClause) \(raw: accessModifier)\(raw: overrideOpt)func \(raw: swiftMethodName)Optional\(raw: genericParameterClauseStr)(\(raw: parameters.map(\.clause.description).joined(separator: ", ")))\(raw: throwsStr) -> \(raw: resultOptional)\(raw: whereClause) { @@ -698,7 +734,8 @@ extension JavaClassTranslator { } """ } else { - return """ + return + """ \(methodAttribute)\(raw: accessModifier)\(raw: overrideOpt)func \(raw: swiftMethodName)\(raw: genericParameterClauseStr)(\(raw: parametersStr))\(raw: throwsStr)\(raw: resultTypeStr)\(raw: whereClause) """ } @@ -879,6 +916,7 @@ struct MethodCollector { } // MARK: Utility functions + extension JavaClassTranslator { /// Determine whether this method is an override of another Java /// method. @@ -914,7 +952,7 @@ extension JavaClassTranslator { return true } } catch { - // FIXME: logging + log.debug("Failed to determine if method '\(method)' is an override, error: \(error)") } } diff --git a/Sources/SwiftJavaToolLib/JavaGenericsSupport.swift b/Sources/SwiftJavaToolLib/JavaGenericsSupport.swift index 7c7819629..1d847276d 100644 --- a/Sources/SwiftJavaToolLib/JavaGenericsSupport.swift +++ b/Sources/SwiftJavaToolLib/JavaGenericsSupport.swift @@ -87,3 +87,25 @@ func isGenericJavaType(_ type: Type?) -> Bool { return false } + +/// Check if a type is type-erased att runtime. +/// +/// E.g. in a method returning a generic `T` the T is type erased and must +/// be represented as a `java.lang.Object` instead. +func isTypeErased(_ type: Type?) -> Bool { + guard let type else { + return false + } + + // Check if it's a type variable (e.g., T, E, etc.) + if type.as(TypeVariable.self) != nil { + return true + } + + // Check if it's a wildcard type (e.g., ? extends Number, ? super String) + if type.as(WildcardType.self) != nil { + return true + } + + return false +} diff --git a/Sources/SwiftJavaToolLib/JavaTranslator.swift b/Sources/SwiftJavaToolLib/JavaTranslator.swift index 71e011794..1c71afdae 100644 --- a/Sources/SwiftJavaToolLib/JavaTranslator.swift +++ b/Sources/SwiftJavaToolLib/JavaTranslator.swift @@ -133,17 +133,15 @@ extension JavaTranslator { preferValueTypes: Bool, outerOptional: OptionalKind ) throws -> String { - // let returnType = method.getReturnType() let genericReturnType = method.getGenericReturnType() // Special handle the case when the return type is the generic type of the method: ` T foo()` - // if isGenericJavaType(genericReturnType) { - // print("[swift] generic method! \(method.getDeclaringClass().getName()).\(method.getName())") - // getGenericJavaTypeOriginInfo(genericReturnType, from: method) - // } - - return try getSwiftTypeNameAsString(method: method, genericReturnType!, preferValueTypes: preferValueTypes, outerOptional: outerOptional) + return try getSwiftTypeNameAsString( + method: method, + genericReturnType!, + preferValueTypes: preferValueTypes, + outerOptional: outerOptional) } /// Turn a Java type into a string. diff --git a/Tests/SwiftJavaMacrosTests/JavaClassMacroTests.swift b/Tests/SwiftJavaMacrosTests/JavaClassMacroTests.swift index 7eead3d25..5cbeae162 100644 --- a/Tests/SwiftJavaMacrosTests/JavaClassMacroTests.swift +++ b/Tests/SwiftJavaMacrosTests/JavaClassMacroTests.swift @@ -296,5 +296,39 @@ class JavaKitMacroTests: XCTestCase { macros: Self.javaKitMacros ) } + + func testJavaOptionalGenericGet() throws { + assertMacroExpansion(""" + @JavaClass("java.lang.Optional") + open class JavaOptional: JavaObject { + @JavaMethod(typeErasedResult: "T") + open func get() -> T! + } + """, + expandedSource: """ + + open class JavaOptional: JavaObject { + open func get() -> T! { + /* convert erased return value to T */ + if let result$ = try! dynamicJavaMethodCall(methodName: "get", resultType: /*type-erased:T*/ JavaObject?.self) { + return T(javaThis: result$.javaThis, environment: try! JavaVirtualMachine.shared().environment()) + } else { + return nil + } + } + + /// The full Java class name for this Swift type. + open override class var fullJavaClassName: String { + "java.lang.Optional" + } + + public required init(javaHolder: JavaObjectHolder) { + super.init(javaHolder: javaHolder) + } + } + """, + macros: Self.javaKitMacros + ) + } } diff --git a/Tests/SwiftJavaToolLibTests/Java2SwiftTests.swift b/Tests/SwiftJavaToolLibTests/Java2SwiftTests.swift index e302fdc5a..a3b33ba75 100644 --- a/Tests/SwiftJavaToolLibTests/Java2SwiftTests.swift +++ b/Tests/SwiftJavaToolLibTests/Java2SwiftTests.swift @@ -262,7 +262,7 @@ class Java2SwiftTests: XCTestCase { public struct MyJavaObjects { """, """ - @JavaStaticMethod + @JavaStaticMethod(typeErasedResult: "T!") public func requireNonNull(_ arg0: T?, _ arg1: MySupplier?) -> T """, ] @@ -475,7 +475,7 @@ class Java2SwiftTests: XCTestCase { public struct MyJavaIntFunction { """, """ - @JavaMethod + @JavaMethod(typeErasedResult: "R!") public func apply(_ arg0: Int32) -> R! """, ] diff --git a/Tests/SwiftJavaToolLibTests/WrapJavaTests/BasicWrapJavaTests.swift b/Tests/SwiftJavaToolLibTests/WrapJavaTests/BasicWrapJavaTests.swift new file mode 100644 index 000000000..d4596c901 --- /dev/null +++ b/Tests/SwiftJavaToolLibTests/WrapJavaTests/BasicWrapJavaTests.swift @@ -0,0 +1,52 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@_spi(Testing) import SwiftJava +import SwiftJavaToolLib +import JavaUtilJar +import SwiftJavaShared +import JavaNet +import SwiftJavaConfigurationShared +import _Subprocess +import XCTest // NOTE: Workaround for https://github.com/swiftlang/swift-java/issues/43 + +final class BasicWrapJavaTests: XCTestCase { + + func testWrapJavaFromCompiledJavaSource() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + class ExampleSimpleClass {} + """) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.ExampleSimpleClass" + ], + classpath: [classpathURL], + expectedChunks: [ + """ + import CSwiftJavaJNI + import SwiftJava + """, + """ + @JavaClass("com.example.ExampleSimpleClass") + open class ExampleSimpleClass: JavaObject { + """ + ] + ) + } + +} \ No newline at end of file diff --git a/Tests/SwiftJavaToolLibTests/WrapJavaTests.swift b/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsWrapJavaTests.swift similarity index 67% rename from Tests/SwiftJavaToolLibTests/WrapJavaTests.swift rename to Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsWrapJavaTests.swift index 1c3d10d24..cba7b38f5 100644 --- a/Tests/SwiftJavaToolLibTests/WrapJavaTests.swift +++ b/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsWrapJavaTests.swift @@ -21,35 +21,8 @@ import SwiftJavaConfigurationShared import _Subprocess import XCTest // NOTE: Workaround for https://github.com/swiftlang/swift-java/issues/43 -final class WrapJavaTests: XCTestCase { +final class GenericsWrapJavaTests: XCTestCase { - func testWrapJavaFromCompiledJavaSource() async throws { - let classpathURL = try await compileJava( - """ - package com.example; - - class ExampleSimpleClass {} - """) - - try assertWrapJavaOutput( - javaClassNames: [ - "com.example.ExampleSimpleClass" - ], - classpath: [classpathURL], - expectedChunks: [ - """ - import CSwiftJavaJNI - import SwiftJava - """, - """ - @JavaClass("com.example.ExampleSimpleClass") - open class ExampleSimpleClass: JavaObject { - """ - ] - ) - } - - // @Test func testWrapJavaGenericMethod_singleGeneric() async throws { let classpathURL = try await compileJava( """ @@ -89,8 +62,8 @@ final class WrapJavaTests: XCTestCase { open class ExampleSimpleClass: JavaObject { """, """ - @JavaMethod - open func getGeneric(_ arg0: Item?) -> KeyType + @JavaMethod(typeErasedResult: "KeyType!") + open func getGeneric(_ arg0: Item?) -> KeyType! """, ] ) @@ -127,8 +100,8 @@ final class WrapJavaTests: XCTestCase { classpath: [classpathURL], expectedChunks: [ """ - @JavaMethod - open func getGeneric() -> KeyType + @JavaMethod(typeErasedResult: "KeyType!") + open func getGeneric() -> KeyType! """, ] ) @@ -189,8 +162,10 @@ final class WrapJavaTests: XCTestCase { """ package com.example; + // Mini decls in order to avoid warnings about some funcs we're not yet importing cleanly final class List {} final class Map {} + final class Number {} class GenericClass { public T getClassGeneric() { return null; } @@ -211,23 +186,46 @@ final class WrapJavaTests: XCTestCase { try assertWrapJavaOutput( javaClassNames: [ + "com.example.Map", + "com.example.List", + "com.example.Number", "com.example.GenericClass", ], classpath: [classpathURL], expectedChunks: [ + """ + @JavaMethod(typeErasedResult: "T!") + open func getClassGeneric() -> T! + """, + """ + @JavaMethod(typeErasedResult: "M!") + open func getMethodGeneric() -> M! + """, """ @JavaMethod - open func getClassGeneric() -> T + open func getMixedGeneric() -> Map! """, """ @JavaMethod open func getNonGeneric() -> String """, + """ + @JavaMethod + open func getParameterizedClassGeneric() -> List! + """, + """ + @JavaMethod + open func getWildcard() -> List! + """, + """ + @JavaMethod + open func getGenericArray() -> [T?] + """, ] ) } - func testGenericSuperclass() async throws { + func testWrapJavaGenericSuperclass() async throws { return // FIXME: we need this let classpathURL = try await compileJava( @@ -270,4 +268,106 @@ final class WrapJavaTests: XCTestCase { ] ) } -} + + func test_wrapJava_genericMethodTypeErasure_returnType() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + final class Kappa { + public T get() { return null; } + } + """) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.Kappa", + ], + classpath: [classpathURL], + expectedChunks: [ + """ + import CSwiftJavaJNI + import SwiftJava + """, + """ + @JavaClass("com.example.Kappa") + open class Kappa: JavaObject { + @JavaMethod(typeErasedResult: "T!") + open func get() -> T! + } + """ + ] + ) + } + + func test_wrapJava_genericMethodTypeErasure_ofNullableOptional_staticMethods() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + final class Optional { + public static Optional ofNullable(T value) { return null; } + + public static T nonNull(T value) { return null; } + } + """) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.Optional" + ], + classpath: [classpathURL], + expectedChunks: [ + """ + @JavaClass("com.example.Optional") + open class Optional: JavaObject { + """, + """ + extension JavaClass { + """, + """ + @JavaStaticMethod + public func ofNullable(_ arg0: T?) -> Optional! where ObjectType == Optional + } + """, + """ + @JavaStaticMethod(typeErasedResult: "T!") + public func nonNull(_ arg0: T?) -> T! where ObjectType == Optional + """ + ] + ) + } + + // TODO: this should be improved some more, we need to generated a `: Map` on the Swift side + func test_wrapJava_genericMethodTypeErasure_genericExtendsMap() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + final class Map {} + + final class Something { + public > M putIn(M map) { return null; } + } + """) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.Map", + "com.example.Something", + ], + classpath: [classpathURL], + expectedChunks: [ + """ + @JavaClass("com.example.Something") + open class Something: JavaObject { + """, + """ + @JavaMethod(typeErasedResult: "M!") + open func putIn(_ arg0: M?) -> M! + """, + ] + ) + } + +} \ No newline at end of file