Skip to content

Commit 97473d9

Browse files
authored
Make nested classes explicitly-specifiable and work through some integration issues (#118)
* Java2Swift: Add a test for a generic static method * Drop the "canonicalClassName" from JavaTypes because it is now incorrect We're using the "normal" name now that uses $ for nested classes. * Remove dead code behind a `#if false` * Teach the Java2Swift plugin to match the nested type naming scheme for Java2Swift * Regenerate java.util.jar.Attributes with the "isFinal" change * 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. * Update JavaSieve example now that we're handling nested classes * Java2Swift: Use the parent's Swift type name to form nested type names * Escape member types named "Type" * Work around issues with Java classes named "Type" by renaming them "JavaType" This is deeply unfortunate, but we need compile-side fixes for nested types named "Type" to work.
1 parent ec76084 commit 97473d9

File tree

10 files changed

+242
-77
lines changed

10 files changed

+242
-77
lines changed

Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ struct Java2SwiftBuildToolPlugin: BuildToolPlugin {
9090
/// Determine the set of Swift files that will be emitted by the Java2Swift
9191
/// tool.
9292
let outputSwiftFiles = config.classes.map { (javaClassName, swiftName) in
93-
outputDirectory.appending(path: "\(swiftName).swift")
93+
let swiftNestedName = swiftName.replacingOccurrences(of: ".", with: "+")
94+
return outputDirectory.appending(path: "\(swiftNestedName).swift")
9495
}
9596

9697
// Find the Java .class files generated from prior plugins.

Samples/JavaSieve/Sources/JavaSieve/Java2Swift.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"com.gazman.quadratic_sieve.data.VectorWorkData" : "VectorWorkData",
2121
"com.gazman.quadratic_sieve.debug.Analytics" : "Analytics",
2222
"com.gazman.quadratic_sieve.debug.AssertUtils" : "AssertUtils",
23+
"com.gazman.quadratic_sieve.debug.AssertUtils$Tester" : "AssertUtils.Tester",
2324
"com.gazman.quadratic_sieve.debug.Logger" : "Logger",
2425
"com.gazman.quadratic_sieve.fact.TrivialDivision" : "TrivialDivision",
2526
"com.gazman.quadratic_sieve.primes.BigPrimes" : "BigPrimes",

Sources/Java2Swift/JavaToSwift.swift

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

204-
// Load all of the requested classes.
205-
#if false
206-
let classLoader = URLClassLoader(
207-
[
208-
try URL("file://\(classPath)", environment: environment)
209-
],
210-
environment: environment
211-
)
212-
#else
204+
// Load all of the explicitly-requested classes.
213205
let classLoader = try JavaClass<ClassLoader>(environment: environment)
214206
.getSystemClassLoader()!
215-
#endif
216207
var javaClasses: [JavaClass<JavaObject>] = []
217208
for (javaClassName, swiftName) in config.classes {
218209
guard let javaClass = try classLoader.loadClass(javaClassName) else {
219210
print("warning: could not find Java class '\(javaClassName)'")
220211
continue
221212
}
222213

214+
// Add this class to the list of classes we'll translate.
223215
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+
// The current class we're in.
228+
let currentClass = allClassesToVisit[currentClassIndex]
229+
guard let currentSwiftName = translator.translatedClasses[currentClass.getName()]?.swiftType else {
230+
continue
231+
}
232+
233+
// Find all of the nested classes that weren't explicitly translated
234+
// already.
235+
let nestedClasses: [JavaClass<JavaObject>] = currentClass.getClasses().compactMap { nestedClass in
236+
guard let nestedClass else { return nil }
224237

225-
// Replace any $'s within the Java class name (which separate nested
226-
// classes) with .'s (which represent nesting in Swift).
227-
let translatedSwiftName = swiftName.replacing("$", with: ".")
228-
229-
// Note that we will be translating this Java class, so it is a known class.
230-
translator.translatedClasses[javaClassName] = (translatedSwiftName, nil, true)
231-
232-
var classes: [JavaClass<JavaObject>?] = javaClass.getClasses()
233-
234-
// Go through all subclasses to find all of the classes to translate
235-
while let internalClass = classes.popLast() {
236-
if let internalClass {
237-
let (javaName, swiftName) = names(from: internalClass.getName())
238-
// If we have already been through this class, don't go through it again
239-
guard translator.translatedClasses[javaName] == nil else { continue }
240-
let currentClassName = swiftName
241-
let currentSanitizedClassName = currentClassName.replacing("$", with: ".")
242-
classes.append(contentsOf: internalClass.getClasses())
243-
translator.translatedClasses[javaName] = (currentSanitizedClassName, nil, true)
238+
// If this is a local class, we're done.
239+
let javaClassName = nestedClass.getName()
240+
if javaClassName.isLocalJavaClass {
241+
return nil
244242
}
243+
244+
// If this class has been explicitly mentioned, we're done.
245+
if translator.translatedClasses[javaClassName] != nil {
246+
return nil
247+
}
248+
249+
// Record this as a translated class.
250+
let swiftUnqualifiedName = javaClassName.javaClassNameToCanonicalName
251+
.defaultSwiftNameForJavaClass
252+
253+
254+
let swiftName = "\(currentSwiftName).\(swiftUnqualifiedName)"
255+
translator.translatedClasses[javaClassName] = (swiftName, nil, true)
256+
return nestedClass
257+
}
258+
259+
// If there were no new nested classes, there's nothing to do.
260+
if nestedClasses.isEmpty {
261+
continue
245262
}
263+
264+
// Record all of the nested classes that we will visit.
265+
translator.nestedClasses[currentClass.getName()] = nestedClasses
266+
allClassesToVisit.append(contentsOf: nestedClasses)
246267
}
247268

248269
// Translate all of the Java classes into Swift classes.
@@ -285,7 +306,7 @@ struct JavaToSwift: ParsableCommand {
285306
javaClassName = javaClassNameOpt
286307
}
287308

288-
return (javaClassName, swiftName)
309+
return (javaClassName, swiftName.javaClassNameToCanonicalName)
289310
}
290311

291312
mutating func writeContents(_ contents: String, to filename: String, description: String) throws {
@@ -326,12 +347,9 @@ struct JavaToSwift: ParsableCommand {
326347
continue
327348
}
328349

329-
// If any of the segments of the Java name start with a number, it's a
330-
// local class that cannot be mapped into Swift.
331-
for segment in entry.getName().split(separator: "$") {
332-
if let firstChar = segment.first, firstChar.isNumber {
333-
continue
334-
}
350+
// If this is a local class, it cannot be mapped into Swift.
351+
if entry.getName().isLocalJavaClass {
352+
continue
335353
}
336354

337355
let javaCanonicalName = String(entry.getName().replacing("/", with: ".")
@@ -374,10 +392,10 @@ extension String {
374392
fileprivate var defaultSwiftNameForJavaClass: String {
375393
if let dotLoc = lastIndex(of: ".") {
376394
let afterDot = index(after: dotLoc)
377-
return String(self[afterDot...])
395+
return String(self[afterDot...]).javaClassNameToCanonicalName.adjustedSwiftTypeName
378396
}
379397

380-
return self
398+
return javaClassNameToCanonicalName.adjustedSwiftTypeName
381399
}
382400
}
383401

@@ -391,3 +409,30 @@ extension JavaClass<ClassLoader> {
391409
@JavaStaticMethod
392410
public func getSystemClassLoader() -> ClassLoader?
393411
}
412+
413+
extension String {
414+
/// Replace all of the $'s for nested names with "." to turn a Java class
415+
/// name into a Java canonical class name,
416+
fileprivate var javaClassNameToCanonicalName: String {
417+
return replacing("$", with: ".")
418+
}
419+
420+
/// Whether this is the name of an anonymous class.
421+
fileprivate var isLocalJavaClass: Bool {
422+
for segment in split(separator: "$") {
423+
if let firstChar = segment.first, firstChar.isNumber {
424+
return true
425+
}
426+
}
427+
428+
return false
429+
}
430+
431+
/// Adjust type name for "bad" type names that don't work well in Swift.
432+
fileprivate var adjustedSwiftTypeName: String {
433+
switch self {
434+
case "Type": return "JavaType"
435+
default: return self
436+
}
437+
}
438+
}

Sources/Java2SwiftLib/JavaTranslator.swift

Lines changed: 51 additions & 12 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
}
@@ -165,19 +170,28 @@ extension JavaTranslator {
165170
let javaType = try JavaType(javaTypeName: javaClass.getName())
166171
let isSwiftOptional = javaType.isSwiftOptional
167172
return (
168-
try javaType.swiftTypeName(resolver: self.getSwiftTypeNameFromJavaClassName(_:)),
173+
try javaType.swiftTypeName { javaClassName in
174+
try self.getSwiftTypeNameFromJavaClassName(javaClassName)
175+
},
169176
isSwiftOptional
170177
)
171178
}
172179

173180
/// Map a Java class name to its corresponding Swift type.
174-
private func getSwiftTypeNameFromJavaClassName(_ name: String) throws -> String {
181+
private func getSwiftTypeNameFromJavaClassName(
182+
_ name: String,
183+
escapeMemberNames: Bool = true
184+
) throws -> String {
175185
if let translated = translatedClasses[name] {
176186
// Note that we need to import this Swift module.
177187
if let swiftModule = translated.swiftModule, swiftModule != swiftModuleName {
178188
importedSwiftModules.insert(swiftModule)
179189
}
180190

191+
if escapeMemberNames {
192+
return translated.swiftType.escapingSwiftMemberNames
193+
}
194+
181195
return translated.swiftType
182196
}
183197

@@ -192,7 +206,7 @@ extension JavaTranslator {
192206
/// JavaClass to house static methods.
193207
package func translateClass(_ javaClass: JavaClass<JavaObject>) throws -> [DeclSyntax] {
194208
let fullName = javaClass.getName()
195-
let swiftTypeName = try getSwiftTypeNameFromJavaClassName(fullName)
209+
let swiftTypeName = try getSwiftTypeNameFromJavaClassName(fullName, escapeMemberNames: false)
196210
let (swiftParentType, swiftInnermostTypeName) = swiftTypeName.splitSwiftTypeName()
197211

198212
// If the swift parent type has not been translated, don't try to translate this one
@@ -391,14 +405,12 @@ extension JavaTranslator {
391405

392406
topLevelDecls.append(classDecl)
393407

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-
}
408+
let subClassDecls = (nestedClasses[fullName] ?? []).compactMap { clazz in
409+
do {
410+
return try translateClass(clazz)
411+
} catch {
412+
logUntranslated("Unable to translate '\(fullName)' subclass '\(clazz.getName())': \(error)")
413+
return nil
402414
}
403415
}.flatMap(\.self)
404416

@@ -633,3 +645,30 @@ extension JavaTranslator {
633645
}
634646
}
635647
}
648+
649+
extension String {
650+
/// Escape Swift types that involve member name references like '.Type'
651+
fileprivate var escapingSwiftMemberNames: String {
652+
var count = 0
653+
return split(separator: ".").map { component in
654+
defer {
655+
count += 1
656+
}
657+
658+
if count > 0 && component.memberRequiresBackticks {
659+
return "`\(component)`"
660+
}
661+
662+
return String(component)
663+
}.joined(separator: ".")
664+
}
665+
}
666+
667+
extension Substring {
668+
fileprivate var memberRequiresBackticks: Bool {
669+
switch self {
670+
case "Type": return true
671+
default: return false
672+
}
673+
}
674+
}

Sources/JavaKit/Optional+JavaObject.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ extension Optional: JavaValue where Wrapped: AnyJavaObject {
3737
}
3838

3939
public static var javaType: JavaType {
40-
JavaType(canonicalClassName: Wrapped.fullJavaClassName)
40+
JavaType(className: Wrapped.fullJavaClassName)
4141
}
4242

4343
public static func jniMethodCall(

Sources/JavaKitJar/generated/Attributes.swift

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -133,57 +133,57 @@ extension Attributes {
133133
}
134134
}
135135
extension JavaClass<Attributes.Name> {
136-
@JavaStaticField
136+
@JavaStaticField(isFinal: true)
137137
public var MANIFEST_VERSION: Attributes.Name?
138138

139-
@JavaStaticField
139+
@JavaStaticField(isFinal: true)
140140
public var SIGNATURE_VERSION: Attributes.Name?
141141

142-
@JavaStaticField
142+
@JavaStaticField(isFinal: true)
143143
public var CONTENT_TYPE: Attributes.Name?
144144

145-
@JavaStaticField
145+
@JavaStaticField(isFinal: true)
146146
public var CLASS_PATH: Attributes.Name?
147147

148-
@JavaStaticField
148+
@JavaStaticField(isFinal: true)
149149
public var MAIN_CLASS: Attributes.Name?
150150

151-
@JavaStaticField
151+
@JavaStaticField(isFinal: true)
152152
public var SEALED: Attributes.Name?
153153

154-
@JavaStaticField
154+
@JavaStaticField(isFinal: true)
155155
public var EXTENSION_LIST: Attributes.Name?
156156

157-
@JavaStaticField
157+
@JavaStaticField(isFinal: true)
158158
public var EXTENSION_NAME: Attributes.Name?
159159

160-
@JavaStaticField
160+
@JavaStaticField(isFinal: true)
161161
public var EXTENSION_INSTALLATION: Attributes.Name?
162162

163-
@JavaStaticField
163+
@JavaStaticField(isFinal: true)
164164
public var IMPLEMENTATION_TITLE: Attributes.Name?
165165

166-
@JavaStaticField
166+
@JavaStaticField(isFinal: true)
167167
public var IMPLEMENTATION_VERSION: Attributes.Name?
168168

169-
@JavaStaticField
169+
@JavaStaticField(isFinal: true)
170170
public var IMPLEMENTATION_VENDOR: Attributes.Name?
171171

172-
@JavaStaticField
172+
@JavaStaticField(isFinal: true)
173173
public var IMPLEMENTATION_VENDOR_ID: Attributes.Name?
174174

175-
@JavaStaticField
175+
@JavaStaticField(isFinal: true)
176176
public var IMPLEMENTATION_URL: Attributes.Name?
177177

178-
@JavaStaticField
178+
@JavaStaticField(isFinal: true)
179179
public var SPECIFICATION_TITLE: Attributes.Name?
180180

181-
@JavaStaticField
181+
@JavaStaticField(isFinal: true)
182182
public var SPECIFICATION_VERSION: Attributes.Name?
183183

184-
@JavaStaticField
184+
@JavaStaticField(isFinal: true)
185185
public var SPECIFICATION_VENDOR: Attributes.Name?
186186

187-
@JavaStaticField
187+
@JavaStaticField(isFinal: true)
188188
public var MULTI_RELEASE: Attributes.Name?
189189
}

Sources/JavaTypes/JavaType+JavaSource.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ extension JavaType {
3232
self = try JavaType(mangledName: name)
3333

3434
case let className:
35-
self = JavaType(canonicalClassName: className)
35+
self = JavaType(className: className)
3636
}
3737
}
3838
}

0 commit comments

Comments
 (0)