From 163253136e0aef7ffc594f347d7512855369a347 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 18 Mar 2026 10:05:54 +0900 Subject: [PATCH 1/2] Fix Swift overloads producing uncompilable Java wrappers (FFM) Swift allows method overloading via parameter labels (e.g. takeValue(a:) and takeValue(b:)), but both produce the same Java signature. This adds a DuplicateNames analysis that detects actual conflicts and appends parameter label suffixes (e.g. takeValue_a, takeValue_b) only when needed. Non-overloaded methods keep their clean names. Co-authored-by: clemo97 --- .../MySwiftLibrary/MySwiftLibrary.swift | 11 +++ .../com/example/swift/HelloJava2Swift.java | 4 + ...MSwift2JavaGenerator+JavaTranslation.swift | 85 +++++++++++++++-- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 10 ++ .../FFM/FFMSwift2JavaGenerator.swift | 13 +++ .../MethodImportTests.swift | 95 +++++++++++++++++++ 6 files changed, 211 insertions(+), 7 deletions(-) diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift index b8b19d902..95420d0ed 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -101,6 +101,17 @@ public func globalReceiveOptional(o1: Int?, o2: (some DataProtocol)?) -> Int { } } +// ==== ----------------------------------------------------------------------- +// MARK: Overloaded functions + +public func globalOverloaded(a: Int) { + p("globalOverloaded(a: \(a))") +} + +public func globalOverloaded(b: Int) { + p("globalOverloaded(b: \(b))") +} + // ==== Internal helpers func p(_ msg: String, file: String = #fileID, line: UInt = #line, function: String = #function) { diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java b/Samples/SwiftJavaExtractFFMSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java index a13125478..1aea9fdc6 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java @@ -101,6 +101,10 @@ static void examples() { } + // Overloaded functions with label-based disambiguation + MySwiftLibrary.globalOverloaded_a(100); + MySwiftLibrary.globalOverloaded_b(200); + System.out.println("DONE."); } diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index 166709d3a..b602c9b36 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -25,10 +25,11 @@ extension FFMSwift2JavaGenerator { let translated: TranslatedFunctionDecl? do { - let translation = JavaTranslation( + var translation = JavaTranslation( config: self.config, knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable) ) + translation.dupeNames = self.currentDupeNames translated = try translation.translate(decl) } catch { self.log.info("Failed to translate: '\(decl.swiftDecl.qualifiedNameForDebug)'; \(error)") @@ -142,13 +143,67 @@ extension FFMSwift2JavaGenerator { } } + // ==== ------------------------------------------------------------------- + // MARK: Duplicate name detection + + /// Detects Java method name conflicts caused by Swift overloads that differ + /// only in parameter labels. When a conflict is detected, the affected methods + /// get a suffix derived from their parameter labels (e.g. `takeValue_a`, + /// `takeValue_b`) so that Java can distinguish them. + struct DuplicateNames { + private var duplicates: Set = [] + + init() {} + + init(for methods: [ImportedFunc], knownTypes: SwiftKnownTypes) { + // Group methods by their Java base name. + var methodsByBaseName: [String: [ImportedFunc]] = [:] + for method in methods { + let baseName: String = switch method.apiKind { + case .getter, .subscriptGetter: method.javaGetterName + case .setter, .subscriptSetter: method.javaSetterName + case .function, .initializer, .enumCase: method.name + } + methodsByBaseName[baseName, default: []].append(method) + } + + // For each group with 2+ methods, check if any two share the same + // Swift parameter types (which means identical Java parameter types). + let lowering = CdeclLowering(knownTypes: knownTypes) + for (baseName, group) in methodsByBaseName where group.count > 1 { + let translatableMethods = group.filter { + (try? lowering.lowerFunctionSignature($0.functionSignature)) != nil + } + var seenSignatures: Set = [] + for method in translatableMethods { + let key = method.functionSignature.parameters + .map { $0.type.description } + .joined(separator: ",") + if !seenSignatures.insert(key).inserted { + duplicates.insert(baseName) + break + } + } + } + } + + func needsSuffix(for baseName: String) -> Bool { + duplicates.contains(baseName) + } + } + + // ==== ------------------------------------------------------------------- + // MARK: Java translation + struct JavaTranslation { let config: Configuration var knownTypes: SwiftKnownTypes + var dupeNames: DuplicateNames init(config: Configuration, knownTypes: SwiftKnownTypes) { self.config = config self.knownTypes = knownTypes + self.dupeNames = DuplicateNames() } func translate(_ decl: ImportedFunc) throws -> TranslatedFunctionDecl { @@ -156,12 +211,7 @@ extension FFMSwift2JavaGenerator { let loweredSignature = try lowering.lowerFunctionSignature(decl.functionSignature) // Name. - let javaName = - switch decl.apiKind { - case .getter, .subscriptGetter: decl.javaGetterName - case .setter, .subscriptSetter: decl.javaSetterName - case .function, .initializer, .enumCase: decl.name - } + let javaName = makeJavaMethodName(decl) // Signature. let translatedSignature = try translate(loweredFunctionSignature: loweredSignature, methodName: javaName) @@ -269,6 +319,27 @@ extension FFMSwift2JavaGenerator { throw JavaTranslationError.unhandledType(type) } + private func makeJavaMethodName(_ decl: ImportedFunc) -> String { + let baseName: String = switch decl.apiKind { + case .getter, .subscriptGetter: decl.javaGetterName + case .setter, .subscriptSetter: decl.javaSetterName + case .function, .initializer, .enumCase: decl.name + } + return baseName + makeMethodNameWithParamsSuffix(decl, baseName: baseName) + } + + private func makeMethodNameWithParamsSuffix(_ decl: ImportedFunc, baseName: String) -> String { + switch decl.apiKind { + case .getter, .subscriptGetter, .setter, .subscriptSetter: + return "" + default: + guard dupeNames.needsSuffix(for: baseName) else { return "" } + return decl.functionSignature.parameters + .map { "_" + ($0.argumentLabel ?? "_") } + .joined() + } + } + /// Translate a Swift API signature to the user-facing Java API signature. /// /// Note that the result signature is for the high-level Java API, not the diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift index 8880d3090..547db3ce5 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift @@ -123,6 +123,11 @@ extension FFMSwift2JavaGenerator { self.lookupContext.symbolTable.printImportedModules(&printer) + self.currentDupeNames = DuplicateNames( + for: self.analysis.importedGlobalFuncs + self.analysis.importedGlobalVariables, + knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable) + ) + for thunk in stt.renderGlobalThunks() { printer.print(thunk) printer.println() @@ -152,6 +157,11 @@ extension FFMSwift2JavaGenerator { self.lookupContext.symbolTable.printImportedModules(&printer) + self.currentDupeNames = DuplicateNames( + for: ty.initializers + ty.variables + ty.methods, + knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable) + ) + for thunk in stt.renderThunks(forType: ty) { printer.print("\(thunk)") printer.print("") diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift index 57ec4e401..ddc94e09e 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift @@ -39,6 +39,9 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator { /// Cached Java translation result. 'nil' indicates failed translation. var translatedDecls: [ImportedFunc: TranslatedFunctionDecl?] = [:] + /// Duplicate name tracking for the current batch of methods being generated. + var currentDupeNames: DuplicateNames = DuplicateNames() + /// Because we need to write empty files for SwiftPM, keep track which files we didn't write yet, /// and write an empty file for those. /// @@ -170,6 +173,11 @@ extension FFMSwift2JavaGenerator { printPackage(&printer) printImports(&printer) + self.currentDupeNames = DuplicateNames( + for: self.analysis.importedGlobalFuncs + self.analysis.importedGlobalVariables, + knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable) + ) + printModuleClass(&printer) { printer in for decl in analysis.importedGlobalVariables { @@ -189,6 +197,11 @@ extension FFMSwift2JavaGenerator { printPackage(&printer) printImports(&printer) // TODO: we could have some imports be driven from types used in the generated decl + self.currentDupeNames = DuplicateNames( + for: decl.initializers + decl.variables + decl.methods, + knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable) + ) + printNominal(&printer, decl) { printer in // We use a static field to abuse the initialization order such that by the time we get type metadata, // we already have loaded the library where it will be obtained from. diff --git a/Tests/JExtractSwiftTests/MethodImportTests.swift b/Tests/JExtractSwiftTests/MethodImportTests.swift index 93dd4ed33..ad144c8e3 100644 --- a/Tests/JExtractSwiftTests/MethodImportTests.swift +++ b/Tests/JExtractSwiftTests/MethodImportTests.swift @@ -476,4 +476,99 @@ final class MethodImportTests { "'Any' return type is not supported yet" ) } + + // ==== ------------------------------------------------------------------- + // MARK: Overloaded method disambiguation + + let overloaded_interfaceFile = + """ + // swift-interface-format-version: 1.0 + // swift-module-flags: -target arm64-apple-macosx15.0 -enable-objc-interop -enable-library-evolution -module-name OverloadModule + import Swift + + public func takeValue(a: Swift.String) -> Swift.Int + public func takeValue(b: Swift.String) -> Swift.Int + public func uniqueFunc(x: Swift.Int) -> Swift.Int + public func overloaded(a: Swift.Int) -> Swift.Int + public func overloaded(a: Swift.String) -> Swift.Int + + public class OverloadedClass { + public func bar(a: Swift.String) + public func bar(b: Swift.String) + public func unique(x: Swift.Int) + @objc deinit + } + """ + + @Test("Overloaded global functions get suffixed Java names") + func overloaded_global_functions_suffixed() throws { + try assertOutput( + input: overloaded_interfaceFile, + .ffm, .java, + swiftModuleName: "OverloadModule", + expectedChunks: [ + "public static long takeValue_a(java.lang.String a)", + "public static long takeValue_b(java.lang.String b)", + ] + ) + } + + @Test("Non-overloaded functions keep clean names") + func non_overloaded_functions_clean_names() throws { + try assertOutput( + input: overloaded_interfaceFile, + .ffm, .java, + swiftModuleName: "OverloadModule", + expectedChunks: [ + "public static long uniqueFunc(long x)", + ], + notExpectedChunks: [ + "public static long uniqueFunc_x(", + ] + ) + } + + @Test("Same name but different types — no suffix needed") + func overloaded_different_types_no_suffix() throws { + try assertOutput( + input: overloaded_interfaceFile, + .ffm, .java, + swiftModuleName: "OverloadModule", + expectedChunks: [ + "public static long overloaded(long a)", + "public static long overloaded(java.lang.String a)", + ], + notExpectedChunks: [ + "public static long overloaded_a(", + ] + ) + } + + @Test("Overloaded methods on a type get suffixed Java names") + func overloaded_methods_on_type_suffixed() throws { + try assertOutput( + input: overloaded_interfaceFile, + .ffm, .java, + swiftModuleName: "OverloadModule", + expectedChunks: [ + "public void bar_a(java.lang.String a)", + "public void bar_b(java.lang.String b)", + ] + ) + } + + @Test("Non-overloaded method on a type keeps clean name") + func non_overloaded_method_on_type_clean_name() throws { + try assertOutput( + input: overloaded_interfaceFile, + .ffm, .java, + swiftModuleName: "OverloadModule", + expectedChunks: [ + "public void unique(long x)", + ], + notExpectedChunks: [ + "public void unique_x(", + ] + ) + } } From 49d2f2675aa425e61eb0f52f32e07fb803cb7bf9 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 18 Mar 2026 12:01:23 +0900 Subject: [PATCH 2/2] clenaups, test coverage, move to camelCase for conflicting names Also handle the same in JNI and FFM --- .../com/example/swift/HelloJava2Swift.java | 4 +- ...MSwift2JavaGenerator+JavaTranslation.swift | 84 +------ ...ift2JavaGenerator+SwiftThunkPrinting.swift | 10 +- .../FFM/FFMSwift2JavaGenerator.swift | 14 +- ...t2JavaGenerator+JavaBindingsPrinting.swift | 8 + ...ISwift2JavaGenerator+JavaTranslation.swift | 14 +- .../JNI/JNISwift2JavaGenerator.swift | 3 + .../JavaIdentifierFactory.swift | 89 ++++++++ .../MethodImportTests.swift | 214 +++++++++++++++--- 9 files changed, 311 insertions(+), 129 deletions(-) create mode 100644 Sources/JExtractSwiftLib/JavaIdentifierFactory.swift diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java b/Samples/SwiftJavaExtractFFMSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java index 1aea9fdc6..62c29482a 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java @@ -102,8 +102,8 @@ static void examples() { // Overloaded functions with label-based disambiguation - MySwiftLibrary.globalOverloaded_a(100); - MySwiftLibrary.globalOverloaded_b(200); + MySwiftLibrary.globalOverloadedA(100); + MySwiftLibrary.globalOverloadedB(200); System.out.println("DONE."); } diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index b602c9b36..4c474cca9 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -25,11 +25,11 @@ extension FFMSwift2JavaGenerator { let translated: TranslatedFunctionDecl? do { - var translation = JavaTranslation( + let translation = JavaTranslation( config: self.config, - knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable) + knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable), + javaIdentifiers: self.currentJavaIdentifiers ) - translation.dupeNames = self.currentDupeNames translated = try translation.translate(decl) } catch { self.log.info("Failed to translate: '\(decl.swiftDecl.qualifiedNameForDebug)'; \(error)") @@ -143,67 +143,18 @@ extension FFMSwift2JavaGenerator { } } - // ==== ------------------------------------------------------------------- - // MARK: Duplicate name detection - - /// Detects Java method name conflicts caused by Swift overloads that differ - /// only in parameter labels. When a conflict is detected, the affected methods - /// get a suffix derived from their parameter labels (e.g. `takeValue_a`, - /// `takeValue_b`) so that Java can distinguish them. - struct DuplicateNames { - private var duplicates: Set = [] - - init() {} - - init(for methods: [ImportedFunc], knownTypes: SwiftKnownTypes) { - // Group methods by their Java base name. - var methodsByBaseName: [String: [ImportedFunc]] = [:] - for method in methods { - let baseName: String = switch method.apiKind { - case .getter, .subscriptGetter: method.javaGetterName - case .setter, .subscriptSetter: method.javaSetterName - case .function, .initializer, .enumCase: method.name - } - methodsByBaseName[baseName, default: []].append(method) - } - - // For each group with 2+ methods, check if any two share the same - // Swift parameter types (which means identical Java parameter types). - let lowering = CdeclLowering(knownTypes: knownTypes) - for (baseName, group) in methodsByBaseName where group.count > 1 { - let translatableMethods = group.filter { - (try? lowering.lowerFunctionSignature($0.functionSignature)) != nil - } - var seenSignatures: Set = [] - for method in translatableMethods { - let key = method.functionSignature.parameters - .map { $0.type.description } - .joined(separator: ",") - if !seenSignatures.insert(key).inserted { - duplicates.insert(baseName) - break - } - } - } - } - - func needsSuffix(for baseName: String) -> Bool { - duplicates.contains(baseName) - } - } - // ==== ------------------------------------------------------------------- // MARK: Java translation struct JavaTranslation { let config: Configuration var knownTypes: SwiftKnownTypes - var dupeNames: DuplicateNames + var javaIdentifiers: JavaIdentifierFactory - init(config: Configuration, knownTypes: SwiftKnownTypes) { + init(config: Configuration, knownTypes: SwiftKnownTypes, javaIdentifiers: JavaIdentifierFactory) { self.config = config self.knownTypes = knownTypes - self.dupeNames = DuplicateNames() + self.javaIdentifiers = javaIdentifiers } func translate(_ decl: ImportedFunc) throws -> TranslatedFunctionDecl { @@ -211,7 +162,7 @@ extension FFMSwift2JavaGenerator { let loweredSignature = try lowering.lowerFunctionSignature(decl.functionSignature) // Name. - let javaName = makeJavaMethodName(decl) + let javaName = javaIdentifiers.makeJavaMethodName(decl) // Signature. let translatedSignature = try translate(loweredFunctionSignature: loweredSignature, methodName: javaName) @@ -319,27 +270,6 @@ extension FFMSwift2JavaGenerator { throw JavaTranslationError.unhandledType(type) } - private func makeJavaMethodName(_ decl: ImportedFunc) -> String { - let baseName: String = switch decl.apiKind { - case .getter, .subscriptGetter: decl.javaGetterName - case .setter, .subscriptSetter: decl.javaSetterName - case .function, .initializer, .enumCase: decl.name - } - return baseName + makeMethodNameWithParamsSuffix(decl, baseName: baseName) - } - - private func makeMethodNameWithParamsSuffix(_ decl: ImportedFunc, baseName: String) -> String { - switch decl.apiKind { - case .getter, .subscriptGetter, .setter, .subscriptSetter: - return "" - default: - guard dupeNames.needsSuffix(for: baseName) else { return "" } - return decl.functionSignature.parameters - .map { "_" + ($0.argumentLabel ?? "_") } - .joined() - } - } - /// Translate a Swift API signature to the user-facing Java API signature. /// /// Note that the result signature is for the high-level Java API, not the diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift index 547db3ce5..1d21f658e 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift @@ -123,9 +123,8 @@ extension FFMSwift2JavaGenerator { self.lookupContext.symbolTable.printImportedModules(&printer) - self.currentDupeNames = DuplicateNames( - for: self.analysis.importedGlobalFuncs + self.analysis.importedGlobalVariables, - knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable) + self.currentJavaIdentifiers = JavaIdentifierFactory( + self.analysis.importedGlobalFuncs + self.analysis.importedGlobalVariables ) for thunk in stt.renderGlobalThunks() { @@ -157,9 +156,8 @@ extension FFMSwift2JavaGenerator { self.lookupContext.symbolTable.printImportedModules(&printer) - self.currentDupeNames = DuplicateNames( - for: ty.initializers + ty.variables + ty.methods, - knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable) + self.currentJavaIdentifiers = JavaIdentifierFactory( + ty.initializers + ty.variables + ty.methods ) for thunk in stt.renderThunks(forType: ty) { diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift index ddc94e09e..dd14ee8ea 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift @@ -39,8 +39,8 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator { /// Cached Java translation result. 'nil' indicates failed translation. var translatedDecls: [ImportedFunc: TranslatedFunctionDecl?] = [:] - /// Duplicate name tracking for the current batch of methods being generated. - var currentDupeNames: DuplicateNames = DuplicateNames() + /// Duplicate identifier tracking for the current batch of methods being generated. + var currentJavaIdentifiers: JavaIdentifierFactory = JavaIdentifierFactory() /// Because we need to write empty files for SwiftPM, keep track which files we didn't write yet, /// and write an empty file for those. @@ -173,9 +173,8 @@ extension FFMSwift2JavaGenerator { printPackage(&printer) printImports(&printer) - self.currentDupeNames = DuplicateNames( - for: self.analysis.importedGlobalFuncs + self.analysis.importedGlobalVariables, - knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable) + self.currentJavaIdentifiers = JavaIdentifierFactory( + self.analysis.importedGlobalFuncs + self.analysis.importedGlobalVariables ) printModuleClass(&printer) { printer in @@ -197,9 +196,8 @@ extension FFMSwift2JavaGenerator { printPackage(&printer) printImports(&printer) // TODO: we could have some imports be driven from types used in the generated decl - self.currentDupeNames = DuplicateNames( - for: decl.initializers + decl.variables + decl.methods, - knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable) + self.currentJavaIdentifiers = JavaIdentifierFactory( + decl.initializers + decl.variables + decl.methods ) printNominal(&printer, decl) { printer in diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index c27b6f308..759aa7418 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -93,6 +93,10 @@ extension JNISwift2JavaGenerator { printPackage(&printer) printImports(&printer) + self.currentJavaIdentifiers = JavaIdentifierFactory( + self.analysis.importedGlobalFuncs + self.analysis.importedGlobalVariables + ) + printModuleClass(&printer) { printer in printer.print( """ @@ -124,6 +128,10 @@ extension JNISwift2JavaGenerator { printPackage(&printer) printImports(&printer) + self.currentJavaIdentifiers = JavaIdentifierFactory( + decl.initializers + decl.variables + decl.methods + ) + switch decl.swiftNominal.kind { case .actor, .class, .enum, .struct: printConcreteType(&printer, decl) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 5212d56f0..966230a55 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -25,7 +25,8 @@ extension JNISwift2JavaGenerator { javaClassLookupTable: self.javaClassLookupTable, knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable), protocolWrappers: self.interfaceProtocolWrappers, - logger: self.logger + logger: self.logger, + javaIdentifiers: self.currentJavaIdentifiers ) } @@ -64,7 +65,8 @@ extension JNISwift2JavaGenerator { javaClassLookupTable: self.javaClassLookupTable, knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable), protocolWrappers: self.interfaceProtocolWrappers, - logger: self.logger + logger: self.logger, + javaIdentifiers: self.currentJavaIdentifiers ) translated = try translation.translate(enumCase: decl) } catch { @@ -84,6 +86,7 @@ extension JNISwift2JavaGenerator { var knownTypes: SwiftKnownTypes let protocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] let logger: Logger + var javaIdentifiers: JavaIdentifierFactory func translate(enumCase: ImportedEnumCase) throws -> TranslatedEnumCase { let nativeTranslation = NativeJavaTranslation( @@ -226,12 +229,7 @@ extension JNISwift2JavaGenerator { let parentName = decl.parentType?.asNominalType?.nominalTypeDecl.qualifiedName ?? swiftModuleName // Name. - let javaName = - switch decl.apiKind { - case .getter, .subscriptGetter: decl.javaGetterName - case .setter, .subscriptSetter: decl.javaSetterName - case .function, .initializer, .enumCase: decl.name - } + let javaName = javaIdentifiers.makeJavaMethodName(decl) // Swift -> Java var translatedFunctionSignature = try translate( diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index 3d0c97b75..a6917227a 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -44,6 +44,9 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { var translatedEnumCases: [ImportedEnumCase: TranslatedEnumCase] = [:] var interfaceProtocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] = [:] + /// Duplicate identifier tracking for the current batch of methods being generated. + var currentJavaIdentifiers: JavaIdentifierFactory = JavaIdentifierFactory() + /// Because we need to write empty files for SwiftPM, keep track which files we didn't write yet, /// and write an empty file for those. /// diff --git a/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift b/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift new file mode 100644 index 000000000..68fdb9c8f --- /dev/null +++ b/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift @@ -0,0 +1,89 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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 +// +//===----------------------------------------------------------------------===// + +/// Detects Java method name conflicts caused by Swift overloads that differ +/// only in parameter labels. When a conflict is detected, the affected methods +/// get a camelCase suffix derived from their parameter labels (e.g. `takeValueA`, +/// `takeValueB`) so that Java can distinguish them. +package struct JavaIdentifierFactory { + private var duplicates: Set = [] + + package init() {} + + package init(_ methods: [ImportedFunc]) { + self.init() + record(methods) + } + + /// Analyze the given methods and record any base names that have conflicts. + private mutating func record(_ methods: [ImportedFunc]) { + // Group methods by their Java base name. + var methodsByBaseName: [String: [ImportedFunc]] = [:] + for method in methods { + let baseName: String = + switch method.apiKind { + case .getter, .subscriptGetter: method.javaGetterName + case .setter, .subscriptSetter: method.javaSetterName + case .function, .initializer, .enumCase: method.name + } + methodsByBaseName[baseName, default: []].append(method) + } + + // For each group with 2+ methods, check if any two share the same + // Swift parameter types (which means identical Java parameter types). + for (baseName, group) in methodsByBaseName where group.count > 1 { + var seenSignatures: Set = [] + for method in group { + let key = method.functionSignature.parameters + .map { $0.type.description } + .joined(separator: ",") + if !seenSignatures.insert(key).inserted { + duplicates.insert(baseName) + break + } + } + } + } + + package func needsSuffix(for baseName: String) -> Bool { + duplicates.contains(baseName) + } + + /// Compute the disambiguated Java method name for a declaration. + package func makeJavaMethodName(_ decl: ImportedFunc) -> String { + let baseName: String = + switch decl.apiKind { + case .getter, .subscriptGetter: decl.javaGetterName + case .setter, .subscriptSetter: decl.javaSetterName + case .function, .initializer, .enumCase: decl.name + } + return baseName + paramsSuffix(decl, baseName: baseName) + } + + private func paramsSuffix(_ decl: ImportedFunc, baseName: String) -> String { + switch decl.apiKind { + case .getter, .subscriptGetter, .setter, .subscriptSetter: + return "" + default: + guard needsSuffix(for: baseName) else { return "" } + let labels = decl.functionSignature.parameters + .compactMap { $0.argumentLabel } + // A parameterless function that still conflicts (e.g. with a property + // getter) gets a bare "_" so it compiles as a distinct Java method. + guard !labels.isEmpty else { return "_" } + // Join labels in camelCase: takeValue(a:) → takeValueA + return labels.map { $0.prefix(1).uppercased() + $0.dropFirst() }.joined() + } + } +} diff --git a/Tests/JExtractSwiftTests/MethodImportTests.swift b/Tests/JExtractSwiftTests/MethodImportTests.swift index ad144c8e3..be9984e08 100644 --- a/Tests/JExtractSwiftTests/MethodImportTests.swift +++ b/Tests/JExtractSwiftTests/MethodImportTests.swift @@ -478,12 +478,10 @@ final class MethodImportTests { } // ==== ------------------------------------------------------------------- - // MARK: Overloaded method disambiguation + // MARK: FFM overloaded method disambiguation let overloaded_interfaceFile = """ - // swift-interface-format-version: 1.0 - // swift-module-flags: -target arm64-apple-macosx15.0 -enable-objc-interop -enable-library-evolution -module-name OverloadModule import Swift public func takeValue(a: Swift.String) -> Swift.Int @@ -496,78 +494,238 @@ final class MethodImportTests { public func bar(a: Swift.String) public func bar(b: Swift.String) public func unique(x: Swift.Int) - @objc deinit } """ - @Test("Overloaded global functions get suffixed Java names") - func overloaded_global_functions_suffixed() throws { + @Test("FFM: Overloaded global functions get suffixed Java names") + func ffm_overloaded_global_functions_suffixed() throws { try assertOutput( input: overloaded_interfaceFile, - .ffm, .java, + .ffm, + .java, swiftModuleName: "OverloadModule", expectedChunks: [ - "public static long takeValue_a(java.lang.String a)", - "public static long takeValue_b(java.lang.String b)", + "public static long takeValueA(java.lang.String a)", + "public static long takeValueB(java.lang.String b)", ] ) } - @Test("Non-overloaded functions keep clean names") - func non_overloaded_functions_clean_names() throws { + @Test("FFM: Non-overloaded functions keep clean names") + func ffm_non_overloaded_functions_clean_names() throws { try assertOutput( input: overloaded_interfaceFile, - .ffm, .java, + .ffm, + .java, swiftModuleName: "OverloadModule", expectedChunks: [ - "public static long uniqueFunc(long x)", + "public static long uniqueFunc(long x)" ], notExpectedChunks: [ - "public static long uniqueFunc_x(", + "public static long uniqueFunc_x(" ] ) } - @Test("Same name but different types — no suffix needed") - func overloaded_different_types_no_suffix() throws { + @Test("FFM: Same name but different types — no suffix needed") + func ffm_overloaded_different_types_no_suffix() throws { try assertOutput( input: overloaded_interfaceFile, - .ffm, .java, + .ffm, + .java, swiftModuleName: "OverloadModule", expectedChunks: [ "public static long overloaded(long a)", "public static long overloaded(java.lang.String a)", ], notExpectedChunks: [ - "public static long overloaded_a(", + "public static long overloaded_a(" ] ) } - @Test("Overloaded methods on a type get suffixed Java names") - func overloaded_methods_on_type_suffixed() throws { + @Test("FFM: Overloaded methods on a type get suffixed Java names") + func ffm_overloaded_methods_on_type_suffixed() throws { try assertOutput( input: overloaded_interfaceFile, - .ffm, .java, + .ffm, + .java, swiftModuleName: "OverloadModule", expectedChunks: [ - "public void bar_a(java.lang.String a)", - "public void bar_b(java.lang.String b)", + "public void barA(java.lang.String a)", + "public void barB(java.lang.String b)", ] ) } - @Test("Non-overloaded method on a type keeps clean name") - func non_overloaded_method_on_type_clean_name() throws { + @Test("FFM: Non-overloaded method on a type keeps clean name") + func ffm_non_overloaded_method_on_type_clean_name() throws { try assertOutput( input: overloaded_interfaceFile, - .ffm, .java, + .ffm, + .java, swiftModuleName: "OverloadModule", expectedChunks: [ - "public void unique(long x)", + "public void unique(long x)" ], notExpectedChunks: [ - "public void unique_x(", + "public void unique_x(" + ] + ) + } + + let propertyMethodConflict_interfaceFile = + """ + import Swift + + public class MyClass { + public var name: Swift.Int { get } + public func getName() -> Swift.Int + } + """ + + @Test("FFM: Property getter and method with same Java name are disambiguated") + func ffm_property_getter_vs_method_conflict() throws { + try assertOutput( + input: propertyMethodConflict_interfaceFile, + .ffm, + .java, + swiftModuleName: "ConflictModule", + expectedChunks: [ + // Property getter keeps standard Java bean name + "public long getName()", + // Method gets a trailing underscore to avoid the conflict + "public long getName_()", + ] + ) + } + + let argumentLabel_interfaceFile = + """ + import Swift + + public func takeValue(outer name: Swift.String) -> Swift.Int + public func takeValue(another name: Swift.String) -> Swift.Int + """ + + @Test("FFM: Overloaded functions with argument labels use label for suffix") + func ffm_overloaded_argument_labels() throws { + try assertOutput( + input: argumentLabel_interfaceFile, + .ffm, + .java, + swiftModuleName: "LabelModule", + expectedChunks: [ + "public static long takeValueOuter(java.lang.String name)", + "public static long takeValueAnother(java.lang.String name)", + ] + ) + } + + // ==== ------------------------------------------------------------------- + // MARK: JNI overloaded method disambiguation + + @Test("JNI: Overloaded global functions get suffixed Java names") + func jni_overloaded_global_functions_suffixed() throws { + try assertOutput( + input: overloaded_interfaceFile, + .jni, + .java, + swiftModuleName: "OverloadModule", + expectedChunks: [ + "public static long takeValueA(java.lang.String a)", + "public static long takeValueB(java.lang.String b)", + ] + ) + } + + @Test("JNI: Non-overloaded functions keep clean names") + func jni_non_overloaded_functions_clean_names() throws { + try assertOutput( + input: overloaded_interfaceFile, + .jni, + .java, + swiftModuleName: "OverloadModule", + expectedChunks: [ + "public static long uniqueFunc(long x)" + ], + notExpectedChunks: [ + "public static long uniqueFunc_x(" + ] + ) + } + + @Test("JNI: Same name but different types — no suffix needed") + func jni_overloaded_different_types_no_suffix() throws { + try assertOutput( + input: overloaded_interfaceFile, + .jni, + .java, + swiftModuleName: "OverloadModule", + expectedChunks: [ + "public static long overloaded(long a)", + "public static long overloaded(java.lang.String a)", + ], + notExpectedChunks: [ + "public static long overloaded_a(" + ] + ) + } + + @Test("JNI: Overloaded methods on a type get suffixed Java names") + func jni_overloaded_methods_on_type_suffixed() throws { + try assertOutput( + input: overloaded_interfaceFile, + .jni, + .java, + swiftModuleName: "OverloadModule", + expectedChunks: [ + "public void barA(java.lang.String a)", + "public void barB(java.lang.String b)", + ] + ) + } + + @Test("JNI: Non-overloaded method on a type keeps clean name") + func jni_non_overloaded_method_on_type_clean_name() throws { + try assertOutput( + input: overloaded_interfaceFile, + .jni, + .java, + swiftModuleName: "OverloadModule", + expectedChunks: [ + "public void unique(long x)" + ], + notExpectedChunks: [ + "public void unique_x(" + ] + ) + } + + @Test("JNI: Property getter and method with same Java name are disambiguated") + func jni_property_getter_vs_method_conflict() throws { + try assertOutput( + input: propertyMethodConflict_interfaceFile, + .jni, + .java, + swiftModuleName: "ConflictModule", + expectedChunks: [ + "public long getName()", + "public long getName_()", + ] + ) + } + + @Test("JNI: Overloaded functions with argument labels use label for suffix") + func jni_overloaded_argument_labels() throws { + try assertOutput( + input: argumentLabel_interfaceFile, + .jni, + .java, + swiftModuleName: "LabelModule", + expectedChunks: [ + "public static long takeValueOuter(java.lang.String name)", + "public static long takeValueAnother(java.lang.String name)", ] ) }