From b7923d42bc31b154c3124af5713dbb661cffb15f Mon Sep 17 00:00:00 2001 From: Pierluigi Cifani Date: Wed, 4 Nov 2026 15:59:59 +0100 Subject: [PATCH 1/2] JNI: keep Swift names that are Java keywords out of generated Java Ports swiftlang/swift-java#954. Parameter, closure parameter and enum case payload names that are Java or reserved Swift keywords, such as `long`, `int` or `default`, get a `_` suffix in the Java bindings and the Swift thunks; getter and setter names drop a property's backticks; and methods that would redeclare a final `java.lang.Object` method (`getClass`, `notify`, `notifyAll`, `wait`) get a `_` suffix. Co-Authored-By: Claude Opus 5.5 --- .../ExtractedDecls+JavaNaming.swift | 17 +-- ...avaGenerator+EscapingClosureWrapJava.swift | 2 +- ...Generator+InterfaceWrapperGeneration.swift | 2 +- ...ISwift2JavaGenerator+JavaTranslation.swift | 8 +- ...wift2JavaGenerator+NativeTranslation.swift | 4 +- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 2 +- .../JavaIdentifierFactory.swift | 23 +++- .../JNI/JNIJavaKitTests.swift | 10 +- .../JExtractSwiftTests/JavaKeywordTests.swift | 101 ++++++++++++++++++ 9 files changed, 147 insertions(+), 22 deletions(-) diff --git a/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift b/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift index ad1281d37..0d2e27d35 100644 --- a/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift +++ b/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift @@ -61,13 +61,15 @@ extension ExtractedFunc { } let returnsBoolean = self.functionSignature.result.type.asNominalTypeDeclaration?.knownTypeKind == .bool + // A property named with backticks, such as `default`, is `getDefault` in Java. + let name = self.name.unescapedSwiftName if !returnsBoolean { - return "get\(self.name.firstCharacterUppercased)" - } else if !self.name.hasJavaBooleanNamingConvention { - return "is\(self.name.firstCharacterUppercased)" + return "get\(name.firstCharacterUppercased)" + } else if !name.hasJavaBooleanNamingConvention { + return "is\(name.firstCharacterUppercased)" } else { - return self.name + return name } } @@ -85,13 +87,14 @@ extension ExtractedFunc { } let isBooleanSetter = self.functionSignature.parameters.first?.type.asNominalTypeDeclaration?.knownTypeKind == .bool + let name = self.name.unescapedSwiftName - if isBooleanSetter && self.name.hasJavaBooleanNamingConvention { + if isBooleanSetter && name.hasJavaBooleanNamingConvention { // Safe to force unwrap due to `hasJavaBooleanNamingConvention` check. - let propertyName = self.name.split(separator: "is", maxSplits: 1).last! + let propertyName = name.split(separator: "is", maxSplits: 1).last! return "set\(propertyName)" } else { - return "set\(self.name.firstCharacterUppercased)" + return "set\(name.firstCharacterUppercased)" } } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift index dff16b960..bf3b20401 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift @@ -145,7 +145,7 @@ extension JNISwift2JavaGenerator { private func renderEscapingClosureApplySignature(_ type: SwiftFunctionType) -> String { let params = type.parameters.enumerated().map { idx, param -> String in - let name = param.parameterName ?? "_\(idx)" + let name = (param.parameterName ?? "_\(idx)").javaSafeIdentifier return "_ \(name): \(wrapJavaTypeName(param.type, isResult: false))" } let paramList = "(\(params.joined(separator: .comma)))" diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift index 06f7c1900..8842656f5 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift @@ -195,7 +195,7 @@ extension JNISwift2JavaGenerator { } let parameterConversions = try functionType.parameters.enumerated().map { idx, param in try self.translateParameter( - parameterName: param.parameterName ?? "_\(idx)", + parameterName: (param.parameterName ?? "_\(idx)").javaSafeIdentifier, type: param.type ) } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index f75f1a628..640d64bc2 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -148,7 +148,7 @@ extension JNISwift2JavaGenerator { let methodName = "" // TODO: Used for closures, replace with better name? let parameterResults = try enumCase.parameters.enumerated().map { idx, parameter in - let resultName = parameter.name ?? "arg\(idx)" + let resultName = (parameter.name ?? "arg\(idx)").javaSafeIdentifier let translatedResult = try self.translateResult( swiftType: parameter.type, methodName: methodName, @@ -275,7 +275,7 @@ extension JNISwift2JavaGenerator { // Closures. var funcTypes: [TranslatedFunctionType] = [] for (idx, param) in decl.functionSignature.parameters.enumerated() { - let parameterName = param.parameterName ?? "_\(idx)" + let parameterName = (param.parameterName ?? "_\(idx)").javaSafeIdentifier switch Self.closureType(param.type) { case .some((let funcTy, _)): @@ -421,7 +421,7 @@ extension JNISwift2JavaGenerator { var translatedParams: [TranslatedParameter] = [] for (i, param) in swiftType.parameters.enumerated() { - let paramName = param.parameterName ?? "_\(i)" + let paramName = (param.parameterName ?? "_\(i)").javaSafeIdentifier translatedParams.append( try translateParameter( swiftType: param.type, @@ -509,7 +509,7 @@ extension JNISwift2JavaGenerator { genericRequirements: [SwiftGenericRequirement], ) throws -> [TranslatedParameter] { try parameters.enumerated().map { idx, param in - let parameterName = param.name ?? "arg\(idx)" + let parameterName = (param.name ?? "arg\(idx)").javaSafeIdentifier return try translateParameter( swiftType: param.type, parameterName: parameterName, diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 308437592..cccdc45d3 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -481,7 +481,7 @@ extension JNISwift2JavaGenerator { ) throws -> NativeParameter { var parameters = [NativeParameter]() for (i, parameter) in functionType.parameters.enumerated() { - let closureParamName = parameter.parameterName ?? "_\(i)" + let closureParamName = (parameter.parameterName ?? "_\(i)").javaSafeIdentifier let closureParameter = try translateClosureParameter( parameter.type, parameterName: closureParamName @@ -1910,7 +1910,7 @@ extension JNISwift2JavaGenerator { let fn = syntheticFunction.functionType let parameterNames = fn.parameters.enumerated().map { idx, param in - param.parameterName ?? "_\(idx)" + (param.parameterName ?? "_\(idx)").javaSafeIdentifier } let closureParameters = parameterNames.joined(separator: .comma) let isVoid = fn.resultType.isVoid diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 66b73a916..51cb28011 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -536,7 +536,7 @@ extension JNISwift2JavaGenerator { }.joined(separator: .comma) printer.printBraceBlock("fileprivate func getAs\(enumCase.name)() -> (\(associatedValueTypes))?") { printer in let params = enumCase.original.parameters.enumerated().map { i, param in - param.name ?? "_\(i)" + (param.name ?? "_\(i)").javaSafeIdentifier }.joined(separator: .comma) printer.printIfBlock("case let .\(enumCase.original.name)(\(params)) = self") { printer in printer.print("return (\(params))") diff --git a/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift b/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift index 40c83663d..b8f944bf0 100644 --- a/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift +++ b/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift @@ -73,7 +73,7 @@ package struct JavaIdentifierFactory { Self.javaOperatorName(decl.name) } var methodName = baseName + paramsSuffix(decl, baseName: baseName) - if Self.javaKeywords.contains(methodName) { + if Self.javaKeywords.contains(methodName) || Self.finalObjectMethods.contains(methodName) { methodName += "_" } return methodName @@ -167,4 +167,25 @@ package struct JavaIdentifierFactory { /// literals "true", "false", "null", ] + + /// Final methods of `java.lang.Object`, which a generated method can't declare again. + static let finalObjectMethods: Set = ["getClass", "notify", "notifyAll", "wait"] + + /// Swift keywords that can't name a parameter or a local variable without backticks. + static let reservedSwiftKeywords: Set = [ + "associatedtype", "class", "deinit", "enum", "extension", "fileprivate", "func", "import", "init", "inout", + "internal", "let", "operator", "precedencegroup", "private", "protocol", "public", "rethrows", "static", + "struct", "subscript", "typealias", "var", "break", "case", "catch", "continue", "default", "defer", "do", + "else", "fallthrough", "for", "guard", "if", "in", "repeat", "return", "switch", "throw", "where", "while", + "Any", "as", "false", "is", "nil", "self", "Self", "super", "throws", "true", "try", + ] +} + +extension String { + /// This name, made usable as a parameter or local variable in both the generated Java and Swift: a Java or + /// Swift keyword such as `long` or `default` gets a `_` suffix. + package var javaSafeIdentifier: String { + JavaIdentifierFactory.javaKeywords.contains(self) || JavaIdentifierFactory.reservedSwiftKeywords.contains(self) + ? self + "_" : self + } } diff --git a/Tests/JExtractSwiftTests/JNI/JNIJavaKitTests.swift b/Tests/JExtractSwiftTests/JNI/JNIJavaKitTests.swift index db070683c..c7c69c064 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIJavaKitTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIJavaKitTests.swift @@ -41,12 +41,12 @@ struct JNIJavaKitTests { * public func function(javaLong: JavaLong, javaInteger: JavaInteger, int: Int64) * } */ - public static void function(java.lang.Long javaLong, java.lang.Integer javaInteger, long int) { - SwiftModule.$function(javaLong, javaInteger, int); + public static void function(java.lang.Long javaLong, java.lang.Integer javaInteger, long int_) { + SwiftModule.$function(javaLong, javaInteger, int_); } """, """ - private static native void $function(java.lang.Long javaLong, java.lang.Integer javaInteger, long int); + private static native void $function(java.lang.Long javaLong, java.lang.Integer javaInteger, long int_); """, ] ) @@ -63,14 +63,14 @@ struct JNIJavaKitTests { expectedChunks: [ """ @_cdecl("Java_com_example_swift_SwiftModule__00024function__Ljava_lang_Long_2Ljava_lang_Integer_2J") - public func Java_com_example_swift_SwiftModule__00024function__Ljava_lang_Long_2Ljava_lang_Integer_2J(environment: UnsafeMutablePointer!, thisClass: jclass, javaLong: jobject?, javaInteger: jobject?, int: jlong) { + public func Java_com_example_swift_SwiftModule__00024function__Ljava_lang_Long_2Ljava_lang_Integer_2J(environment: UnsafeMutablePointer!, thisClass: jclass, javaLong: jobject?, javaInteger: jobject?, int_: jlong) { guard let javaLong_unwrapped$ = javaLong else { fatalError("javaLong was null in call to \\(#function), but Swift requires non-optional!") } guard let javaInteger_unwrapped$ = javaInteger else { fatalError("javaInteger was null in call to \\(#function), but Swift requires non-optional!") } - SwiftModule.function(javaLong: JavaLong(javaThis: javaLong_unwrapped$, environment: environment), javaInteger: JavaInteger(javaThis: javaInteger_unwrapped$, environment: environment), int: Int64(fromJNI: int, in: environment)) + SwiftModule.function(javaLong: JavaLong(javaThis: javaLong_unwrapped$, environment: environment), javaInteger: JavaInteger(javaThis: javaInteger_unwrapped$, environment: environment), int: Int64(fromJNI: int_, in: environment)) } """ ] diff --git a/Tests/JExtractSwiftTests/JavaKeywordTests.swift b/Tests/JExtractSwiftTests/JavaKeywordTests.swift index 81588be3b..a0e3aeca4 100644 --- a/Tests/JExtractSwiftTests/JavaKeywordTests.swift +++ b/Tests/JExtractSwiftTests/JavaKeywordTests.swift @@ -139,4 +139,105 @@ struct JavaKeywordTests { ] ) } + + @Test + func jniParameterNames() throws { + let text = + """ + public struct Side { + public func scale(double: Double, int: Int64, `default`: Bool) -> Double + } + """ + + try assertOutput( + input: text, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + "public double scale(double double_, long int_, boolean default_) {", + "return Side.$scale(double_, int_, default_, this.$memoryAddress());", + ] + ) + + try assertOutput( + input: text, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + "thisClass: jclass, double_: jdouble, int_: jlong, default_: jboolean, selfPointer: jlong) -> jdouble {", + "scale(double: Double(fromJNI: double_, in: environment), int: Int64(fromJNI: int_, in: environment), default: Bool(fromJNI: default_, in: environment))", + ] + ) + } + + @Test + func jniEnumCasePayloadNames() throws { + let text = + """ + public enum Pricing { + case quote(slug: String, long: Int64) + } + """ + + try assertOutput( + input: text, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + "record Quote(java.lang.String slug, long long_) implements Case {}" + ] + ) + + try assertOutput( + input: text, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + "if case let .quote(slug, long_) = self {" + ] + ) + } + + @Test + func backtickedPropertyName() throws { + try assertOutput( + input: """ + public struct Side { + public static var `default`: Side + } + """, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + "public static Side getDefault(SwiftArena swiftArena) {" + ], + notExpectedChunks: ["get`default`"] + ) + } + + @Test + func finalObjectMethodNames() throws { + try assertOutput( + input: """ + public struct Lock { + public var `class`: String + public func wait() + public func notify(_ count: Int64) + } + """, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + "public java.lang.String getClass_() {", + "public void wait_() {", + "public void notify_(long count) {", + ] + ) + } } From 8cfe730976a04222e3f0952a754595c4e5104755 Mon Sep 17 00:00:00 2001 From: Pierluigi Cifani Date: Wed, 4 Nov 2026 16:01:16 +0100 Subject: [PATCH 2/2] Samples: Swift names that are Java keywords Co-Authored-By: Claude Opus 5.5 --- .../KotlinSampleLibrary/Positions.swift | 38 +++++++++++++++++++ .../com/example/kotlin/KeywordNamesTest.kt | 34 +++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleLibrary/Positions.swift create mode 100644 Samples/SwiftJavaExtractJNIKotlinSampleApp/src/test/kotlin/com/example/kotlin/KeywordNamesTest.kt diff --git a/Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleLibrary/Positions.swift b/Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleLibrary/Positions.swift new file mode 100644 index 000000000..af3efdb50 --- /dev/null +++ b/Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleLibrary/Positions.swift @@ -0,0 +1,38 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 +// +//===----------------------------------------------------------------------===// + +/// Swift names that are Java keywords: `long`, `short`, `default`, and a method named `wait`. +public struct Position: Sendable { + public static let `default`: Position = Position(size: 0, long: true) + + public var size: Int64 + public var isLong: Bool + + public init(size: Int64, long: Bool) { + self.size = size + self.isLong = long + } + + public func label(long: String, short: String) -> String { + isLong ? long : short + } + + public func wait() -> Int64 { + size + } +} + +public enum Exposure { + case split(long: Int64, short: Int64) +} diff --git a/Samples/SwiftJavaExtractJNIKotlinSampleApp/src/test/kotlin/com/example/kotlin/KeywordNamesTest.kt b/Samples/SwiftJavaExtractJNIKotlinSampleApp/src/test/kotlin/com/example/kotlin/KeywordNamesTest.kt new file mode 100644 index 000000000..d6aa78c3a --- /dev/null +++ b/Samples/SwiftJavaExtractJNIKotlinSampleApp/src/test/kotlin/com/example/kotlin/KeywordNamesTest.kt @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 +// +//===----------------------------------------------------------------------===// + +package com.example.kotlin + +import kotlin.test.Test +import kotlin.test.assertEquals + +// Swift names that are Java keywords: parameters get a `_` suffix, `default` drops its backticks, and +// `wait()`, which Java's `Object` already declares, is `wait_()`. +class KeywordNamesTest { + @Test + fun keywordNames() { + val position = Position.init(3, true) + assertEquals("long side", position.label("long side", "short side")) + assertEquals(3L, position.wait_()) + assertEquals(0L, Position.getDefault().size) + + val split = Exposure.split(5, 2).case as Exposure.Case.Split + assertEquals(5L, split.long_) + assertEquals(2L, split.short_) + } +}