Skip to content

Commit 5ef5527

Browse files
committed
Java2Swift: Drive recursion into nested subclasses from the outside
Rather than having the Java-to-Swift translator walking all nested classes itself, provide it with the set of nested classes that should be translated along with the enclosing class. This allows us to be explicit about the naming of nested classes in the config file (where we want to), while still getting the convenience of translating the nested classes automatically for you by default.
1 parent 70cd2de commit 5ef5527

File tree

3 files changed

+92
-42
lines changed

3 files changed

+92
-42
lines changed

Sources/Java2Swift/JavaToSwift.swift

Lines changed: 66 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ struct JavaToSwift: ParsableCommand {
201201
// Add the configuration for this module.
202202
translator.addConfiguration(config, forSwiftModule: moduleName)
203203

204-
// Load all of the requested classes.
204+
// Load all of the explicitly-requested classes.
205205
let classLoader = try JavaClass<ClassLoader>(environment: environment)
206206
.getSystemClassLoader()!
207207
var javaClasses: [JavaClass<JavaObject>] = []
@@ -211,29 +211,50 @@ struct JavaToSwift: ParsableCommand {
211211
continue
212212
}
213213

214+
// Add this class to the list of classes we'll translate.
214215
javaClasses.append(javaClass)
216+
}
217+
218+
// Find all of the nested classes for each class, adding them to the list
219+
// of classes to be translated if they were already specified.
220+
var allClassesToVisit = javaClasses
221+
var currentClassIndex: Int = 0
222+
while currentClassIndex < allClassesToVisit.count {
223+
defer {
224+
currentClassIndex += 1
225+
}
226+
227+
// Find all of the nested classes that weren't explicitly translated
228+
// already.
229+
let currentClass = allClassesToVisit[currentClassIndex]
230+
let nestedClasses: [JavaClass<JavaObject>] = currentClass.getClasses().compactMap { nestedClass in
231+
guard let nestedClass else { return nil }
232+
233+
// If this is a local class, we're done.
234+
let javaClassName = nestedClass.getName()
235+
if javaClassName.isLocalJavaClass {
236+
return nil
237+
}
215238

216-
// Replace any $'s within the Java class name (which separate nested
217-
// classes) with .'s (which represent nesting in Swift).
218-
let translatedSwiftName = swiftName.replacing("$", with: ".")
219-
220-
// Note that we will be translating this Java class, so it is a known class.
221-
translator.translatedClasses[javaClassName] = (translatedSwiftName, nil, true)
222-
223-
var classes: [JavaClass<JavaObject>?] = javaClass.getClasses()
224-
225-
// Go through all subclasses to find all of the classes to translate
226-
while let internalClass = classes.popLast() {
227-
if let internalClass {
228-
let (javaName, swiftName) = names(from: internalClass.getName())
229-
// If we have already been through this class, don't go through it again
230-
guard translator.translatedClasses[javaName] == nil else { continue }
231-
let currentClassName = swiftName
232-
let currentSanitizedClassName = currentClassName.replacing("$", with: ".")
233-
classes.append(contentsOf: internalClass.getClasses())
234-
translator.translatedClasses[javaName] = (currentSanitizedClassName, nil, true)
239+
// If this class has been explicitly mentioned, we're done.
240+
if translator.translatedClasses[javaClassName] != nil {
241+
return nil
235242
}
243+
244+
// Record this as a translated class.
245+
let swiftName = javaClassName.defaultSwiftNameForJavaClass
246+
translator.translatedClasses[javaClassName] = (swiftName, nil, true)
247+
return nestedClass
236248
}
249+
250+
// If there were no new nested classes, there's nothing to do.
251+
if nestedClasses.isEmpty {
252+
continue
253+
}
254+
255+
// Record all of the nested classes that we will visit.
256+
translator.nestedClasses[currentClass.getName()] = nestedClasses
257+
allClassesToVisit.append(contentsOf: nestedClasses)
237258
}
238259

239260
// Translate all of the Java classes into Swift classes.
@@ -276,7 +297,7 @@ struct JavaToSwift: ParsableCommand {
276297
javaClassName = javaClassNameOpt
277298
}
278299

279-
return (javaClassName, swiftName)
300+
return (javaClassName, swiftName.javaClassNameToCanonicalName)
280301
}
281302

282303
mutating func writeContents(_ contents: String, to filename: String, description: String) throws {
@@ -317,12 +338,9 @@ struct JavaToSwift: ParsableCommand {
317338
continue
318339
}
319340

320-
// If any of the segments of the Java name start with a number, it's a
321-
// local class that cannot be mapped into Swift.
322-
for segment in entry.getName().split(separator: "$") {
323-
if let firstChar = segment.first, firstChar.isNumber {
324-
continue
325-
}
341+
// If this is a local class, it cannot be mapped into Swift.
342+
if entry.getName().isLocalJavaClass {
343+
continue
326344
}
327345

328346
let javaCanonicalName = String(entry.getName().replacing("/", with: ".")
@@ -365,10 +383,10 @@ extension String {
365383
fileprivate var defaultSwiftNameForJavaClass: String {
366384
if let dotLoc = lastIndex(of: ".") {
367385
let afterDot = index(after: dotLoc)
368-
return String(self[afterDot...])
386+
return String(self[afterDot...]).javaClassNameToCanonicalName
369387
}
370388

371-
return self
389+
return javaClassNameToCanonicalName
372390
}
373391
}
374392

@@ -382,3 +400,22 @@ extension JavaClass<ClassLoader> {
382400
@JavaStaticMethod
383401
public func getSystemClassLoader() -> ClassLoader?
384402
}
403+
404+
extension String {
405+
/// Replace all of the $'s for nested names with "." to turn a Java class
406+
/// name into a Java canonical class name,
407+
fileprivate var javaClassNameToCanonicalName: String {
408+
return replacing("$", with: ".")
409+
}
410+
411+
/// Whether this is the name of an anonymous class.
412+
fileprivate var isLocalJavaClass: Bool {
413+
for segment in split(separator: "$") {
414+
if let firstChar = segment.first, firstChar.isNumber {
415+
return true
416+
}
417+
}
418+
419+
return false
420+
}
421+
}

Sources/Java2SwiftLib/JavaTranslator.swift

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ package class JavaTranslator {
4343
/// methods will be implemented in Swift.
4444
package var swiftNativeImplementations: Set<String> = []
4545

46+
/// The set of nested classes that we should traverse from the given class,
47+
/// indexed by the name of the class.
48+
///
49+
/// TODO: Make JavaClass Hashable so we can index by the object?
50+
package var nestedClasses: [String: [JavaClass<JavaObject>]] = [:]
51+
4652
package init(
4753
swiftModuleName: String,
4854
environment: JNIEnvironment,
@@ -79,7 +85,6 @@ extension JavaTranslator {
7985
/// itself. This should only be used to refer to types that are built-in to
8086
/// JavaKit and therefore aren't captured in any configuration file.
8187
package static let defaultTranslatedClasses: [String: (swiftType: String, swiftModule: String?, isOptional: Bool)] = [
82-
"java.lang.Class": ("JavaClass", "JavaKit", true),
8388
"java.lang.String": ("String", "JavaKit", false),
8489
]
8590
}
@@ -391,14 +396,12 @@ extension JavaTranslator {
391396

392397
topLevelDecls.append(classDecl)
393398

394-
let subClassDecls = javaClass.getClasses().compactMap {
395-
$0.flatMap { clazz in
396-
do {
397-
return try translateClass(clazz)
398-
} catch {
399-
logUntranslated("Unable to translate '\(fullName)' subclass '\(clazz.getName())': \(error)")
400-
return nil
401-
}
399+
let subClassDecls = (nestedClasses[fullName] ?? []).compactMap { clazz in
400+
do {
401+
return try translateClass(clazz)
402+
} catch {
403+
logUntranslated("Unable to translate '\(fullName)' subclass '\(clazz.getName())': \(error)")
404+
return nil
402405
}
403406
}.flatMap(\.self)
404407

Tests/Java2SwiftTests/Java2SwiftTests.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,15 @@ var jvm: JavaVirtualMachine {
2525

2626
@JavaClass("java.time.Month")
2727
public struct JavaMonth {}
28+
2829
@JavaClass("java.lang.ProcessBuilder")
29-
struct ProcessBuilder {}
30+
struct ProcessBuilder {
31+
@JavaClass("java.lang.ProcessBuilder$Redirect")
32+
struct Redirect {
33+
@JavaClass("java.lang.ProcessBuilder$Redirect$Type")
34+
struct JavaType { }
35+
}
36+
}
3037

3138
class Java2SwiftTests: XCTestCase {
3239
func testJavaLangObjectMapping() throws {
@@ -146,6 +153,10 @@ class Java2SwiftTests: XCTestCase {
146153
"java.lang.ProcessBuilder$Redirect": ("ProcessBuilder.Redirect", nil, true),
147154
"java.lang.ProcessBuilder$Redirect$Type": ("ProcessBuilder.Redirect.Type", nil, true),
148155
],
156+
nestedClasses: [
157+
"java.lang.ProcessBuilder": [JavaClass<ProcessBuilder.Redirect>().as(JavaClass<JavaObject>.self)!],
158+
"java.lang.ProcessBuilder$Redirect": [JavaClass<ProcessBuilder.Redirect.JavaType>().as(JavaClass<JavaObject>.self)!],
159+
],
149160
expectedChunks: [
150161
"import JavaKit",
151162
"""
@@ -165,7 +176,6 @@ class Java2SwiftTests: XCTestCase {
165176
]
166177
)
167178
}
168-
169179
}
170180

171181
@JavaClass("java.util.ArrayList")
@@ -184,6 +194,7 @@ func assertTranslatedClass<JavaClassType: AnyJavaObject>(
184194
translatedClasses: [
185195
String: (swiftType: String, swiftModule: String?, isOptional: Bool)
186196
] = JavaTranslator.defaultTranslatedClasses,
197+
nestedClasses: [String: [JavaClass<JavaObject>]] = [:],
187198
expectedChunks: [String],
188199
file: StaticString = #filePath,
189200
line: UInt = #line
@@ -196,8 +207,7 @@ func assertTranslatedClass<JavaClassType: AnyJavaObject>(
196207

197208
translator.translatedClasses = translatedClasses
198209
translator.translatedClasses[javaType.fullJavaClassName] = (swiftTypeName, nil, true)
199-
200-
210+
translator.nestedClasses = nestedClasses
201211
translator.startNewFile()
202212
let translatedDecls = try translator.translateClass(
203213
JavaClass<JavaObject>(

0 commit comments

Comments
 (0)