Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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) }) }
}
Original file line number Diff line number Diff line change
@@ -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<Destination.Case.Market>(case)
assertEquals("7", case.arg0.id)
}

@Test
fun anotherCasesRecordNamedLikeThePayload() {
val featured = Router.featured("1").getCase(SwiftArena.ofAuto())
assertIs<Feed.Case.Featured>(featured)
assertEquals("1", featured.arg0.id)

val list = Router.list(arrayOf("a", "b")).getCase(SwiftArena.ofAuto())
assertIs<Feed.Case.List>(list)
assertEquals(listOf("a", "b"), list.arg0.map { it.id })
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<String>,
genericParameterNames: Set<String>
) -> 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 {
Expand Down
95 changes: 95 additions & 0 deletions Tests/JExtractSwiftTests/JNI/JNIEnumCaseNameShadowingTests.swift
Original file line number Diff line number Diff line change
@@ -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 {}
}
"""
]
)
}
}
Loading