From aca445af8264a25ec838266f776125473307373c Mon Sep 17 00:00:00 2001 From: Iceman Date: Mon, 11 May 2026 10:56:01 +0900 Subject: [PATCH 1/7] Conditional extension functions are not exported --- .../Sources/MySwiftLibrary/GenericType.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/GenericType.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/GenericType.swift index 0968489eb..967ac7364 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/GenericType.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/GenericType.swift @@ -85,3 +85,10 @@ public enum GenericEnum { public func makeIntGenericEnum() -> GenericEnum { if Bool.random() { return .foo } else { return .bar } } + +extension MyID where T: BinaryInteger { + // Conditional extension functions are not exported + public func computeSomeValue() -> Int { + Int(rawValue) + } +} From b5ed2a7fed7ea7f464f8c2964c26005d1e625b62 Mon Sep 17 00:00:00 2001 From: Iceman Date: Mon, 11 May 2026 11:38:13 +0900 Subject: [PATCH 2/7] Add constrainedExtensionsAreIgnored test case --- .../Sources/MySwiftLibrary/GenericType.swift | 10 +++++++ .../JNI/JNIGenericTypeTests.swift | 29 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/GenericType.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/GenericType.swift index 967ac7364..b94eb0cd7 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/GenericType.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/GenericType.swift @@ -92,3 +92,13 @@ extension MyID where T: BinaryInteger { Int(rawValue) } } + +extension MyID where T == Int128 { + // Conditional extension functions are not exported + public func decomposed() -> (high: Int64, low: Int64) { + let value = self.rawValue + let high = Int64(truncatingIfNeeded: value >> 64) + let low = Int64(bitPattern: UInt64(truncatingIfNeeded: value)) + return (high: high, low: low) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift b/Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift index 7c97ad398..0fedc683c 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift @@ -213,4 +213,33 @@ struct JNIGenericTypeTests { ] ) } + + @Test("Constrained extensions are ignored") + func constrainedExtensionsAreIgnored() throws { + let input = + #""" + public struct MyID {} + + extension MyID where T: BinaryInteger { + public func computeSomeValue() -> Int + } + extension MyID where T == Int128 { + public func decomposed() -> (high: Int64, low: Int64) + } + """# + + try assertOutput( + input: input, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + "public final class MyID implements JNISwiftInstance {", + ], + notExpectedChunks: [ + "computeSomeValue", + "decomposed", + ], + ) + } } From 6c7a18f57d6d94b0764ee9ffe56cadb00805a0c2 Mon Sep 17 00:00:00 2001 From: Iceman Date: Mon, 11 May 2026 12:02:40 +0900 Subject: [PATCH 3/7] More strict same-type constraint test case --- .../SpecializationTests.swift | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/Tests/JExtractSwiftTests/SpecializationTests.swift b/Tests/JExtractSwiftTests/SpecializationTests.swift index aa975dc91..0231422f6 100644 --- a/Tests/JExtractSwiftTests/SpecializationTests.swift +++ b/Tests/JExtractSwiftTests/SpecializationTests.swift @@ -46,10 +46,23 @@ struct SpecializationTests { public struct Tool { public var name: String } + + public struct Bait { + public var name: String + } extension Box where Element == Fish { public func observeTheFish() {} } + extension Box where Fish == Element { + public func swappedObserveTheFish() {} + } + extension Box where Element == Bait { + public func observeTheBait() {} + } + extension Box where Bait == Element { + public func swappedObserveTheBait() {} + } public typealias FishBox = Box public typealias ToolBox = Box @@ -141,13 +154,18 @@ struct SpecializationTests { "public static FishBox wrapMemoryAddressUnsafe(long selfPointer, SwiftArena swiftArena)", // Base method from Box "public long count()", - // Method body must call FishBox's own native method, not Box's "FishBox.$count(", // Constrained extension method (Element == Fish) "public void observeTheFish()", - // Constrained method body must also call FishBox's native method "FishBox.$observeTheFish(", + // Constrained extension method (Fish == Element) + "public void swappedObserveTheFish()", + "FishBox.$swappedObserveTheFish(", ], + notExpectedChunks: [ + "public void observeTheBait()", + "public void swappedObserveTheBait()" + ] ) } From 2dc28a5318ae26d592fe88126052f9f60644a836 Mon Sep 17 00:00:00 2001 From: Iceman Date: Mon, 11 May 2026 12:09:50 +0900 Subject: [PATCH 4/7] Carefully evaluates the where condition --- .../JExtractSwiftLib/Swift2JavaVisitor.swift | 66 ++++++++++++------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift index 0f625d24e..b8b1706e0 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift @@ -31,7 +31,11 @@ final class Swift2JavaVisitor { var log: Logger { translator.log } /// Constrained extensions deferred until specializations are applied - private var deferredConstrainedExtensions: [(ImportedNominalType, ExtensionDeclSyntax, String)] = [] + private var deferredConstrainedExtensions: [( + node: ExtensionDeclSyntax, + sourceFilePath: String, + sameConstraint: [(String, String)] + )] = [] func visit(inputFile: SwiftJavaInputFile) { let node = inputFile.syntax @@ -119,16 +123,22 @@ final class Swift2JavaVisitor { return } - // If the extension has where-clause constraints, defer it until specializations are applied - let whereConstraints = parseWhereConstraints(node.genericWhereClause) - if !whereConstraints.isEmpty { + switch parseWhereConstraints(node.genericWhereClause) { + case .none: + break + case .unsupported: + log.debug( + "Skip importing constrained extension '\(node.extendedType.trimmedDescription)'; unsupported where-clause requirements: \(node.genericWhereClause?.trimmedDescription ?? "")" + ) + return + case .sameType(let whereConstraints): let matchingSpecializations = findMatchingSpecializations( extendedType: importedNominalType, whereConstraints: whereConstraints, ) if matchingSpecializations.isEmpty { // Specializations may not exist yet — defer for later - deferredConstrainedExtensions.append((importedNominalType, node, sourceFilePath)) + deferredConstrainedExtensions.append((node, sourceFilePath, whereConstraints)) return } @@ -572,8 +582,10 @@ final class Swift2JavaVisitor { } // Process constrained extensions that were deferred - for (baseType, node, sourceFilePath) in deferredConstrainedExtensions { - let whereConstraints = parseWhereConstraints(node.genericWhereClause) + for (node, sourceFilePath, whereConstraints) in deferredConstrainedExtensions { + guard let baseType = translator.importedNominalType(node.extendedType) else { + continue + } let matchingSpecializations = findMatchingSpecializations( extendedType: baseType, whereConstraints: whereConstraints, @@ -594,24 +606,32 @@ final class Swift2JavaVisitor { // ==== ----------------------------------------------------------------------- // MARK: Constrained extension merging - /// Parse where clause constraints into a dictionary mapping param names to concrete types - private func parseWhereConstraints(_ whereClause: GenericWhereClauseSyntax?) -> [String: String] { - guard let whereClause else { return [:] } - var constraints: [String: String] = [:] + private enum ParsedWhereConstraints { + case none + case sameType([(String, String)]) + case unsupported + } + + private func parseWhereConstraints(_ whereClause: GenericWhereClauseSyntax?) -> ParsedWhereConstraints { + guard let whereClause else { return .none } + var constraints: [(String, String)] = [] for requirement in whereClause.requirements { - if case .sameTypeRequirement(let sameType) = requirement.requirement { + switch requirement.requirement { + case .sameTypeRequirement(let sameType): let lhs = sameType.leftType.trimmedDescription let rhs = sameType.rightType.trimmedDescription - constraints[lhs] = rhs + constraints.append((lhs, rhs)) + case .conformanceRequirement, .layoutRequirement: + return .unsupported } } - return constraints + return .sameType(constraints) } /// Find specializations whose type args match the given where-clause constraints private func findMatchingSpecializations( extendedType: ImportedNominalType, - whereConstraints: [String: String], + whereConstraints: [(String, String)], ) -> [ImportedNominalType] { guard let specializations = translator.specializations[extendedType] else { return [] @@ -623,18 +643,18 @@ final class Swift2JavaVisitor { /// Check if where clause constraints match a specialization's generic arguments private func constraintsMatchSpecialization( - _ constraints: [String: String], + _ constraints: [(String, String)], specialized: ImportedNominalType, ) -> Bool { - for (paramName, concreteType) in constraints { - if let expectedType = specialized.genericArguments[paramName] { - if expectedType != concreteType { - return false - } + for (lhs, rhs) in constraints { + if specialized.genericArguments[lhs] == rhs { + return true + } + if specialized.genericArguments[rhs] == lhs { + return true } - // If the param isn't in the mapping, we allow it (might be a secondary constraint) } - return true + return false } } From a79a6ad1200aeab05743854626b73cb9b638cab0 Mon Sep 17 00:00:00 2001 From: Iceman Date: Mon, 11 May 2026 12:10:17 +0900 Subject: [PATCH 5/7] swift format --- .../Sources/MySwiftLibrary/GenericType.swift | 8 ++++---- Sources/JExtractSwiftLib/Swift2JavaVisitor.swift | 11 ++++++----- .../JExtractSwiftTests/JNI/JNIGenericTypeTests.swift | 4 ++-- Tests/JExtractSwiftTests/SpecializationTests.swift | 4 ++-- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/GenericType.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/GenericType.swift index b94eb0cd7..62c0e6def 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/GenericType.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/GenericType.swift @@ -96,9 +96,9 @@ extension MyID where T: BinaryInteger { extension MyID where T == Int128 { // Conditional extension functions are not exported public func decomposed() -> (high: Int64, low: Int64) { - let value = self.rawValue - let high = Int64(truncatingIfNeeded: value >> 64) - let low = Int64(bitPattern: UInt64(truncatingIfNeeded: value)) - return (high: high, low: low) + let value = self.rawValue + let high = Int64(truncatingIfNeeded: value >> 64) + let low = Int64(bitPattern: UInt64(truncatingIfNeeded: value)) + return (high: high, low: low) } } diff --git a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift index b8b1706e0..8a67c367b 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift @@ -31,11 +31,12 @@ final class Swift2JavaVisitor { var log: Logger { translator.log } /// Constrained extensions deferred until specializations are applied - private var deferredConstrainedExtensions: [( - node: ExtensionDeclSyntax, - sourceFilePath: String, - sameConstraint: [(String, String)] - )] = [] + private var deferredConstrainedExtensions: + [( + node: ExtensionDeclSyntax, + sourceFilePath: String, + sameConstraint: [(String, String)] + )] = [] func visit(inputFile: SwiftJavaInputFile) { let node = inputFile.syntax diff --git a/Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift b/Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift index 0fedc683c..0aab1635d 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift @@ -219,7 +219,7 @@ struct JNIGenericTypeTests { let input = #""" public struct MyID {} - + extension MyID where T: BinaryInteger { public func computeSomeValue() -> Int } @@ -234,7 +234,7 @@ struct JNIGenericTypeTests { .java, detectChunkByInitialLines: 1, expectedChunks: [ - "public final class MyID implements JNISwiftInstance {", + "public final class MyID implements JNISwiftInstance {" ], notExpectedChunks: [ "computeSomeValue", diff --git a/Tests/JExtractSwiftTests/SpecializationTests.swift b/Tests/JExtractSwiftTests/SpecializationTests.swift index 0231422f6..e4f85ce22 100644 --- a/Tests/JExtractSwiftTests/SpecializationTests.swift +++ b/Tests/JExtractSwiftTests/SpecializationTests.swift @@ -46,7 +46,7 @@ struct SpecializationTests { public struct Tool { public var name: String } - + public struct Bait { public var name: String } @@ -164,7 +164,7 @@ struct SpecializationTests { ], notExpectedChunks: [ "public void observeTheBait()", - "public void swappedObserveTheBait()" + "public void swappedObserveTheBait()", ] ) } From 24ff62b769d1a947d4cef2a6a2710c56e4d259f3 Mon Sep 17 00:00:00 2001 From: Iceman Date: Mon, 11 May 2026 12:18:50 +0900 Subject: [PATCH 6/7] Skip native function header check --- Tests/JExtractSwiftTests/SpecializationTests.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Tests/JExtractSwiftTests/SpecializationTests.swift b/Tests/JExtractSwiftTests/SpecializationTests.swift index e4f85ce22..3ca1e6783 100644 --- a/Tests/JExtractSwiftTests/SpecializationTests.swift +++ b/Tests/JExtractSwiftTests/SpecializationTests.swift @@ -154,13 +154,10 @@ struct SpecializationTests { "public static FishBox wrapMemoryAddressUnsafe(long selfPointer, SwiftArena swiftArena)", // Base method from Box "public long count()", - "FishBox.$count(", // Constrained extension method (Element == Fish) "public void observeTheFish()", - "FishBox.$observeTheFish(", // Constrained extension method (Fish == Element) "public void swappedObserveTheFish()", - "FishBox.$swappedObserveTheFish(", ], notExpectedChunks: [ "public void observeTheBait()", From 6c11cf5e3d6f276c50b3f43689a757ec83660149 Mon Sep 17 00:00:00 2001 From: Iceman Date: Tue, 12 May 2026 10:23:12 +0900 Subject: [PATCH 7/7] Group a large tuple into type and formatting --- .../JExtractSwiftLib/Swift2JavaVisitor.swift | 32 +++++++++++-------- .../JNI/JNIGenericTypeTests.swift | 5 +-- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift index 8a67c367b..9e4a838b0 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift @@ -31,12 +31,12 @@ final class Swift2JavaVisitor { var log: Logger { translator.log } /// Constrained extensions deferred until specializations are applied - private var deferredConstrainedExtensions: - [( - node: ExtensionDeclSyntax, - sourceFilePath: String, - sameConstraint: [(String, String)] - )] = [] + private struct DeferredConstrainedExtension { + var node: ExtensionDeclSyntax + var sourceFilePath: String + var constraints: [(String, String)] + } + private var deferredConstrainedExtensions: [DeferredConstrainedExtension] = [] func visit(inputFile: SwiftJavaInputFile) { let node = inputFile.syntax @@ -139,7 +139,13 @@ final class Swift2JavaVisitor { ) if matchingSpecializations.isEmpty { // Specializations may not exist yet — defer for later - deferredConstrainedExtensions.append((node, sourceFilePath, whereConstraints)) + deferredConstrainedExtensions.append( + .init( + node: node, + sourceFilePath: sourceFilePath, + constraints: whereConstraints + ) + ) return } @@ -583,21 +589,21 @@ final class Swift2JavaVisitor { } // Process constrained extensions that were deferred - for (node, sourceFilePath, whereConstraints) in deferredConstrainedExtensions { - guard let baseType = translator.importedNominalType(node.extendedType) else { + for deferred in deferredConstrainedExtensions { + guard let baseType = translator.importedNominalType(deferred.node.extendedType) else { continue } let matchingSpecializations = findMatchingSpecializations( extendedType: baseType, - whereConstraints: whereConstraints, + whereConstraints: deferred.constraints, ) guard !matchingSpecializations.isEmpty else { - log.debug("Skipping deferred constrained extension of \(node.extendedType.trimmedDescription) — no matching specialization") + log.debug("Skipping deferred constrained extension of \(deferred.node.extendedType.trimmedDescription) — no matching specialization") continue } for specialized in matchingSpecializations { - for memberItem in node.memberBlock.members { - self.visit(decl: memberItem.decl, in: specialized, sourceFilePath: sourceFilePath) + for memberItem in deferred.node.memberBlock.members { + self.visit(decl: memberItem.decl, in: specialized, sourceFilePath: deferred.sourceFilePath) } } } diff --git a/Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift b/Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift index 53ac705d2..1b8202d71 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift @@ -219,12 +219,9 @@ struct JNIGenericTypeTests { let input = #""" public struct MyID {} - + public enum MyEnum { case foo(MyID) - - - } """#