From b9f14f4b98637e58125bce79507fafb224f691b6 Mon Sep 17 00:00:00 2001 From: Iceman Date: Sat, 23 May 2026 17:19:19 +0900 Subject: [PATCH 1/4] Propagate enum generic parameters to case type --- ...t2JavaGenerator+JavaBindingsPrinting.swift | 23 ++++++++++++++----- .../JNI/JNIGenericTypeTests.swift | 8 +++---- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index ff114b22..e02a7f76 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/Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift b/Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift index 7570580a..96f92fcf 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(); }; } """, From 843b84a04c3729e4da66643a72e6d28e5e785233 Mon Sep 17 00:00:00 2001 From: Iceman Date: Sat, 23 May 2026 17:53:43 +0900 Subject: [PATCH 2/4] Translate owner generic parameter type --- Sources/SwiftJavaToolLib/JavaTranslator.swift | 36 ++++++++++++++++++ .../JavaTranslatorTests.swift | 38 ++++++++++++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftJavaToolLib/JavaTranslator.swift b/Sources/SwiftJavaToolLib/JavaTranslator.swift index 0020e99a..3cb9a7ab 100644 --- a/Sources/SwiftJavaToolLib/JavaTranslator.swift +++ b/Sources/SwiftJavaToolLib/JavaTranslator.swift @@ -224,6 +224,38 @@ extension JavaTranslator { optionalSuffix = "" } + if let ownerType = parameterizedType.getOwnerType() { + var ownerSwiftType = try getSwiftTypeNameAsString( + method: method, + ownerType, + substitution: substitution, + preferValueTypes: false, + outerOptional: .nonoptional, + eraseTypeArguments: eraseTypeArguments + ) + + if ownerType.as(ParameterizedType.self) == nil, + let ownerClass = ownerType.as(JavaClass.self), + ownerClass.getDeclaringClass() != nil + { + var ownerTypeArguments: [String] = [] + for typeParam in ownerClass.getTypeParameters() { + guard let typeParam else { continue } + if eraseTypeArguments { + ownerTypeArguments.append("JavaObject") + } else { + ownerTypeArguments.append(typeParam.getName()) + } + } + + if !ownerTypeArguments.isEmpty { + ownerSwiftType += "<\(ownerTypeArguments.joined(separator: ", "))>" + } + } + + rawSwiftType = "\(ownerSwiftType).\(rawSwiftType.splitSwiftTypeName().name)" + } + let typeArguments: [String] = try parameterizedType.getActualTypeArguments().compactMap { typeArg in guard let typeArg else { return nil } if eraseTypeArguments { @@ -252,6 +284,10 @@ extension JavaTranslator { return mappedSwiftName } + if typeArguments.isEmpty { + return "\(rawSwiftType)\(optionalSuffix)" + } + return "\(rawSwiftType)<\(typeArguments.joined(separator: ", "))>\(optionalSuffix)" } } diff --git a/Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift b/Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift index 02c41b0a..514651ad 100644 --- a/Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift +++ b/Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift @@ -22,7 +22,7 @@ import XCTest // NOTE: Workaround for https://github.com/swiftlang/swift-java/is class JavaTranslatorTests: XCTestCase { - func translateGenericMethodParameters() async throws { + func testTranslateGenericMethodParameters() async throws { let classpathURL = try await compileJava( """ package com.example; @@ -56,4 +56,40 @@ class JavaTranslatorTests: XCTestCase { } } + + func testTranslateNestedParameterizedTypes() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + class MyOptional { + interface Case { + final class None implements Case { + None() {} + } + } + + 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!", + ] + ) + } } From 6ffbd4accc497c8bb709eae5361e48d92541cf7b Mon Sep 17 00:00:00 2001 From: Iceman Date: Sat, 23 May 2026 22:24:51 +0900 Subject: [PATCH 3/4] erase raw owner type argument --- Sources/SwiftJavaToolLib/JavaTranslator.swift | 77 +++++++++++-------- .../JavaTranslatorTests.swift | 41 ++++++++-- 2 files changed, 79 insertions(+), 39 deletions(-) diff --git a/Sources/SwiftJavaToolLib/JavaTranslator.swift b/Sources/SwiftJavaToolLib/JavaTranslator.swift index 3cb9a7ab..46682bae 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 @@ -225,34 +232,16 @@ extension JavaTranslator { } if let ownerType = parameterizedType.getOwnerType() { - var ownerSwiftType = try getSwiftTypeNameAsString( - method: method, - ownerType, - substitution: substitution, - preferValueTypes: false, - outerOptional: .nonoptional, - eraseTypeArguments: eraseTypeArguments - ) - - if ownerType.as(ParameterizedType.self) == nil, - let ownerClass = ownerType.as(JavaClass.self), - ownerClass.getDeclaringClass() != nil - { - var ownerTypeArguments: [String] = [] - for typeParam in ownerClass.getTypeParameters() { - guard let typeParam else { continue } - if eraseTypeArguments { - ownerTypeArguments.append("JavaObject") - } else { - ownerTypeArguments.append(typeParam.getName()) - } - } - - if !ownerTypeArguments.isEmpty { - ownerSwiftType += "<\(ownerTypeArguments.joined(separator: ", "))>" - } - } - + 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)" } @@ -267,7 +256,9 @@ extension JavaTranslator { typeArg, substitution: substitution, preferValueTypes: false, - outerOptional: .nonoptional + outerOptional: .nonoptional, + eraseTypeArguments: eraseTypeArguments, + eraseRawOwnerTypeArguments: eraseRawOwnerTypeArguments ) // FIXME: improve the get instead... @@ -297,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/SwiftJavaToolLibTests/JavaTranslatorTests.swift b/Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift index 514651ad..598e4de5 100644 --- a/Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift +++ b/Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift @@ -58,15 +58,46 @@ 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 { - final class None implements Case { - None() {} - } + record None() implements Case {} } Case getCase() { return null; } @@ -87,8 +118,8 @@ class JavaTranslatorTests: XCTestCase { ], classpath: [classpathURL], expectedChunks: [ - "func getCase() -> MyOptional.Case!", - "func getAsNone() -> MyOptional.Case.None!", + "func getCase() -> MyOptional.Case!", + "func getAsNone() -> MyOptional.Case.None!", ] ) } From 90f8791bd7b13b3735ffccf4ad2cf224a75ea32b Mon Sep 17 00:00:00 2001 From: Iceman Date: Tue, 26 May 2026 11:23:17 +0900 Subject: [PATCH 4/4] resume test name --- Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift b/Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift index 598e4de5..4577af8e 100644 --- a/Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift +++ b/Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift @@ -22,7 +22,7 @@ import XCTest // NOTE: Workaround for https://github.com/swiftlang/swift-java/is class JavaTranslatorTests: XCTestCase { - func testTranslateGenericMethodParameters() async throws { + func translateGenericMethodParameters() async throws { let classpathURL = try await compileJava( """ package com.example;