diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index ff114b228..e02a7f767 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -494,7 +494,14 @@ extension JNISwift2JavaGenerator { return } - printer.printBraceBlock("public sealed interface Case") { printer in + let caseGenericClause = + if decl.genericParameterNames.isEmpty { + "" + } else { + "<\(decl.genericParameterNames.joined(separator: ", "))>" + } + + printer.printBraceBlock("public sealed interface Case\(caseGenericClause)") { printer in for enumCase in decl.cases { guard let translatedCase = self.translatedEnumCase(for: enumCase) else { continue @@ -505,7 +512,7 @@ extension JNISwift2JavaGenerator { } // Print record - printer.print("record \(translatedCase.name)(\(members.joined(separator: ", "))) implements Case {}") + printer.print("record \(translatedCase.name)\(caseGenericClause)(\(members.joined(separator: ", "))) implements Case\(caseGenericClause) {}") } } printer.println() @@ -514,13 +521,13 @@ extension JNISwift2JavaGenerator { self.translatedEnumCase(for: $0) }.contains(where: \.requiresSwiftArena) - printer.printBraceBlock("public Case getCase(\(requiresSwiftArena ? "SwiftArena swiftArena" : ""))") { printer in + printer.printBraceBlock("public Case\(caseGenericClause) getCase(\(requiresSwiftArena ? "SwiftArena swiftArena" : ""))") { printer in printer.printBraceBlock("return switch (this.getDiscriminator())", .semicolonNewLine) { printer in for enumCase in decl.cases { if let translatedCase = self.translatedEnumCase(for: enumCase) { if enumCase.parameters.isEmpty { printer.print( - "case \(enumCase.name.uppercased()) -> new Case.\(translatedCase.name)();" + "case \(enumCase.name.uppercased()) -> new Case.\(translatedCase.name)\(caseGenericClause)();" ) } else { let arenaArgument = translatedCase.requiresSwiftArena ? "swiftArena" : "" @@ -552,12 +559,16 @@ extension JNISwift2JavaGenerator { } private func printEnumCases(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + let caseTypeParameters: [JavaType] = decl.genericParameterNames.map { + .class(package: nil, name: $0) + } + for enumCase in decl.cases { guard let translatedCase = self.translatedEnumCase(for: enumCase) else { continue } - let caseType = JavaType.class(package: nil, name: "Case.\(translatedCase.name)") + let caseType = JavaType.class(package: nil, name: "Case.\(translatedCase.name)", typeParameters: caseTypeParameters) let resultType = JavaType.optional(caseType) if let getAsCaseFunctionDecl = translatedCase.getAsCaseFunction, var getAsCaseFunction = self.translatedDecl(for: getAsCaseFunctionDecl) @@ -591,7 +602,7 @@ extension JNISwift2JavaGenerator { if (getDiscriminator() != Discriminator.\(enumCase.name.uppercased())) { return java.util.Optional.empty(); } - return java.util.Optional.of(new Case.\(translatedCase.name)()); + return java.util.Optional.of(new Case.\(translatedCase.name)\(caseTypeParameters.isEmpty ? "" : "<>")()); } """ ) diff --git a/Sources/SwiftJavaToolLib/JavaTranslator.swift b/Sources/SwiftJavaToolLib/JavaTranslator.swift index 0020e99a2..46682bae3 100644 --- a/Sources/SwiftJavaToolLib/JavaTranslator.swift +++ b/Sources/SwiftJavaToolLib/JavaTranslator.swift @@ -158,7 +158,8 @@ extension JavaTranslator { substitution: SubstitutionMap?, preferValueTypes: Bool, outerOptional: OptionalKind, - eraseTypeArguments: Bool = false + eraseTypeArguments: Bool = false, + eraseRawOwnerTypeArguments: Bool = false ) throws -> String { // Replace if it is a type variable and we have a substitution for it. let javaType = substitution?.resolve(javaType) ?? javaType @@ -181,7 +182,9 @@ extension JavaTranslator { bound, substitution: substitution, preferValueTypes: preferValueTypes, - outerOptional: outerOptional + outerOptional: outerOptional, + eraseTypeArguments: eraseTypeArguments, + eraseRawOwnerTypeArguments: eraseRawOwnerTypeArguments ) } @@ -192,7 +195,9 @@ extension JavaTranslator { arrayType.getGenericComponentType()!, substitution: substitution, preferValueTypes: preferValueTypes, - outerOptional: .optional + outerOptional: .optional, + eraseTypeArguments: eraseTypeArguments, + eraseRawOwnerTypeArguments: eraseRawOwnerTypeArguments ) return "[\(elementType)]" } @@ -213,7 +218,9 @@ extension JavaTranslator { rawJavaType, substitution: substitution, preferValueTypes: false, - outerOptional: outerOptional + outerOptional: outerOptional, + eraseTypeArguments: false, + eraseRawOwnerTypeArguments: false ) let optionalSuffix: String @@ -224,6 +231,20 @@ extension JavaTranslator { optionalSuffix = "" } + if let ownerType = parameterizedType.getOwnerType() { + let ownerSwiftType = + try getSwiftTypeNameAsString( + method: method, + ownerType, + substitution: substitution, + preferValueTypes: false, + outerOptional: .nonoptional, + eraseTypeArguments: eraseTypeArguments, + eraseRawOwnerTypeArguments: ownerType.is(JavaClass.self) + ) + rawSwiftType = "\(ownerSwiftType).\(rawSwiftType.splitSwiftTypeName().name)" + } + let typeArguments: [String] = try parameterizedType.getActualTypeArguments().compactMap { typeArg in guard let typeArg else { return nil } if eraseTypeArguments { @@ -235,7 +256,9 @@ extension JavaTranslator { typeArg, substitution: substitution, preferValueTypes: false, - outerOptional: .nonoptional + outerOptional: .nonoptional, + eraseTypeArguments: eraseTypeArguments, + eraseRawOwnerTypeArguments: eraseRawOwnerTypeArguments ) // FIXME: improve the get instead... @@ -252,6 +275,10 @@ extension JavaTranslator { return mappedSwiftName } + if typeArguments.isEmpty { + return "\(rawSwiftType)\(optionalSuffix)" + } + return "\(rawSwiftType)<\(typeArguments.joined(separator: ", "))>\(optionalSuffix)" } } @@ -261,7 +288,25 @@ extension JavaTranslator { throw TranslationError.unhandledJavaType(javaType) } - let (swiftName, isOptional) = try getSwiftTypeName(javaClass, preferValueTypes: preferValueTypes) + var (swiftName, isOptional) = try getSwiftTypeName(javaClass, preferValueTypes: preferValueTypes) + if eraseRawOwnerTypeArguments, let declaringClass = javaClass.getDeclaringClass() { + let ownerSwiftType = try getSwiftTypeNameAsString( + declaringClass.as(Type.self), + substitution: substitution, + preferValueTypes: preferValueTypes, + outerOptional: .nonoptional, + eraseTypeArguments: eraseTypeArguments, + eraseRawOwnerTypeArguments: true + ) + swiftName = "\(ownerSwiftType).\(swiftName.splitSwiftTypeName().name)" + } + + if eraseTypeArguments || eraseRawOwnerTypeArguments { + let typeParameterCount = javaClass.getTypeParameters().count + if typeParameterCount > 0 { + swiftName += "<\((0.." + } + } let resultString = if isOptional { outerOptional.adjustTypeName(swiftName) diff --git a/Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift b/Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift index 7570580a0..96f92fcf7 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift @@ -319,15 +319,15 @@ struct JNIGenericTypeTests { } """, """ - public sealed interface Case { - record None() implements Case {} + public sealed interface Case { + record None() implements Case {} } """, """ - public Case getCase() { + public Case getCase() { return switch (this.getDiscriminator()) { case SOME -> throw new UnsupportedOperationException("MyOptional.some contains unsupported values."); - case NONE -> new Case.None(); + case NONE -> new Case.None(); }; } """, diff --git a/Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift b/Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift index 02c41b0aa..4577af8e4 100644 --- a/Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift +++ b/Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift @@ -56,4 +56,71 @@ class JavaTranslatorTests: XCTestCase { } } + + func testTranslateNestedParameterizedTypes() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + class MyString {} + class MyInt {} + + class Outer { + class Inner { + } + + Outer.Inner foo() { return null; } + Outer.Inner bar() { return null; } + } + """ + ) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.MyString", + "com.example.MyInt", + "com.example.Outer$Inner", + "com.example.Outer", + ], + classpath: [classpathURL], + expectedChunks: [ + "func foo() -> Outer.Inner!", + "func bar() -> Outer.Inner!", + ] + ) + } + + func testTranslateNestedParameterizedStaticTypes() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + class MyOptional { + interface Case { + record None() implements Case {} + } + + Case getCase() { return null; } + Case.None getAsNone() { return null; } + } + """ + ) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.MyOptional$Case", + "com.example.MyOptional$Case$None", + "com.example.MyOptional", + ], + classNameMappings: [ + "com.example.MyOptional$Case": "MyOptional.Case", + "com.example.MyOptional$Case$None": "MyOptional.Case.None", + ], + classpath: [classpathURL], + expectedChunks: [ + "func getCase() -> MyOptional.Case!", + "func getAsNone() -> MyOptional.Case.None!", + ] + ) + } }