From b15c62cd52ce8e73b30779d9006bbf8cb2029997 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 12 May 2026 11:36:33 +0900 Subject: [PATCH] Fix extension constraint matching All same type constraints must match, not just one of them. This is a follow up after https://github.com/swiftlang/swift-java/pull/734#discussion_r3223126771 https://github.com/swiftlang/swift-java/pull/734 --- .../JExtractSwiftLib/Swift2JavaVisitor.swift | 14 ++- .../JNI/JNIGenericTypeTests.swift | 98 +++++++++++++++++++ 2 files changed, 104 insertions(+), 8 deletions(-) diff --git a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift index 9e4a838b..275b48af 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift @@ -648,20 +648,18 @@ final class Swift2JavaVisitor { } } - /// Check if where clause constraints match a specialization's generic arguments + /// Check if where clause constraints match a specialization's generic arguments. + /// Same-type where-clauses are conjunctive: every constraint must hold. private func constraintsMatchSpecialization( _ constraints: [(String, String)], specialized: ImportedNominalType, ) -> Bool { for (lhs, rhs) in constraints { - if specialized.genericArguments[lhs] == rhs { - return true - } - if specialized.genericArguments[rhs] == lhs { - return true - } + if specialized.genericArguments[lhs] == rhs { continue } + if specialized.genericArguments[rhs] == lhs { continue } + return false } - return false + return true } } diff --git a/Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift b/Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift index 1b8202d7..cdc712ed 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift @@ -281,4 +281,102 @@ struct JNIGenericTypeTests { ], ) } + + @Test("Constrained extensions are ignored, unless specialized") + func constrainedExtensionsAreIgnoredUnlessSpecialized() throws { + let input = + #""" + public struct Fish { + public var name: String + } + public struct Tool { + public var name: String + } + + public protocol Animal {} + extension Fish: Animal {} + + public struct Tank { + public var contents: T + } + + extension Tank where T: Animal { // currently not supported yet + public func feed() {} + } + extension Tank where T == Fish { + public func observeTheFish() {} + } + extension Tank where T == Tool { + public func useTheTool() {} + } + + public typealias FishTank = Tank + """# + + try assertOutput( + input: input, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + "public final class FishTank implements JNISwiftInstance {", + "observeTheFish", + ], + notExpectedChunks: [ + "feed", + "useTheTool", + ], + ) + } + + @Test("Multi-constraint extensions require all constraints to match") + func multiConstraintExtensionsRequireAllToMatch() throws { + let input = + #""" + public struct Fish { + public var name: String + } + public struct Tool { + public var name: String + } + public struct Bait { + public var name: String + } + + public struct Pair { + public var first: A + public var second: B + } + + extension Pair where A == Fish, B == Tool { + // OK: Both constraints match FishToolPair + public func bothMatch() {} + } + extension Pair where A == Fish, B == Bait { + // NOPE: Only A matches FishToolPair + public func onlyAMatches() {} + } + extension Pair where A == Tool, B == Tool { + // NOPE: Only B matches FishToolPair + public func onlyBMatches() {} + } + + public typealias FishToolPair = Pair + """# + + try assertOutput( + input: input, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + "public final class FishToolPair implements JNISwiftInstance {", + "bothMatch", + ], + notExpectedChunks: [ + "onlyAMatches", + "onlyBMatches", + ], + ) + } }