Skip to content

Commit d2cb92e

Browse files
authored
jextract/jni: Propagate generic parameters to Case types of generic enums (#765)
* Propagate enum generic parameters to case type * Translate owner generic parameter type * erase raw owner type argument * resume test name
1 parent b600c61 commit d2cb92e

4 files changed

Lines changed: 139 additions & 16 deletions

File tree

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,14 @@ extension JNISwift2JavaGenerator {
494494
return
495495
}
496496

497-
printer.printBraceBlock("public sealed interface Case") { printer in
497+
let caseGenericClause =
498+
if decl.genericParameterNames.isEmpty {
499+
""
500+
} else {
501+
"<\(decl.genericParameterNames.joined(separator: ", "))>"
502+
}
503+
504+
printer.printBraceBlock("public sealed interface Case\(caseGenericClause)") { printer in
498505
for enumCase in decl.cases {
499506
guard let translatedCase = self.translatedEnumCase(for: enumCase) else {
500507
continue
@@ -505,7 +512,7 @@ extension JNISwift2JavaGenerator {
505512
}
506513

507514
// Print record
508-
printer.print("record \(translatedCase.name)(\(members.joined(separator: ", "))) implements Case {}")
515+
printer.print("record \(translatedCase.name)\(caseGenericClause)(\(members.joined(separator: ", "))) implements Case\(caseGenericClause) {}")
509516
}
510517
}
511518
printer.println()
@@ -514,13 +521,13 @@ extension JNISwift2JavaGenerator {
514521
self.translatedEnumCase(for: $0)
515522
}.contains(where: \.requiresSwiftArena)
516523

517-
printer.printBraceBlock("public Case getCase(\(requiresSwiftArena ? "SwiftArena swiftArena" : ""))") { printer in
524+
printer.printBraceBlock("public Case\(caseGenericClause) getCase(\(requiresSwiftArena ? "SwiftArena swiftArena" : ""))") { printer in
518525
printer.printBraceBlock("return switch (this.getDiscriminator())", .semicolonNewLine) { printer in
519526
for enumCase in decl.cases {
520527
if let translatedCase = self.translatedEnumCase(for: enumCase) {
521528
if enumCase.parameters.isEmpty {
522529
printer.print(
523-
"case \(enumCase.name.uppercased()) -> new Case.\(translatedCase.name)();"
530+
"case \(enumCase.name.uppercased()) -> new Case.\(translatedCase.name)\(caseGenericClause)();"
524531
)
525532
} else {
526533
let arenaArgument = translatedCase.requiresSwiftArena ? "swiftArena" : ""
@@ -552,12 +559,16 @@ extension JNISwift2JavaGenerator {
552559
}
553560

554561
private func printEnumCases(_ printer: inout CodePrinter, _ decl: ImportedNominalType) {
562+
let caseTypeParameters: [JavaType] = decl.genericParameterNames.map {
563+
.class(package: nil, name: $0)
564+
}
565+
555566
for enumCase in decl.cases {
556567
guard let translatedCase = self.translatedEnumCase(for: enumCase) else {
557568
continue
558569
}
559570

560-
let caseType = JavaType.class(package: nil, name: "Case.\(translatedCase.name)")
571+
let caseType = JavaType.class(package: nil, name: "Case.\(translatedCase.name)", typeParameters: caseTypeParameters)
561572
let resultType = JavaType.optional(caseType)
562573
if let getAsCaseFunctionDecl = translatedCase.getAsCaseFunction,
563574
var getAsCaseFunction = self.translatedDecl(for: getAsCaseFunctionDecl)
@@ -591,7 +602,7 @@ extension JNISwift2JavaGenerator {
591602
if (getDiscriminator() != Discriminator.\(enumCase.name.uppercased())) {
592603
return java.util.Optional.empty();
593604
}
594-
return java.util.Optional.of(new Case.\(translatedCase.name)());
605+
return java.util.Optional.of(new Case.\(translatedCase.name)\(caseTypeParameters.isEmpty ? "" : "<>")());
595606
}
596607
"""
597608
)

Sources/SwiftJavaToolLib/JavaTranslator.swift

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,8 @@ extension JavaTranslator {
158158
substitution: SubstitutionMap?,
159159
preferValueTypes: Bool,
160160
outerOptional: OptionalKind,
161-
eraseTypeArguments: Bool = false
161+
eraseTypeArguments: Bool = false,
162+
eraseRawOwnerTypeArguments: Bool = false
162163
) throws -> String {
163164
// Replace if it is a type variable and we have a substitution for it.
164165
let javaType = substitution?.resolve(javaType) ?? javaType
@@ -181,7 +182,9 @@ extension JavaTranslator {
181182
bound,
182183
substitution: substitution,
183184
preferValueTypes: preferValueTypes,
184-
outerOptional: outerOptional
185+
outerOptional: outerOptional,
186+
eraseTypeArguments: eraseTypeArguments,
187+
eraseRawOwnerTypeArguments: eraseRawOwnerTypeArguments
185188
)
186189
}
187190

@@ -192,7 +195,9 @@ extension JavaTranslator {
192195
arrayType.getGenericComponentType()!,
193196
substitution: substitution,
194197
preferValueTypes: preferValueTypes,
195-
outerOptional: .optional
198+
outerOptional: .optional,
199+
eraseTypeArguments: eraseTypeArguments,
200+
eraseRawOwnerTypeArguments: eraseRawOwnerTypeArguments
196201
)
197202
return "[\(elementType)]"
198203
}
@@ -213,7 +218,9 @@ extension JavaTranslator {
213218
rawJavaType,
214219
substitution: substitution,
215220
preferValueTypes: false,
216-
outerOptional: outerOptional
221+
outerOptional: outerOptional,
222+
eraseTypeArguments: false,
223+
eraseRawOwnerTypeArguments: false
217224
)
218225

219226
let optionalSuffix: String
@@ -224,6 +231,20 @@ extension JavaTranslator {
224231
optionalSuffix = ""
225232
}
226233

234+
if let ownerType = parameterizedType.getOwnerType() {
235+
let ownerSwiftType =
236+
try getSwiftTypeNameAsString(
237+
method: method,
238+
ownerType,
239+
substitution: substitution,
240+
preferValueTypes: false,
241+
outerOptional: .nonoptional,
242+
eraseTypeArguments: eraseTypeArguments,
243+
eraseRawOwnerTypeArguments: ownerType.is(JavaClass<JavaObject>.self)
244+
)
245+
rawSwiftType = "\(ownerSwiftType).\(rawSwiftType.splitSwiftTypeName().name)"
246+
}
247+
227248
let typeArguments: [String] = try parameterizedType.getActualTypeArguments().compactMap { typeArg in
228249
guard let typeArg else { return nil }
229250
if eraseTypeArguments {
@@ -235,7 +256,9 @@ extension JavaTranslator {
235256
typeArg,
236257
substitution: substitution,
237258
preferValueTypes: false,
238-
outerOptional: .nonoptional
259+
outerOptional: .nonoptional,
260+
eraseTypeArguments: eraseTypeArguments,
261+
eraseRawOwnerTypeArguments: eraseRawOwnerTypeArguments
239262
)
240263

241264
// FIXME: improve the get instead...
@@ -252,6 +275,10 @@ extension JavaTranslator {
252275
return mappedSwiftName
253276
}
254277

278+
if typeArguments.isEmpty {
279+
return "\(rawSwiftType)\(optionalSuffix)"
280+
}
281+
255282
return "\(rawSwiftType)<\(typeArguments.joined(separator: ", "))>\(optionalSuffix)"
256283
}
257284
}
@@ -261,7 +288,25 @@ extension JavaTranslator {
261288
throw TranslationError.unhandledJavaType(javaType)
262289
}
263290

264-
let (swiftName, isOptional) = try getSwiftTypeName(javaClass, preferValueTypes: preferValueTypes)
291+
var (swiftName, isOptional) = try getSwiftTypeName(javaClass, preferValueTypes: preferValueTypes)
292+
if eraseRawOwnerTypeArguments, let declaringClass = javaClass.getDeclaringClass() {
293+
let ownerSwiftType = try getSwiftTypeNameAsString(
294+
declaringClass.as(Type.self),
295+
substitution: substitution,
296+
preferValueTypes: preferValueTypes,
297+
outerOptional: .nonoptional,
298+
eraseTypeArguments: eraseTypeArguments,
299+
eraseRawOwnerTypeArguments: true
300+
)
301+
swiftName = "\(ownerSwiftType).\(swiftName.splitSwiftTypeName().name)"
302+
}
303+
304+
if eraseTypeArguments || eraseRawOwnerTypeArguments {
305+
let typeParameterCount = javaClass.getTypeParameters().count
306+
if typeParameterCount > 0 {
307+
swiftName += "<\((0..<typeParameterCount).map { _ in "JavaObject" }.joined(separator: ", "))>"
308+
}
309+
}
265310
let resultString =
266311
if isOptional {
267312
outerOptional.adjustTypeName(swiftName)

Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -319,15 +319,15 @@ struct JNIGenericTypeTests {
319319
}
320320
""",
321321
"""
322-
public sealed interface Case {
323-
record None() implements Case {}
322+
public sealed interface Case<Wrapped> {
323+
record None<Wrapped>() implements Case<Wrapped> {}
324324
}
325325
""",
326326
"""
327-
public Case getCase() {
327+
public Case<Wrapped> getCase() {
328328
return switch (this.getDiscriminator()) {
329329
case SOME -> throw new UnsupportedOperationException("MyOptional.some contains unsupported values.");
330-
case NONE -> new Case.None();
330+
case NONE -> new Case.None<Wrapped>();
331331
};
332332
}
333333
""",

Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,71 @@ class JavaTranslatorTests: XCTestCase {
5656

5757
}
5858
}
59+
60+
func testTranslateNestedParameterizedTypes() async throws {
61+
let classpathURL = try await compileJava(
62+
"""
63+
package com.example;
64+
65+
class MyString {}
66+
class MyInt {}
67+
68+
class Outer<T> {
69+
class Inner<U> {
70+
}
71+
72+
Outer<MyString>.Inner<MyInt> foo() { return null; }
73+
Outer<MyInt>.Inner<T> bar() { return null; }
74+
}
75+
"""
76+
)
77+
78+
try assertWrapJavaOutput(
79+
javaClassNames: [
80+
"com.example.MyString",
81+
"com.example.MyInt",
82+
"com.example.Outer$Inner",
83+
"com.example.Outer",
84+
],
85+
classpath: [classpathURL],
86+
expectedChunks: [
87+
"func foo() -> Outer<MyString>.Inner<MyInt>!",
88+
"func bar() -> Outer<MyInt>.Inner<T>!",
89+
]
90+
)
91+
}
92+
93+
func testTranslateNestedParameterizedStaticTypes() async throws {
94+
let classpathURL = try await compileJava(
95+
"""
96+
package com.example;
97+
98+
class MyOptional<Wrapped> {
99+
interface Case<Wrapped> {
100+
record None<Wrapped>() implements Case<Wrapped> {}
101+
}
102+
103+
Case<Wrapped> getCase() { return null; }
104+
Case.None<Wrapped> getAsNone() { return null; }
105+
}
106+
"""
107+
)
108+
109+
try assertWrapJavaOutput(
110+
javaClassNames: [
111+
"com.example.MyOptional$Case",
112+
"com.example.MyOptional$Case$None",
113+
"com.example.MyOptional",
114+
],
115+
classNameMappings: [
116+
"com.example.MyOptional$Case": "MyOptional.Case",
117+
"com.example.MyOptional$Case$None": "MyOptional.Case.None",
118+
],
119+
classpath: [classpathURL],
120+
expectedChunks: [
121+
"func getCase() -> MyOptional<JavaObject>.Case<Wrapped>!",
122+
"func getAsNone() -> MyOptional<JavaObject>.Case<JavaObject>.None<Wrapped>!",
123+
]
124+
)
125+
}
59126
}

0 commit comments

Comments
 (0)