From 09877ff6314faa4522f82d135ddf09501e15fa60 Mon Sep 17 00:00:00 2001 From: Pierluigi Cifani Date: Wed, 4 Nov 2026 15:58:14 +0100 Subject: [PATCH] jextract: keep Swift names that are Java keywords out of generated Java Swift parameters may be named `long`, `int`, `double`, `default` or `class`, and properties may be written with backticks, such as `` `default` ``. In JNI mode those names reached the generated Java as they are: `boolean long`, `record Quote(String slug, long long)` and `get`default`()` don't compile, and the Swift thunks had the same problem with `class` and `default`. - Parameter names, closure parameter names and enum case payload names get a `_` suffix when they are Java keywords or reserved Swift keywords, in both the Java bindings and the Swift thunks. Argument labels stay as they are. - Getter and setter names drop a property's backticks: `getDefault()`. - Method names that would redeclare a final method of `java.lang.Object` (`getClass`, `notify`, `notifyAll`, `wait`), including the getter of a property named `class`, get a `_` suffix, as Java keywords already did. JNIJavaKitTests expected `long int` as a Java parameter, which doesn't compile; it now expects `long int_`. 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 6b6a4e729..3132b8d37 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift @@ -59,7 +59,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 let typeName = param.type.description return "_ \(name): \(typeName)" } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift index 236977e5e..ca2dcb8ae 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift @@ -149,7 +149,7 @@ extension JNISwift2JavaGenerator { ) throws -> SyntheticEscapingClosureFunctionType { 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 9dc93d507..7bc7c823b 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -136,7 +136,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, @@ -226,7 +226,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 param.type { case .function(let funcTy): @@ -303,7 +303,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, @@ -387,7 +387,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 6490586d7..4ca6a03ca 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -422,7 +422,7 @@ extension JNISwift2JavaGenerator { // Non-escaping closures use the legacy translation 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 @@ -1726,7 +1726,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 c5ac7bba0..0affc060e 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -458,7 +458,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) {", + ] + ) + } }