diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/EnumWithValueCases.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Enums.swift similarity index 89% rename from Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/EnumWithValueCases.swift rename to Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Enums.swift index f3a7f45e1..d44985907 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/EnumWithValueCases.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Enums.swift @@ -16,3 +16,8 @@ public enum EnumWithValueCases { case firstCase(UInt) case secondCase } + +public enum EnumWithBacktick { + case `let` + case `default` +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EnumWithValueCasesTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EnumTest.java similarity index 76% rename from Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EnumWithValueCasesTest.java rename to Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EnumTest.java index 840e7b64f..4cd2d2886 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EnumWithValueCasesTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EnumTest.java @@ -15,20 +15,25 @@ package com.example.swift; import org.junit.jupiter.api.Test; -import org.swift.swiftkit.core.ConfinedSwiftMemorySession; import org.swift.swiftkit.core.SwiftArena; -import java.util.Optional; - import static org.junit.jupiter.api.Assertions.*; -public class EnumWithValueCasesTest { +public class EnumTest { @Test - void fn() { + void enumWithValueCases() { try (var arena = SwiftArena.ofConfined()) { EnumWithValueCases e = EnumWithValueCases.firstCase(48, arena); EnumWithValueCases.FirstCase c = (EnumWithValueCases.FirstCase) e.getCase(); assertNotNull(c); } } -} \ No newline at end of file + + @Test + void enumWithBacktick() { + try (var arena = SwiftArena.ofConfined()) { + EnumWithBacktick e = EnumWithBacktick.default_(arena); + assertTrue(e.getAsDefault().isPresent()); + } + } +} diff --git a/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift index f7439e790..9fdb6a64c 100644 --- a/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift @@ -87,6 +87,14 @@ extension String { return .class(package: javaPackageName, name: javaClassName) } + + /// Unescapes the name if it is surrounded by backticks. + var unescapedSwiftName: String { + if count >= 2 && hasPrefix("`") && hasSuffix("`") { + return String(dropFirst().dropLast()) + } + return self + } } extension Array where Element == String { diff --git a/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift b/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift index 68fdb9c8f..266e6bbec 100644 --- a/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift +++ b/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift @@ -68,7 +68,11 @@ package struct JavaIdentifierFactory { case .setter, .subscriptSetter: decl.javaSetterName case .function, .initializer, .enumCase: decl.name } - return baseName + paramsSuffix(decl, baseName: baseName) + var methodName = baseName + paramsSuffix(decl, baseName: baseName) + if Self.javaKeywords.contains(methodName) { + methodName += "_" + } + return methodName } private func paramsSuffix(_ decl: ImportedFunc, baseName: String) -> String { @@ -86,4 +90,22 @@ package struct JavaIdentifierFactory { return labels.map { $0.prefix(1).uppercased() + $0.dropFirst() }.joined() } } + + static let javaKeywords: Set = [ + /// https://docs.oracle.com/javase/specs/jls/se25/html/jls-3.html#jls-3.9 + "abstract", "continue", "for", "new", "switch", + "assert", "default", "if", "package", "synchronized", + "boolean", "do", "goto", "private", "this", + "break", "double", "implements", "protected", "throw", + "byte", "else", "import", "public", "throws", + "case", "enum", "instanceof", "return", "transient", + "catch", "extends", "int", "short", "try", + "char", "final", "interface", "static", "void", + "class", "finally", "long", "strictfp", "volatile", + "const", "float", "native", "super", "while", + "_", + + /// literals + "true", "false", "null", + ] } diff --git a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift index c6390bc14..38f92d114 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift @@ -186,7 +186,7 @@ final class Swift2JavaVisitor { let imported = ImportedFunc( module: translator.swiftModuleName, swiftDecl: node, - name: node.name.text, + name: node.name.text.unescapedSwiftName, apiKind: .function, functionSignature: signature, ) @@ -227,16 +227,17 @@ final class Swift2JavaVisitor { lookupContext: translator.lookupContext, ) + let caseName = caseElement.name.text.unescapedSwiftName let caseFunction = ImportedFunc( module: translator.swiftModuleName, swiftDecl: node, - name: caseElement.name.text, + name: caseName, apiKind: .enumCase, functionSignature: signature, ) let importedCase = ImportedEnumCase( - name: caseElement.name.text, + name: caseName, parameters: parameters ?? [], swiftDecl: node, enumType: SwiftNominalType(nominalTypeDecl: typeContext.swiftNominal), diff --git a/Tests/JExtractSwiftTests/JavaKeywordTests.swift b/Tests/JExtractSwiftTests/JavaKeywordTests.swift new file mode 100644 index 000000000..eea723480 --- /dev/null +++ b/Tests/JExtractSwiftTests/JavaKeywordTests.swift @@ -0,0 +1,141 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import Testing + +@Suite +struct JavaKeywordTests { + @Test + func functionName() throws { + let text = + """ + public struct Foo { + public func final() + } + """ + + try assertOutput( + input: text, + .ffm, + .java, + expectedChunks: [ + """ + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_Foo_final"); + """, + """ + public void final_() { + """, + ] + ) + + try assertOutput( + input: text, + .ffm, + .swift, + expectedChunks: [ + """ + @_cdecl("swiftjava_SwiftModule_Foo_final") + """ + ] + ) + } + + @Test + func enumCase() throws { + let text = + """ + public enum MyEnum { + case null + } + """ + + try assertOutput( + input: text, + .jni, + .java, + expectedChunks: [ + """ + public static MyEnum null_(SwiftArena swiftArena) { + """, + """ + public record Null() implements Case { + """, + ] + ) + } + + @Test + func enumCaseWithAssociatedValue() throws { + let text = + """ + public enum MyEnumWithValue { + case instanceof(String) + case none + } + """ + + try assertOutput( + input: text, + .jni, + .java, + expectedChunks: [ + """ + public static MyEnumWithValue instanceof_(java.lang.String arg0, SwiftArena swiftArena) { + """, + """ + public record Instanceof(java.lang.String arg0) implements Case { + """, + """ + private static native Instanceof._NativeParameters $getAsInstanceof(long selfPointer); + """, + ] + ) + + try assertOutput( + input: text, + .jni, + .swift, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyEnumWithValue__00024getAsInstanceof__J") + """ + ] + ) + } + + @Test + func enumCaseWithBacktick() throws { + let text = + """ + public enum MyEnum { + case `default` + } + """ + + try assertOutput( + input: text, + .jni, + .java, + expectedChunks: [ + """ + public static MyEnum default_(SwiftArena swiftArena) { + """, + """ + public record Default() implements Case { + """, + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/SwiftEscapedNameTests.swift b/Tests/JExtractSwiftTests/SwiftEscapedNameTests.swift new file mode 100644 index 000000000..c388584ba --- /dev/null +++ b/Tests/JExtractSwiftTests/SwiftEscapedNameTests.swift @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import Testing + +@Suite +struct SwiftEscapedNameTests { + @Test + func function() throws { + try assertOutput( + input: """ + public struct MyStruct { + public func `guard`() + } + """, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + "public void guard() {", + "private static native void $guard(long selfPointer);", + ], + ) + } + + @Test + func enumCase() throws { + try assertOutput( + input: """ + public enum MyEnum { + case `let` + } + """, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + "public static MyEnum let(SwiftArena swiftArena) {", + "public record Let() implements Case {", + ], + ) + } +}