Skip to content
Merged
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,37 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024-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
//
//===----------------------------------------------------------------------===//

public struct Box<Element> {
public var count: Int64

public init(count: Int64) {
self.count = count
}
}

public struct Fish {
public var name: String

public init(name: String) {
self.name = name
}
}

extension Box where Element == Fish {
public func describeFish() -> String {
"A box of \(count) fish"
}
}

public typealias FishBox = Box<Fish>
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This triggers specialization for a new FishBox "type" on the java side 🐟

Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024-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.swift;

import org.junit.jupiter.api.Test;

import java.lang.reflect.Method;

import static org.junit.jupiter.api.Assertions.*;

public class BoxSpecializationTest {
@Test
void fishBoxHasExpectedMethods() throws Exception {
// Verify FishBox class exists and has the expected methods
Class<?> fishBoxClass = FishBox.class;
assertNotNull(fishBoxClass);

// Base type property getter
Method getCount = fishBoxClass.getMethod("getCount");
assertNotNull(getCount);
assertEquals(long.class, getCount.getReturnType());

// Base type property setter
Method setCount = fishBoxClass.getMethod("setCount", long.class);
assertNotNull(setCount);

// Constrained extension method (only on FishBox, not on Box)
Method describeFish = fishBoxClass.getMethod("describeFish");
assertNotNull(describeFish);
assertEquals(String.class, describeFish.getReturnType());
}

@Test
void fishBoxDoesNotHaveGenericTypeParameter() {
// FishBox is a concrete specialization — no generic type parameters
assertEquals(0, FishBox.class.getTypeParameters().length,
"FishBox should have no generic type parameters");
}

@Test
void boxHasGenericTypeParameter() {
// Box<Element> retains its generic parameter
assertEquals(1, Box.class.getTypeParameters().length,
"Box should have one generic type parameter");
assertEquals("Element", Box.class.getTypeParameters()[0].getName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ extension ImplicitlyUnwrappedOptionalTypeSyntax {
wrappedType: wrappedType,
self.unexpectedBetweenWrappedTypeAndExclamationMark,
self.unexpectedAfterExclamationMark,
trailingTrivia: self.trailingTrivia
trailingTrivia: self.trailingTrivia,
)
}
}
Expand Down Expand Up @@ -128,7 +128,9 @@ extension WithModifiersSyntax {
}

extension AttributeListSyntax.Element {
/// Whether this node has `SwiftJava` attributes.
/// Whether this node has `SwiftJava` wrapping attributes (types that wrap Java classes).
/// These are skipped during jextract because they represent Java->Swift wrappers.
/// Note: `@JavaExport` is NOT included here — it forces export of Swift types to Java.
var isJava: Bool {
guard case let .attribute(attr) = self else {
// FIXME: Handle #if.
Expand All @@ -143,6 +145,14 @@ extension AttributeListSyntax.Element {
return false
}
}

/// Whether this is a `@JavaExport` attribute (used on typealiases for specialization,
/// or on struct/class/enum to force-include them even when excluded by filters)
var isJavaExport: Bool {
guard case let .attribute(attr) = self else { return false }
guard let attrName = attr.attributeName.as(IdentifierTypeSyntax.self)?.name.text else { return false }
return attrName == "JavaExport"
}
}

extension DeclSyntaxProtocol {
Expand Down Expand Up @@ -260,7 +270,7 @@ extension DeclSyntaxProtocol {
.with(\.accessorBlock, nil)
.with(\.initializer, nil)
}
)
),
)
.triviaSanitizedDescription
case .enumCaseDecl(let node):
Expand Down
189 changes: 179 additions & 10 deletions Sources/JExtractSwiftLib/ImportedDecls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,38 +29,207 @@ package enum SwiftAPIKind: Equatable {

/// Describes a Swift nominal type (e.g., a class, struct, enum) that has been
/// imported and is being translated into Java.
///
/// When `base` is non-nil, this is a specialization of a generic type
/// (e.g. `FishBox` specializing `Box<Element>` with `Element` = `Fish`).
/// The specialization delegates its member collections to the base type
/// so that extensions discovered later are visible through all specializations.
package final class ImportedNominalType: ImportedDecl {
let swiftNominal: SwiftNominalTypeDeclaration

/// If this type is a specialization (FishTank), then this points at the Tank base type of the specialization.
/// His allows simplified
package let specializationBaseType: ImportedNominalType?

// The short path from module root to the file in which this nominal was originally declared.
// E.g. for `Sources/Example/My/Types.swift` it would be `My/Types.swift`.
package var sourceFilePath: String {
self.swiftNominal.sourceFilePath
}

package var initializers: [ImportedFunc] = []
package var methods: [ImportedFunc] = []
package var variables: [ImportedFunc] = []
package var cases: [ImportedEnumCase] = []
var inheritedTypes: [SwiftType]
package var parent: SwiftNominalTypeDeclaration?
// Backing storage for member collections
private var _initializers: [ImportedFunc] = []
private var _methods: [ImportedFunc] = []
private var _variables: [ImportedFunc] = []
private var _cases: [ImportedEnumCase] = []
private var _inheritedTypes: [SwiftType]
private var _parent: SwiftNominalTypeDeclaration?

// Additional members from constrained extensions that only apply to this specialization
package var constrainedInitializers: [ImportedFunc] = []
package var constrainedMethods: [ImportedFunc] = []
package var constrainedVariables: [ImportedFunc] = []

package var initializers: [ImportedFunc] {
get {
if let specializationBaseType { specializationBaseType.initializers + constrainedInitializers } else { _initializers }
}
set {
if let specializationBaseType {
let baseSet = Set(specializationBaseType.initializers.map { ObjectIdentifier($0) })
constrainedInitializers = newValue.filter { !baseSet.contains(ObjectIdentifier($0)) }
} else {
_initializers = newValue
}
}
}
package var methods: [ImportedFunc] {
get {
if let specializationBaseType { specializationBaseType.methods + constrainedMethods } else { _methods }
}
set {
if let specializationBaseType {
let baseSet = Set(specializationBaseType.methods.map { ObjectIdentifier($0) })
constrainedMethods = newValue.filter { !baseSet.contains(ObjectIdentifier($0)) }
} else {
_methods = newValue
}
}
}
package var variables: [ImportedFunc] {
get {
if let specializationBaseType { specializationBaseType.variables + constrainedVariables } else { _variables }
}
set {
if let specializationBaseType {
let baseSet = Set(specializationBaseType.variables.map { ObjectIdentifier($0) })
constrainedVariables = newValue.filter { !baseSet.contains(ObjectIdentifier($0)) }
} else {
_variables = newValue
}
}
}
package var cases: [ImportedEnumCase] {
get {
if let specializationBaseType { specializationBaseType.cases } else { _cases }
}
set {
if let specializationBaseType { specializationBaseType.cases = newValue } else { _cases = newValue }
}
}
var inheritedTypes: [SwiftType] {
get {
if let specializationBaseType { specializationBaseType.inheritedTypes } else { _inheritedTypes }
}
set {
if let specializationBaseType { specializationBaseType.inheritedTypes = newValue } else { _inheritedTypes = newValue }
}
}
package var parent: SwiftNominalTypeDeclaration? {
get {
if let specializationBaseType { specializationBaseType.parent } else { _parent }
}
set {
if let specializationBaseType { specializationBaseType.parent = newValue } else { _parent = newValue }
}
}

/// The Swift base type name, e.g. "Box" — always the unparameterized name
package var baseTypeName: String { swiftNominal.qualifiedName }

/// The specialized/Java-facing name, e.g. "FishBox" — nil for base types
package private(set) var specializedTypeName: String?

/// Whether this type is a specialization of a generic type
package var isSpecialization: Bool { specializationBaseType != nil }

/// Generic parameter names (e.g. ["Element"] for Box<Element>). Empty for non-generic types
package var genericParameterNames: [String] {
swiftNominal.genericParameters.map(\.name)
}

/// Maps generic parameter -> concrete type argument. Empty for unspecialized types
/// e.g. {"Element": "Fish"} for FishBox
package var genericArguments: [String: String] = [:]

/// True when all generic parameters have corresponding arguments
package var isFullySpecialized: Bool {
!genericParameterNames.isEmpty && genericParameterNames.allSatisfy { genericArguments.keys.contains($0) }
}

init(swiftNominal: SwiftNominalTypeDeclaration, lookupContext: SwiftTypeLookupContext) throws {
self.swiftNominal = swiftNominal
self.inheritedTypes =
self.specializationBaseType = nil
self._inheritedTypes =
swiftNominal.inheritanceTypes?.compactMap {
try? SwiftType($0.type, lookupContext: lookupContext)
} ?? []
self.parent = swiftNominal.parent
self._parent = swiftNominal.parent
}

/// Init for creating a specialization
private init(base: ImportedNominalType, specializedTypeName: String, genericArguments: [String: String]) {
self.swiftNominal = base.swiftNominal
self.specializationBaseType = base
self.specializedTypeName = specializedTypeName
self.genericArguments = genericArguments
self._inheritedTypes = []
}

var swiftType: SwiftType {
.nominal(.init(nominalTypeDecl: swiftNominal))
}

/// The effective Java-facing name — "FishBox" for specialized, "Box" for base
var effectiveJavaName: String {
specializedTypeName ?? swiftNominal.qualifiedName
}

/// The simple Java class name (no qualification) for file naming purposes
var effectiveJavaSimpleName: String {
specializedTypeName ?? swiftNominal.name
}

/// The Swift type for thunk generation — "Box<Fish>" for specialized, "Box" for base
/// Computed from baseTypeName + genericArguments
var effectiveSwiftTypeName: String {
guard !genericArguments.isEmpty else { return baseTypeName }
let orderedArgs = genericParameterNames.compactMap { genericArguments[$0] }
guard !orderedArgs.isEmpty else { return baseTypeName }
return "\(baseTypeName)<\(orderedArgs.joined(separator: ", "))>"
}

var qualifiedName: String {
self.swiftNominal.qualifiedName
}

/// The Java generic clause, e.g. "<Element>" for generic base types, "" for specialized or non-generic
var javaGenericClause: String {
if isSpecialization {
""
} else if genericParameterNames.isEmpty {
""
} else {
"<\(genericParameterNames.joined(separator: ", "))>"
}
}

/// Create a specialized version of this generic type
package func specialize(
as specializedName: String,
with substitutions: [String: String],
) throws -> ImportedNominalType {
guard !genericParameterNames.isEmpty else {
throw SpecializationError(
message: "Unable to specialize non-generic type '\(baseTypeName)' as '\(specializedName)'"
)
}
let missingParams = genericParameterNames.filter { substitutions[$0] == nil }
guard missingParams.isEmpty else {
throw SpecializationError(
message: "Missing type arguments for: \(missingParams) when specializing \(baseTypeName) as \(specializedName)"
)
}
return ImportedNominalType(
base: self,
specializedTypeName: specializedName,
genericArguments: substitutions,
)
}
}

struct SpecializationError: Error {
let message: String
}

public final class ImportedEnumCase: ImportedDecl, CustomStringConvertible {
Expand All @@ -82,7 +251,7 @@ public final class ImportedEnumCase: ImportedDecl, CustomStringConvertible {
parameters: [SwiftEnumCaseParameter],
swiftDecl: any DeclSyntaxProtocol,
enumType: SwiftNominalType,
caseFunction: ImportedFunc
caseFunction: ImportedFunc,
) {
self.name = name
self.parameters = parameters
Expand Down Expand Up @@ -191,7 +360,7 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible {
swiftDecl: any DeclSyntaxProtocol,
name: String,
apiKind: SwiftAPIKind,
functionSignature: SwiftFunctionSignature
functionSignature: SwiftFunctionSignature,
) {
self.module = module
self.name = name
Expand Down
2 changes: 1 addition & 1 deletion Sources/JExtractSwiftLib/JNI/JNICaching.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

enum JNICaching {
static func cacheName(for type: ImportedNominalType) -> String {
cacheName(for: type.swiftNominal.qualifiedName)
cacheName(for: type.effectiveJavaName)
}

static func cacheName(for type: SwiftNominalType) -> String {
Expand Down
Loading
Loading