diff --git a/Sources/JavaStdlib/JavaUtilFunction/generated/JavaBinaryOperator.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaBinaryOperator.swift index 7ee22097..f085d61f 100644 --- a/Sources/JavaStdlib/JavaUtilFunction/generated/JavaBinaryOperator.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaBinaryOperator.swift @@ -2,18 +2,23 @@ import SwiftJava import SwiftJavaJNICore -@JavaInterface( - "java.util.function.BinaryOperator", - extends: JavaBiFunction.self -) +@JavaInterface("java.util.function.BinaryOperator", extends: JavaBiFunction.self) public struct JavaBinaryOperator { - @JavaMethod - public func apply(_ arg0: JavaObject?, _ arg1: JavaObject?) -> JavaObject? + /// Java method `apply`. + /// + /// ### Java method signature + /// ```java + /// public abstract R java.util.function.BiFunction.apply(T,U) + /// ``` + @JavaMethod(typeErasedResult: "T!") + public func apply(_ arg0: T?, _ arg1: T?) -> T! + /// Java method `andThen`. + /// + /// ### Java method signature + /// ```java + /// public default java.util.function.BiFunction java.util.function.BiFunction.andThen(java.util.function.Function) + /// ``` @JavaMethod - public func andThen( - _ arg0: JavaFunction? - ) -> JavaBiFunction< - JavaObject, JavaObject, JavaObject - >? + public func andThen(_ arg0: JavaFunction?) -> JavaBiFunction! } diff --git a/Sources/JavaStdlib/JavaUtilFunction/generated/JavaFunction.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaFunction.swift index 612266c4..1f60edbf 100644 --- a/Sources/JavaStdlib/JavaUtilFunction/generated/JavaFunction.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaFunction.swift @@ -4,26 +4,40 @@ import SwiftJavaJNICore @JavaInterface("java.util.function.Function") public struct JavaFunction { - @JavaMethod - public func apply(_ arg0: JavaObject?) -> JavaObject? + /// Java method `apply`. + /// + /// ### Java method signature + /// ```java + /// public abstract R java.util.function.Function.apply(T) + /// ``` + @JavaMethod(typeErasedResult: "R!") + public func apply(_ arg0: T?) -> R! + /// Java method `compose`. + /// + /// ### Java method signature + /// ```java + /// public default java.util.function.Function java.util.function.Function.compose(java.util.function.Function) + /// ``` @JavaMethod - public func compose( - _ arg0: JavaFunction? - ) -> JavaFunction< - JavaObject, JavaObject - >? + public func compose(_ arg0: JavaFunction?) -> JavaFunction! + /// Java method `andThen`. + /// + /// ### Java method signature + /// ```java + /// public default java.util.function.Function java.util.function.Function.andThen(java.util.function.Function) + /// ``` @JavaMethod - public func andThen( - _ arg0: JavaFunction? - ) -> JavaFunction< - JavaObject, JavaObject - >? + public func andThen(_ arg0: JavaFunction?) -> JavaFunction! } extension JavaClass { + /// Java method `identity`. + /// + /// ### Java method signature + /// ```java + /// public static java.util.function.Function java.util.function.Function.identity() + /// ``` @JavaStaticMethod - public func identity() -> JavaFunction< - JavaObject, JavaObject - >? where ObjectType == JavaFunction + public func identity() -> JavaFunction! where ObjectType == JavaFunction } diff --git a/Sources/JavaStdlib/JavaUtilFunction/generated/JavaUnaryOperator.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaUnaryOperator.swift index 9db34cbc..d674ead5 100644 --- a/Sources/JavaStdlib/JavaUtilFunction/generated/JavaUnaryOperator.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaUnaryOperator.swift @@ -2,30 +2,42 @@ import SwiftJava import SwiftJavaJNICore -@JavaInterface( - "java.util.function.UnaryOperator", - extends: JavaFunction.self -) +@JavaInterface("java.util.function.UnaryOperator", extends: JavaFunction.self) public struct JavaUnaryOperator { - @JavaMethod - public func apply(_ arg0: JavaObject?) -> JavaObject? + /// Java method `apply`. + /// + /// ### Java method signature + /// ```java + /// public abstract R java.util.function.Function.apply(T) + /// ``` + @JavaMethod(typeErasedResult: "T!") + public func apply(_ arg0: T?) -> T! + /// Java method `compose`. + /// + /// ### Java method signature + /// ```java + /// public default java.util.function.Function java.util.function.Function.compose(java.util.function.Function) + /// ``` @JavaMethod - public func compose( - _ arg0: JavaFunction? - ) -> JavaFunction< - JavaObject, JavaObject - >? + public func compose(_ arg0: JavaFunction?) -> JavaFunction! + /// Java method `andThen`. + /// + /// ### Java method signature + /// ```java + /// public default java.util.function.Function java.util.function.Function.andThen(java.util.function.Function) + /// ``` @JavaMethod - public func andThen( - _ arg0: JavaFunction? - ) -> JavaFunction< - JavaObject, JavaObject - >? + public func andThen(_ arg0: JavaFunction?) -> JavaFunction! } extension JavaClass { + /// Java method `identity`. + /// + /// ### Java method signature + /// ```java + /// public static java.util.function.UnaryOperator java.util.function.UnaryOperator.identity() + /// ``` @JavaStaticMethod - public func identity() -> JavaUnaryOperator? - where ObjectType == JavaUnaryOperator + public func identity() -> JavaUnaryOperator! where ObjectType == JavaUnaryOperator } diff --git a/Sources/SwiftJavaToolLib/JavaClassTranslator.swift b/Sources/SwiftJavaToolLib/JavaClassTranslator.swift index 7717d81d..31bc2e56 100644 --- a/Sources/SwiftJavaToolLib/JavaClassTranslator.swift +++ b/Sources/SwiftJavaToolLib/JavaClassTranslator.swift @@ -59,6 +59,9 @@ struct JavaClassTranslator { /// The Swift names of the interfaces that this class implements. let swiftInterfaces: [String] + /// Substitution map for resolving generic types. + let substitution: SubstitutionMap + /// The annotations of the Java class. /// In other words, RUNTIME retained annotations, visible through reflection. let annotations: [Annotation] @@ -138,6 +141,9 @@ struct JavaClassTranslator { self.javaTypeParameters = javaClass.getTypeParameters().compactMap { $0 } self.nestedClasses = translator.nestedClasses[fullName] ?? [] + // Generic substitution. + self.substitution = SubstitutionMap(startingFrom: javaClass) + // Superclass, incl parameter types (if any) if !javaClass.isInterface() { var javaSuperclass = javaClass.getSuperclass() @@ -149,12 +155,13 @@ struct JavaClassTranslator { swiftSuperclassName = try translator.getSwiftTypeName(javaSuperclassNonOpt, preferValueTypes: false).swiftName if let javaGenericSuperclass = javaGenericSuperclass?.as(ParameterizedType.self) { for typeArg in javaGenericSuperclass.getActualTypeArguments() { - let javaTypeArgName = typeArg?.getTypeName() ?? "" - if let swiftTypeArgName = self.translator.translatedClasses[javaTypeArgName] { - swiftSuperclassTypeArgs.append(swiftTypeArgName.qualifiedName) - } else { - swiftSuperclassTypeArgs.append("/* MISSING MAPPING FOR */ \(javaTypeArgName)") - } + let mappedSwiftName = try translator.getSwiftTypeNameAsString( + typeArg!, + substitution: substitution, + preferValueTypes: false, + outerOptional: .nonoptional + ) + swiftSuperclassTypeArgs.append(mappedSwiftName) } } break @@ -185,6 +192,7 @@ struct JavaClassTranslator { do { return try translator.getSwiftTypeNameAsString( javaType, + substitution: nil, preferValueTypes: false, outerOptional: .nonoptional, eraseTypeArguments: true @@ -920,6 +928,7 @@ extension JavaClassTranslator { let resultTypeStr: String let resultType = try translator.getSwiftReturnTypeNameAsString( method: javaMethod, + substitution: substitution, preferValueTypes: true, outerOptional: .implicitlyUnwrappedOptional ) @@ -1050,6 +1059,7 @@ extension JavaClassTranslator { package func renderField(_ javaField: Field) throws -> DeclSyntax { let typeName = try translator.getSwiftTypeNameAsString( javaField.getGenericType()!, + substitution: substitution, preferValueTypes: true, outerOptional: .implicitlyUnwrappedOptional ) @@ -1163,6 +1173,7 @@ extension JavaClassTranslator { let typeName = try translator.getSwiftTypeNameAsString( method: javaMethod, javaParameter.getParameterizedType()!, + substitution: substitution, preferValueTypes: true, outerOptional: .optional ) @@ -1181,6 +1192,7 @@ extension JavaClassTranslator { let typeName = try translator.getSwiftTypeNameAsString( javaParameter.getParameterizedType()!, + substitution: substitution, preferValueTypes: true, outerOptional: .optional ) diff --git a/Sources/SwiftJavaToolLib/JavaGenericsSupport.swift b/Sources/SwiftJavaToolLib/JavaGenericsSupport.swift index 7c0e78db..361675d7 100644 --- a/Sources/SwiftJavaToolLib/JavaGenericsSupport.swift +++ b/Sources/SwiftJavaToolLib/JavaGenericsSupport.swift @@ -109,3 +109,68 @@ func isTypeErased(_ type: Type?) -> Bool { return false } + +/// Handles generic type substitution by mapping type variables of ancestor classes +/// to actual types provided in 'extends' or 'implements' clauses. +struct SubstitutionMap { + private struct MapKey: Hashable { + var declaringClassName: String + var typeVariableName: String + } + private var mapping: [MapKey: Type] = [:] + + init(startingFrom javaClass: JavaClass) { + buildSubstitutionMap(for: javaClass) + } + + private mutating func buildSubstitutionMap(for currentClass: JavaClass) { + var genericParents: [Type] = currentClass.getGenericInterfaces().compactMap { $0 } + if let genericSuperclass = currentClass.getGenericSuperclass() { + genericParents.append(genericSuperclass) + } + + for genericParent in genericParents { + guard let parameterizedParent = genericParent.as(ParameterizedType.self) else { + // If the parent is not parameterized, still check its ancestors recursively + if let rawParent = genericParent.as(JavaClass.self) { + buildSubstitutionMap(for: rawParent) + } + continue + } + guard let rawParent = parameterizedParent.getRawType()?.as(JavaClass.self) else { + continue + } + + let typeParameters = rawParent.getTypeParameters() + let actualTypeArguments = parameterizedParent.getActualTypeArguments() + + for (typeParam, actualArg) in zip(typeParameters, actualTypeArguments) { + guard let typeParam, let actualArg else { continue } + + let key = MapKey( + declaringClassName: rawParent.getName(), + typeVariableName: typeParam.getName() + ) + mapping[key] = actualArg + } + + buildSubstitutionMap(for: rawParent) + } + } + + func resolve(_ type: Type) -> Type? { + if let typeVar = type.as(TypeVariable.self), + let declClass = typeVar.getGenericDeclaration().as(JavaClass.self) + { + let key = MapKey( + declaringClassName: declClass.getName(), + typeVariableName: typeVar.getName() + ) + if let substituted = mapping[key] { + // Recursively resolve if the substituted type is also a type variable. + return resolve(substituted) ?? substituted + } + } + return nil + } +} diff --git a/Sources/SwiftJavaToolLib/JavaTranslator.swift b/Sources/SwiftJavaToolLib/JavaTranslator.swift index c19b2fc1..03a5e557 100644 --- a/Sources/SwiftJavaToolLib/JavaTranslator.swift +++ b/Sources/SwiftJavaToolLib/JavaTranslator.swift @@ -135,6 +135,7 @@ extension JavaTranslator { func getSwiftReturnTypeNameAsString( method: JavaLangReflect.Method, + substitution: SubstitutionMap?, preferValueTypes: Bool, outerOptional: OptionalKind ) throws -> String { @@ -145,6 +146,7 @@ extension JavaTranslator { return try getSwiftTypeNameAsString( method: method, genericReturnType!, + substitution: substitution, preferValueTypes: preferValueTypes, outerOptional: outerOptional ) @@ -154,14 +156,18 @@ extension JavaTranslator { func getSwiftTypeNameAsString( method: JavaLangReflect.Method? = nil, _ javaType: Type, + substitution: SubstitutionMap?, preferValueTypes: Bool, outerOptional: OptionalKind, eraseTypeArguments: Bool = false ) throws -> String { + // Replace if it is a type variable and we have a substitution for it. + let javaType = substitution?.resolve(javaType) ?? javaType + // Replace type variables with their bounds. if let typeVariable = javaType.as(TypeVariable.self), typeVariable.getBounds().count == 1, - let bound = typeVariable.getBounds()[0] + typeVariable.getBounds()[0] != nil { return outerOptional.adjustTypeName(typeVariable.getName()) } @@ -174,6 +180,7 @@ extension JavaTranslator { // Replace a wildcard type with its first bound. return try getSwiftTypeNameAsString( bound, + substitution: substitution, preferValueTypes: preferValueTypes, outerOptional: outerOptional ) @@ -184,6 +191,7 @@ extension JavaTranslator { if preferValueTypes { let elementType = try getSwiftTypeNameAsString( arrayType.getGenericComponentType()!, + substitution: substitution, preferValueTypes: preferValueTypes, outerOptional: .optional ) @@ -204,6 +212,7 @@ extension JavaTranslator { if let rawJavaType = parameterizedType.getRawType() { var rawSwiftType = try getSwiftTypeNameAsString( rawJavaType, + substitution: substitution, preferValueTypes: false, outerOptional: outerOptional ) @@ -225,12 +234,13 @@ extension JavaTranslator { let mappedSwiftName = try getSwiftTypeNameAsString( method: method, typeArg, + substitution: substitution, preferValueTypes: false, outerOptional: .nonoptional ) // FIXME: improve the get instead... - if mappedSwiftName == nil || mappedSwiftName == "JavaObject" { + if mappedSwiftName == "JavaObject" { // Try to salvage it, is it perhaps a type parameter? if let method { let typeParameters = method.getTypeParameters() as [TypeVariable?] diff --git a/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsSubstitutionWrapJavaTests.swift b/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsSubstitutionWrapJavaTests.swift new file mode 100644 index 00000000..174ef4c6 --- /dev/null +++ b/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsSubstitutionWrapJavaTests.swift @@ -0,0 +1,196 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 JavaNet +import JavaUtilJar +import Subprocess +@_spi(Testing) import SwiftJava +import SwiftJavaConfigurationShared +import SwiftJavaShared +import SwiftJavaToolLib +import XCTest // NOTE: Workaround for https://github.com/swiftlang/swift-java/issues/43 + +final class GenericsSubstitutionWrapJavaTests: XCTestCase { + func testInterface() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + interface MyFunction { + R apply(T t); + } + + interface MyUnaryOperator extends MyFunction { + } + """ + ) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.MyFunction", + "com.example.MyUnaryOperator", + ], + classpath: [classpathURL], + expectedChunks: [ + """ + @JavaInterface("com.example.MyFunction") + public struct MyFunction { + """, + """ + @JavaMethod(typeErasedResult: "R!") + public func apply(_ arg0: T?) -> R! + """, + """ + @JavaInterface("com.example.MyUnaryOperator", extends: MyFunction.self) + public struct MyUnaryOperator { + """, + """ + @JavaMethod(typeErasedResult: "T!") + public func apply(_ arg0: T?) -> T! + """, + ] + ) + } + + func testClass() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + abstract class ClassFunction { + abstract R apply(T t); + } + + class ClassUnaryOperator extends ClassFunction { + public T apply(T t) { return t; } + } + """ + ) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.ClassFunction", + "com.example.ClassUnaryOperator", + ], + classpath: [classpathURL], + expectedChunks: [ + """ + @JavaClass("com.example.ClassFunction") + open class ClassFunction: JavaObject { + """, + """ + @JavaMethod(typeErasedResult: "R!") + open func apply(_ arg0: T?) -> R! + """, + """ + @JavaClass("com.example.ClassUnaryOperator") + open class ClassUnaryOperator: ClassFunction { + """, + """ + @JavaMethod(typeErasedResult: "T!") + open override func apply(_ arg0: T?) -> T! + """, + ] + ) + } + + func testNestedParameter() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + class String {} + interface Foo {} + interface Bar {} + interface Baz {} + interface TakeTwo { + void takeTwo(T t, U u); + } + + interface MyInterface extends TakeTwo>, Baz> { + } + """ + ) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.String", + "com.example.Foo", + "com.example.Bar", + "com.example.Baz", + "com.example.TakeTwo", + "com.example.MyInterface", + ], + classpath: [classpathURL], + expectedChunks: [ + """ + @JavaInterface("com.example.MyInterface", extends: TakeTwo.self) + public struct MyInterface { + """, + """ + @JavaMethod + public func takeTwo(_ arg0: Bar>?, _ arg1: Baz?) + """, + ] + ) + } + + func testDuplicatedParameterName() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + class String {} + class Integer {} + + interface Foo { + void foo(T t); + } + interface Bar { + void bar(T t); + } + + interface MyInterface extends Foo, Bar { + void foo(String t); + void bar(Integer t); + } + """ + ) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.String", + "com.example.Integer", + "com.example.Foo", + "com.example.Bar", + "com.example.MyInterface", + ], + classpath: [classpathURL], + expectedChunks: [ + """ + @JavaInterface("com.example.MyInterface", extends: Foo.self, Bar.self) + public struct MyInterface { + """, + """ + @JavaMethod + public func foo(_ arg0: String?) + """, + """ + @JavaMethod + public func bar(_ arg0: Integer?) + """, + ] + ) + } +}