From ddc563d685c3c9b86b4a4b72c45633c4d01a4617 Mon Sep 17 00:00:00 2001 From: Stephan Diederich Date: Fri, 6 Mar 2026 11:50:43 +0100 Subject: [PATCH 1/2] fix duplicate properties from inherited interfaces previously we fixes duplicated properties from classes, but missed them from interfaces. This fixes that & adds a tests --- .../JavaClassTranslator.swift | 5 +++ .../WrapJavaTests/BasicWrapJavaTests.swift | 37 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/Sources/SwiftJavaToolLib/JavaClassTranslator.swift b/Sources/SwiftJavaToolLib/JavaClassTranslator.swift index a30a4f51..1eb33929 100644 --- a/Sources/SwiftJavaToolLib/JavaClassTranslator.swift +++ b/Sources/SwiftJavaToolLib/JavaClassTranslator.swift @@ -307,6 +307,11 @@ extension JavaClassTranslator { // Static fields go into a separate list. if field.isStatic { + // Deduplicate by name: getFields() can return the same field from both an + // interface and its super-interface (e.g. serialVersionUID on Key and PublicKey). + guard !staticFields.contains(where: { $0.getName() == field.getName() }) else { + return + } staticFields.append(field) // Enum constants will be used to produce a Swift enum projecting the diff --git a/Tests/SwiftJavaToolLibTests/WrapJavaTests/BasicWrapJavaTests.swift b/Tests/SwiftJavaToolLibTests/WrapJavaTests/BasicWrapJavaTests.swift index d54b7d8a..9aa65f6f 100644 --- a/Tests/SwiftJavaToolLibTests/WrapJavaTests/BasicWrapJavaTests.swift +++ b/Tests/SwiftJavaToolLibTests/WrapJavaTests/BasicWrapJavaTests.swift @@ -168,6 +168,43 @@ final class BasicWrapJavaTests: XCTestCase { ) } + // Test that static fields are not duplicated when both a Java interface and its + // super-interface independently declare the same field name. + // + // Real-world case: java.security.PublicKey extends java.security.Key, and both + // declare serialVersionUID. Class.getFields() returns both Field objects (with + // different declaring classes), which previously caused two @JavaStaticField + // declarations to be emitted in extension JavaClass. + // + // Note: uses real JDK classes rather than compileJava() — the duplicate only + // manifests with JDK bytecode; freshly compiled interfaces apply stricter + // field-hiding rules that prevent getFields() from returning both fields. + // + // The closing `}` in the expected chunk is load-bearing: if there are two + // serialVersionUID declarations the `}` would be preceded by the second field, + // not the first, so the chunk would not match. + func test_wrapJava_noDuplicateStaticFieldsFromSuperInterface() async throws { + let classpathURL = try await compileJava("class Dummy {}") + try assertWrapJavaOutput( + javaClassNames: [ + "java.security.Key", + "java.security.PublicKey", + ], + classpath: [classpathURL], + expectedChunks: [ + // PublicKey should close its extension block right after one serialVersionUID. + // A duplicate would insert a second field before the `}`, breaking this match. + """ + extension JavaClass { + @available(*, deprecated) + @JavaStaticField(isFinal: true) + public var serialVersionUID: Int64 + } + """, + ] + ) + } + // Test that Java methods named "init" get @JavaMethod("init") annotation. // Since "init" is a Swift keyword and gets escaped with backticks in the function name, // we explicitly specify the Java method name in the annotation. From 58d3e0cd4d9cdc847a347e52eb260521fc2758bf Mon Sep 17 00:00:00 2001 From: Stephan Diederich Date: Fri, 6 Mar 2026 13:39:04 +0100 Subject: [PATCH 2/2] Update Tests/SwiftJavaToolLibTests/WrapJavaTests/BasicWrapJavaTests.swift Co-authored-by: Konrad `ktoso` Malawski --- .../WrapJavaTests/BasicWrapJavaTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SwiftJavaToolLibTests/WrapJavaTests/BasicWrapJavaTests.swift b/Tests/SwiftJavaToolLibTests/WrapJavaTests/BasicWrapJavaTests.swift index 9aa65f6f..cd21bfad 100644 --- a/Tests/SwiftJavaToolLibTests/WrapJavaTests/BasicWrapJavaTests.swift +++ b/Tests/SwiftJavaToolLibTests/WrapJavaTests/BasicWrapJavaTests.swift @@ -200,7 +200,7 @@ final class BasicWrapJavaTests: XCTestCase { @JavaStaticField(isFinal: true) public var serialVersionUID: Int64 } - """, + """ ] ) }