Skip to content

Commit 5cd3a68

Browse files
authored
jextract: Implement identifier escaping for functions and enum cases (#697)
1 parent b43b85e commit 5cd3a68

7 files changed

Lines changed: 246 additions & 10 deletions

File tree

Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/EnumWithValueCases.swift renamed to Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Enums.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,8 @@ public enum EnumWithValueCases {
1616
case firstCase(UInt)
1717
case secondCase
1818
}
19+
20+
public enum EnumWithBacktick {
21+
case `let`
22+
case `default`
23+
}

Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EnumWithValueCasesTest.java renamed to Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EnumTest.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,25 @@
1515
package com.example.swift;
1616

1717
import org.junit.jupiter.api.Test;
18-
import org.swift.swiftkit.core.ConfinedSwiftMemorySession;
1918
import org.swift.swiftkit.core.SwiftArena;
2019

21-
import java.util.Optional;
22-
2320
import static org.junit.jupiter.api.Assertions.*;
2421

25-
public class EnumWithValueCasesTest {
22+
public class EnumTest {
2623
@Test
27-
void fn() {
24+
void enumWithValueCases() {
2825
try (var arena = SwiftArena.ofConfined()) {
2926
EnumWithValueCases e = EnumWithValueCases.firstCase(48, arena);
3027
EnumWithValueCases.FirstCase c = (EnumWithValueCases.FirstCase) e.getCase();
3128
assertNotNull(c);
3229
}
3330
}
34-
}
31+
32+
@Test
33+
void enumWithBacktick() {
34+
try (var arena = SwiftArena.ofConfined()) {
35+
EnumWithBacktick e = EnumWithBacktick.default_(arena);
36+
assertTrue(e.getAsDefault().isPresent());
37+
}
38+
}
39+
}

Sources/JExtractSwiftLib/Convenience/String+Extensions.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,14 @@ extension String {
8787

8888
return .class(package: javaPackageName, name: javaClassName)
8989
}
90+
91+
/// Unescapes the name if it is surrounded by backticks.
92+
var unescapedSwiftName: String {
93+
if count >= 2 && hasPrefix("`") && hasSuffix("`") {
94+
return String(dropFirst().dropLast())
95+
}
96+
return self
97+
}
9098
}
9199

92100
extension Array where Element == String {

Sources/JExtractSwiftLib/JavaIdentifierFactory.swift

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ package struct JavaIdentifierFactory {
6868
case .setter, .subscriptSetter: decl.javaSetterName
6969
case .function, .initializer, .enumCase: decl.name
7070
}
71-
return baseName + paramsSuffix(decl, baseName: baseName)
71+
var methodName = baseName + paramsSuffix(decl, baseName: baseName)
72+
if Self.javaKeywords.contains(methodName) {
73+
methodName += "_"
74+
}
75+
return methodName
7276
}
7377

7478
private func paramsSuffix(_ decl: ImportedFunc, baseName: String) -> String {
@@ -86,4 +90,22 @@ package struct JavaIdentifierFactory {
8690
return labels.map { $0.prefix(1).uppercased() + $0.dropFirst() }.joined()
8791
}
8892
}
93+
94+
static let javaKeywords: Set<String> = [
95+
/// https://docs.oracle.com/javase/specs/jls/se25/html/jls-3.html#jls-3.9
96+
"abstract", "continue", "for", "new", "switch",
97+
"assert", "default", "if", "package", "synchronized",
98+
"boolean", "do", "goto", "private", "this",
99+
"break", "double", "implements", "protected", "throw",
100+
"byte", "else", "import", "public", "throws",
101+
"case", "enum", "instanceof", "return", "transient",
102+
"catch", "extends", "int", "short", "try",
103+
"char", "final", "interface", "static", "void",
104+
"class", "finally", "long", "strictfp", "volatile",
105+
"const", "float", "native", "super", "while",
106+
"_",
107+
108+
/// literals
109+
"true", "false", "null",
110+
]
89111
}

Sources/JExtractSwiftLib/Swift2JavaVisitor.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ final class Swift2JavaVisitor {
186186
let imported = ImportedFunc(
187187
module: translator.swiftModuleName,
188188
swiftDecl: node,
189-
name: node.name.text,
189+
name: node.name.text.unescapedSwiftName,
190190
apiKind: .function,
191191
functionSignature: signature,
192192
)
@@ -227,16 +227,17 @@ final class Swift2JavaVisitor {
227227
lookupContext: translator.lookupContext,
228228
)
229229

230+
let caseName = caseElement.name.text.unescapedSwiftName
230231
let caseFunction = ImportedFunc(
231232
module: translator.swiftModuleName,
232233
swiftDecl: node,
233-
name: caseElement.name.text,
234+
name: caseName,
234235
apiKind: .enumCase,
235236
functionSignature: signature,
236237
)
237238

238239
let importedCase = ImportedEnumCase(
239-
name: caseElement.name.text,
240+
name: caseName,
240241
parameters: parameters ?? [],
241242
swiftDecl: node,
242243
enumType: SwiftNominalType(nominalTypeDecl: typeContext.swiftNominal),
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Testing
16+
17+
@Suite
18+
struct JavaKeywordTests {
19+
@Test
20+
func functionName() throws {
21+
let text =
22+
"""
23+
public struct Foo {
24+
public func final()
25+
}
26+
"""
27+
28+
try assertOutput(
29+
input: text,
30+
.ffm,
31+
.java,
32+
expectedChunks: [
33+
"""
34+
private static final MemorySegment ADDR =
35+
SwiftModule.findOrThrow("swiftjava_SwiftModule_Foo_final");
36+
""",
37+
"""
38+
public void final_() {
39+
""",
40+
]
41+
)
42+
43+
try assertOutput(
44+
input: text,
45+
.ffm,
46+
.swift,
47+
expectedChunks: [
48+
"""
49+
@_cdecl("swiftjava_SwiftModule_Foo_final")
50+
"""
51+
]
52+
)
53+
}
54+
55+
@Test
56+
func enumCase() throws {
57+
let text =
58+
"""
59+
public enum MyEnum {
60+
case null
61+
}
62+
"""
63+
64+
try assertOutput(
65+
input: text,
66+
.jni,
67+
.java,
68+
expectedChunks: [
69+
"""
70+
public static MyEnum null_(SwiftArena swiftArena) {
71+
""",
72+
"""
73+
public record Null() implements Case {
74+
""",
75+
]
76+
)
77+
}
78+
79+
@Test
80+
func enumCaseWithAssociatedValue() throws {
81+
let text =
82+
"""
83+
public enum MyEnumWithValue {
84+
case instanceof(String)
85+
case none
86+
}
87+
"""
88+
89+
try assertOutput(
90+
input: text,
91+
.jni,
92+
.java,
93+
expectedChunks: [
94+
"""
95+
public static MyEnumWithValue instanceof_(java.lang.String arg0, SwiftArena swiftArena) {
96+
""",
97+
"""
98+
public record Instanceof(java.lang.String arg0) implements Case {
99+
""",
100+
"""
101+
private static native Instanceof._NativeParameters $getAsInstanceof(long selfPointer);
102+
""",
103+
]
104+
)
105+
106+
try assertOutput(
107+
input: text,
108+
.jni,
109+
.swift,
110+
expectedChunks: [
111+
"""
112+
@_cdecl("Java_com_example_swift_MyEnumWithValue__00024getAsInstanceof__J")
113+
"""
114+
]
115+
)
116+
}
117+
118+
@Test
119+
func enumCaseWithBacktick() throws {
120+
let text =
121+
"""
122+
public enum MyEnum {
123+
case `default`
124+
}
125+
"""
126+
127+
try assertOutput(
128+
input: text,
129+
.jni,
130+
.java,
131+
expectedChunks: [
132+
"""
133+
public static MyEnum default_(SwiftArena swiftArena) {
134+
""",
135+
"""
136+
public record Default() implements Case {
137+
""",
138+
]
139+
)
140+
}
141+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Testing
16+
17+
@Suite
18+
struct SwiftEscapedNameTests {
19+
@Test
20+
func function() throws {
21+
try assertOutput(
22+
input: """
23+
public struct MyStruct {
24+
public func `guard`()
25+
}
26+
""",
27+
.jni,
28+
.java,
29+
detectChunkByInitialLines: 1,
30+
expectedChunks: [
31+
"public void guard() {",
32+
"private static native void $guard(long selfPointer);",
33+
],
34+
)
35+
}
36+
37+
@Test
38+
func enumCase() throws {
39+
try assertOutput(
40+
input: """
41+
public enum MyEnum {
42+
case `let`
43+
}
44+
""",
45+
.jni,
46+
.java,
47+
detectChunkByInitialLines: 1,
48+
expectedChunks: [
49+
"public static MyEnum let(SwiftArena swiftArena) {",
50+
"public record Let() implements Case {",
51+
],
52+
)
53+
}
54+
}

0 commit comments

Comments
 (0)