From 9246e57650502e4a3feb55272bd269e1457526d6 Mon Sep 17 00:00:00 2001 From: Pierluigi Cifani Date: Tue, 3 Nov 2026 14:10:55 +0100 Subject: [PATCH 1/2] JNI: qualify enum payload types shadowed by Case record names Inside the generated `sealed interface Case`, each record's name shadows a type with the same simple name. For `case market(Market)` the record became `record Market(Market arg0)`, whose component referred to the record itself, and the generated Java didn't compile. The same happened when another case's record had the payload's name, including for arrays. Fully qualify package-less component types (types generated into this module's Java package) whose leading name matches a record, recursively through arrays and type arguments. Generic parameters and unshadowed payloads are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../Sources/MySwiftLibrary/Enums.swift | 11 +++ .../test/java/com/example/swift/EnumTest.java | 10 ++ ...t2JavaGenerator+JavaBindingsPrinting.swift | 37 +++++++- .../JNI/JNIEnumCaseNameShadowingTests.swift | 95 +++++++++++++++++++ 4 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 Tests/JExtractSwiftTests/JNI/JNIEnumCaseNameShadowingTests.swift diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Enums.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Enums.swift index be8f7445c..7514b10d0 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Enums.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Enums.swift @@ -37,3 +37,14 @@ public enum ComplexAssociatedValues { case optionalTypealiasedGeneric(id: MyIntID?) case array([String]) } + +/// A case named like its payload type: the Java record `Case.MySwiftStruct` would shadow +/// the `MySwiftStruct` class without qualification. +public enum StructHolder { + case empty + case mySwiftStruct(MySwiftStruct) + + public static func holding(len: Int64) -> StructHolder { + .mySwiftStruct(MySwiftStruct(cap: len, len: len)) + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EnumTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EnumTest.java index 275963981..7e9967c41 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EnumTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EnumTest.java @@ -95,4 +95,14 @@ void complexAssociatedValues_array() { }); } } + + @Test + void caseNamedLikeItsPayload() { + try (var arena = SwiftArena.ofConfined()) { + StructHolder holder = StructHolder.holding(3, arena); + StructHolder.Case c = holder.getCase(arena); + assertInstanceOf(StructHolder.Case.MySwiftStruct.class, c); + assertEquals(3, ((StructHolder.Case.MySwiftStruct) c).arg0().getLen()); + } + } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 504514f57..cac874099 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -665,14 +665,25 @@ extension JNISwift2JavaGenerator { "<\(decl.genericParameterNames.joined(separator: .comma))>" } + // Inside `Case`, each record's name shadows a type of the same simple name, so for + // `case market(Market)` the component `Market` would mean the record `Case.Market`. + let recordNames = Set(decl.cases.compactMap { self.translatedEnumCase(for: $0)?.name }) + let genericParameterNames = Set(decl.genericParameterNames) + printer.printBraceBlock("public sealed interface Case\(caseGenericClause)") { printer in for enumCase in decl.cases { guard let translatedCase = self.translatedEnumCase(for: enumCase) else { continue } - let members = translatedCase.parameters.map { - $0.renderParameter() + let members = translatedCase.parameters.map { parameter in + var parameter = parameter + if case .concrete(let type) = parameter.type { + parameter.type = .concrete( + qualifyingShadowedTypes(type, shadowedBy: recordNames, genericParameterNames: genericParameterNames) + ) + } + return parameter.renderParameter() } // Print record @@ -710,6 +721,28 @@ extension JNISwift2JavaGenerator { } } + /// Fully qualifies package-less class types whose leading name is shadowed by one of `names`. + /// Package-less types are the ones jextract generates into this module's Java package. + private func qualifyingShadowedTypes( + _ type: JavaType, + shadowedBy names: Set, + genericParameterNames: Set + ) -> JavaType { + switch type { + case .class(let package, let name, let typeParameters): + let typeParameters = typeParameters.map { + qualifyingShadowedTypes($0, shadowedBy: names, genericParameterNames: genericParameterNames) + } + let leadingName = String(name.prefix { $0 != "." }) + let isShadowed = package == nil && names.contains(leadingName) && !genericParameterNames.contains(leadingName) + return .class(package: isShadowed ? javaPackage : package, name: name, typeParameters: typeParameters) + case .array(let element): + return .array(qualifyingShadowedTypes(element, shadowedBy: names, genericParameterNames: genericParameterNames)) + default: + return type + } + } + private func printEnumStaticInitializers(_ printer: inout JavaPrinter, _ decl: ExtractedNominalType) { let isEffectivelyGeneric = decl.swiftNominal.isGeneric && !decl.isSpecialization if !decl.cases.isEmpty && isEffectivelyGeneric { diff --git a/Tests/JExtractSwiftTests/JNI/JNIEnumCaseNameShadowingTests.swift b/Tests/JExtractSwiftTests/JNI/JNIEnumCaseNameShadowingTests.swift new file mode 100644 index 000000000..a2e3f52c6 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIEnumCaseNameShadowingTests.swift @@ -0,0 +1,95 @@ +//===----------------------------------------------------------------------===// +// +// 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 JExtractSwiftLib +import Testing + +/// Inside the generated `Case` interface, each record name shadows a type with the same +/// simple name, so payload types with such a name must be fully qualified. +@Suite +struct JNIEnumCaseNameShadowingTests { + @Test + func caseNamedLikeItsPayload() throws { + try assertOutput( + input: """ + public struct Market {} + public enum Destination { + case home + case market(Market) + } + """, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public sealed interface Case { + record Home() implements Case {} + record Market(com.example.swift.Market arg0) implements Case {} + } + """ + ] + ) + } + + @Test + func anotherCasesRecordShadowsThePayload() throws { + try assertOutput( + input: """ + public struct Market {} + public enum Feed { + case market + case featured(Market) + case list([Market]) + } + """, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public sealed interface Case { + record Market() implements Case {} + record Featured(com.example.swift.Market arg0) implements Case {} + record List(com.example.swift.Market[] arg0) implements Case {} + } + """ + ] + ) + } + + @Test + func unshadowedPayloadsStayUnqualified() throws { + try assertOutput( + input: """ + public struct Item {} + public enum Route { + case home + case detail(Item) + } + """, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public sealed interface Case { + record Home() implements Case {} + record Detail(Item arg0) implements Case {} + } + """ + ] + ) + } +} From a0aa245cfe2c5d38d7ad6fc694cb426e61c4968e Mon Sep 17 00:00:00 2001 From: Pierluigi Cifani Date: Tue, 3 Nov 2026 15:36:34 +0100 Subject: [PATCH 2/2] Samples: enum cases named like their payload type, from Kotlin Co-Authored-By: Claude Opus 5.5 --- .../KotlinSampleLibrary/EnumCaseNames.swift | 41 +++++++++++++++++++ .../com/example/kotlin/EnumCaseNamesTest.kt | 40 ++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleLibrary/EnumCaseNames.swift create mode 100644 Samples/SwiftJavaExtractJNIKotlinSampleApp/src/test/kotlin/com/example/kotlin/EnumCaseNamesTest.kt diff --git a/Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleLibrary/EnumCaseNames.swift b/Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleLibrary/EnumCaseNames.swift new file mode 100644 index 000000000..d77ba84b8 --- /dev/null +++ b/Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleLibrary/EnumCaseNames.swift @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// Enum cases named like a payload type: inside the generated `Case` interface, the record +/// `Case.Market` would shadow the `Market` class. +public struct Market: Sendable { + public var id: String + + public init(id: String) { + self.id = id + } +} + +public enum Destination: Sendable { + case home + case market(Market) +} + +/// Another case's record has the payload's name. +public enum Feed: Sendable { + case market + case featured(Market) + case list([Market]) +} + +public enum Router { + public static func open(id: String) -> Destination { .market(Market(id: id)) } + public static func featured(id: String) -> Feed { .featured(Market(id: id)) } + public static func list(ids: [String]) -> Feed { .list(ids.map { Market(id: $0) }) } +} diff --git a/Samples/SwiftJavaExtractJNIKotlinSampleApp/src/test/kotlin/com/example/kotlin/EnumCaseNamesTest.kt b/Samples/SwiftJavaExtractJNIKotlinSampleApp/src/test/kotlin/com/example/kotlin/EnumCaseNamesTest.kt new file mode 100644 index 000000000..dfd206262 --- /dev/null +++ b/Samples/SwiftJavaExtractJNIKotlinSampleApp/src/test/kotlin/com/example/kotlin/EnumCaseNamesTest.kt @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +package com.example.kotlin + +import org.swift.swiftkit.core.SwiftArena +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class EnumCaseNamesTest { + @Test + fun caseNamedLikeItsPayload() { + val case = Router.open("7").getCase(SwiftArena.ofAuto()) + assertIs(case) + assertEquals("7", case.arg0.id) + } + + @Test + fun anotherCasesRecordNamedLikeThePayload() { + val featured = Router.featured("1").getCase(SwiftArena.ofAuto()) + assertIs(featured) + assertEquals("1", featured.arg0.id) + + val list = Router.list(arrayOf("a", "b")).getCase(SwiftArena.ofAuto()) + assertIs(list) + assertEquals(listOf("a", "b"), list.arg0.map { it.id }) + } +}