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..cd21bfad 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.