diff --git a/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift b/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift index 37ee1b3a..452ec8b3 100644 --- a/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift +++ b/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift @@ -90,7 +90,8 @@ struct Java2SwiftBuildToolPlugin: BuildToolPlugin { /// Determine the set of Swift files that will be emitted by the Java2Swift /// tool. let outputSwiftFiles = config.classes.map { (javaClassName, swiftName) in - outputDirectory.appending(path: "\(swiftName).swift") + let swiftNestedName = swiftName.replacingOccurrences(of: ".", with: "+") + return outputDirectory.appending(path: "\(swiftNestedName).swift") } // Find the Java .class files generated from prior plugins. diff --git a/Samples/JavaSieve/Sources/JavaSieve/Java2Swift.config b/Samples/JavaSieve/Sources/JavaSieve/Java2Swift.config index aa88865f..61d5cb31 100644 --- a/Samples/JavaSieve/Sources/JavaSieve/Java2Swift.config +++ b/Samples/JavaSieve/Sources/JavaSieve/Java2Swift.config @@ -20,6 +20,7 @@ "com.gazman.quadratic_sieve.data.VectorWorkData" : "VectorWorkData", "com.gazman.quadratic_sieve.debug.Analytics" : "Analytics", "com.gazman.quadratic_sieve.debug.AssertUtils" : "AssertUtils", + "com.gazman.quadratic_sieve.debug.AssertUtils$Tester" : "AssertUtils.Tester", "com.gazman.quadratic_sieve.debug.Logger" : "Logger", "com.gazman.quadratic_sieve.fact.TrivialDivision" : "TrivialDivision", "com.gazman.quadratic_sieve.primes.BigPrimes" : "BigPrimes", diff --git a/Sources/Java2Swift/JavaToSwift.swift b/Sources/Java2Swift/JavaToSwift.swift index 86d3cf8d..205c6a43 100644 --- a/Sources/Java2Swift/JavaToSwift.swift +++ b/Sources/Java2Swift/JavaToSwift.swift @@ -201,18 +201,9 @@ struct JavaToSwift: ParsableCommand { // Add the configuration for this module. translator.addConfiguration(config, forSwiftModule: moduleName) - // Load all of the requested classes. - #if false - let classLoader = URLClassLoader( - [ - try URL("file://\(classPath)", environment: environment) - ], - environment: environment - ) - #else + // Load all of the explicitly-requested classes. let classLoader = try JavaClass(environment: environment) .getSystemClassLoader()! - #endif var javaClasses: [JavaClass] = [] for (javaClassName, swiftName) in config.classes { guard let javaClass = try classLoader.loadClass(javaClassName) else { @@ -220,29 +211,59 @@ struct JavaToSwift: ParsableCommand { continue } + // Add this class to the list of classes we'll translate. javaClasses.append(javaClass) + } + + // Find all of the nested classes for each class, adding them to the list + // of classes to be translated if they were already specified. + var allClassesToVisit = javaClasses + var currentClassIndex: Int = 0 + while currentClassIndex < allClassesToVisit.count { + defer { + currentClassIndex += 1 + } + + // The current class we're in. + let currentClass = allClassesToVisit[currentClassIndex] + guard let currentSwiftName = translator.translatedClasses[currentClass.getName()]?.swiftType else { + continue + } + + // Find all of the nested classes that weren't explicitly translated + // already. + let nestedClasses: [JavaClass] = currentClass.getClasses().compactMap { nestedClass in + guard let nestedClass else { return nil } - // Replace any $'s within the Java class name (which separate nested - // classes) with .'s (which represent nesting in Swift). - let translatedSwiftName = swiftName.replacing("$", with: ".") - - // Note that we will be translating this Java class, so it is a known class. - translator.translatedClasses[javaClassName] = (translatedSwiftName, nil, true) - - var classes: [JavaClass?] = javaClass.getClasses() - - // Go through all subclasses to find all of the classes to translate - while let internalClass = classes.popLast() { - if let internalClass { - let (javaName, swiftName) = names(from: internalClass.getName()) - // If we have already been through this class, don't go through it again - guard translator.translatedClasses[javaName] == nil else { continue } - let currentClassName = swiftName - let currentSanitizedClassName = currentClassName.replacing("$", with: ".") - classes.append(contentsOf: internalClass.getClasses()) - translator.translatedClasses[javaName] = (currentSanitizedClassName, nil, true) + // If this is a local class, we're done. + let javaClassName = nestedClass.getName() + if javaClassName.isLocalJavaClass { + return nil } + + // If this class has been explicitly mentioned, we're done. + if translator.translatedClasses[javaClassName] != nil { + return nil + } + + // Record this as a translated class. + let swiftUnqualifiedName = javaClassName.javaClassNameToCanonicalName + .defaultSwiftNameForJavaClass + + + let swiftName = "\(currentSwiftName).\(swiftUnqualifiedName)" + translator.translatedClasses[javaClassName] = (swiftName, nil, true) + return nestedClass + } + + // If there were no new nested classes, there's nothing to do. + if nestedClasses.isEmpty { + continue } + + // Record all of the nested classes that we will visit. + translator.nestedClasses[currentClass.getName()] = nestedClasses + allClassesToVisit.append(contentsOf: nestedClasses) } // Translate all of the Java classes into Swift classes. @@ -285,7 +306,7 @@ struct JavaToSwift: ParsableCommand { javaClassName = javaClassNameOpt } - return (javaClassName, swiftName) + return (javaClassName, swiftName.javaClassNameToCanonicalName) } mutating func writeContents(_ contents: String, to filename: String, description: String) throws { @@ -326,12 +347,9 @@ struct JavaToSwift: ParsableCommand { continue } - // If any of the segments of the Java name start with a number, it's a - // local class that cannot be mapped into Swift. - for segment in entry.getName().split(separator: "$") { - if let firstChar = segment.first, firstChar.isNumber { - continue - } + // If this is a local class, it cannot be mapped into Swift. + if entry.getName().isLocalJavaClass { + continue } let javaCanonicalName = String(entry.getName().replacing("/", with: ".") @@ -374,10 +392,10 @@ extension String { fileprivate var defaultSwiftNameForJavaClass: String { if let dotLoc = lastIndex(of: ".") { let afterDot = index(after: dotLoc) - return String(self[afterDot...]) + return String(self[afterDot...]).javaClassNameToCanonicalName.adjustedSwiftTypeName } - return self + return javaClassNameToCanonicalName.adjustedSwiftTypeName } } @@ -391,3 +409,30 @@ extension JavaClass { @JavaStaticMethod public func getSystemClassLoader() -> ClassLoader? } + +extension String { + /// Replace all of the $'s for nested names with "." to turn a Java class + /// name into a Java canonical class name, + fileprivate var javaClassNameToCanonicalName: String { + return replacing("$", with: ".") + } + + /// Whether this is the name of an anonymous class. + fileprivate var isLocalJavaClass: Bool { + for segment in split(separator: "$") { + if let firstChar = segment.first, firstChar.isNumber { + return true + } + } + + return false + } + + /// Adjust type name for "bad" type names that don't work well in Swift. + fileprivate var adjustedSwiftTypeName: String { + switch self { + case "Type": return "JavaType" + default: return self + } + } +} diff --git a/Sources/Java2SwiftLib/JavaTranslator.swift b/Sources/Java2SwiftLib/JavaTranslator.swift index e5890e2a..9a9a5b4d 100644 --- a/Sources/Java2SwiftLib/JavaTranslator.swift +++ b/Sources/Java2SwiftLib/JavaTranslator.swift @@ -43,6 +43,12 @@ package class JavaTranslator { /// methods will be implemented in Swift. package var swiftNativeImplementations: Set = [] + /// The set of nested classes that we should traverse from the given class, + /// indexed by the name of the class. + /// + /// TODO: Make JavaClass Hashable so we can index by the object? + package var nestedClasses: [String: [JavaClass]] = [:] + package init( swiftModuleName: String, environment: JNIEnvironment, @@ -79,7 +85,6 @@ extension JavaTranslator { /// itself. This should only be used to refer to types that are built-in to /// JavaKit and therefore aren't captured in any configuration file. package static let defaultTranslatedClasses: [String: (swiftType: String, swiftModule: String?, isOptional: Bool)] = [ - "java.lang.Class": ("JavaClass", "JavaKit", true), "java.lang.String": ("String", "JavaKit", false), ] } @@ -165,19 +170,28 @@ extension JavaTranslator { let javaType = try JavaType(javaTypeName: javaClass.getName()) let isSwiftOptional = javaType.isSwiftOptional return ( - try javaType.swiftTypeName(resolver: self.getSwiftTypeNameFromJavaClassName(_:)), + try javaType.swiftTypeName { javaClassName in + try self.getSwiftTypeNameFromJavaClassName(javaClassName) + }, isSwiftOptional ) } /// Map a Java class name to its corresponding Swift type. - private func getSwiftTypeNameFromJavaClassName(_ name: String) throws -> String { + private func getSwiftTypeNameFromJavaClassName( + _ name: String, + escapeMemberNames: Bool = true + ) throws -> String { if let translated = translatedClasses[name] { // Note that we need to import this Swift module. if let swiftModule = translated.swiftModule, swiftModule != swiftModuleName { importedSwiftModules.insert(swiftModule) } + if escapeMemberNames { + return translated.swiftType.escapingSwiftMemberNames + } + return translated.swiftType } @@ -192,7 +206,7 @@ extension JavaTranslator { /// JavaClass to house static methods. package func translateClass(_ javaClass: JavaClass) throws -> [DeclSyntax] { let fullName = javaClass.getName() - let swiftTypeName = try getSwiftTypeNameFromJavaClassName(fullName) + let swiftTypeName = try getSwiftTypeNameFromJavaClassName(fullName, escapeMemberNames: false) let (swiftParentType, swiftInnermostTypeName) = swiftTypeName.splitSwiftTypeName() // If the swift parent type has not been translated, don't try to translate this one @@ -391,14 +405,12 @@ extension JavaTranslator { topLevelDecls.append(classDecl) - let subClassDecls = javaClass.getClasses().compactMap { - $0.flatMap { clazz in - do { - return try translateClass(clazz) - } catch { - logUntranslated("Unable to translate '\(fullName)' subclass '\(clazz.getName())': \(error)") - return nil - } + let subClassDecls = (nestedClasses[fullName] ?? []).compactMap { clazz in + do { + return try translateClass(clazz) + } catch { + logUntranslated("Unable to translate '\(fullName)' subclass '\(clazz.getName())': \(error)") + return nil } }.flatMap(\.self) @@ -633,3 +645,30 @@ extension JavaTranslator { } } } + +extension String { + /// Escape Swift types that involve member name references like '.Type' + fileprivate var escapingSwiftMemberNames: String { + var count = 0 + return split(separator: ".").map { component in + defer { + count += 1 + } + + if count > 0 && component.memberRequiresBackticks { + return "`\(component)`" + } + + return String(component) + }.joined(separator: ".") + } +} + +extension Substring { + fileprivate var memberRequiresBackticks: Bool { + switch self { + case "Type": return true + default: return false + } + } +} diff --git a/Sources/JavaKit/Optional+JavaObject.swift b/Sources/JavaKit/Optional+JavaObject.swift index 55ad366e..b2f115db 100644 --- a/Sources/JavaKit/Optional+JavaObject.swift +++ b/Sources/JavaKit/Optional+JavaObject.swift @@ -37,7 +37,7 @@ extension Optional: JavaValue where Wrapped: AnyJavaObject { } public static var javaType: JavaType { - JavaType(canonicalClassName: Wrapped.fullJavaClassName) + JavaType(className: Wrapped.fullJavaClassName) } public static func jniMethodCall( diff --git a/Sources/JavaKitJar/generated/Attributes.swift b/Sources/JavaKitJar/generated/Attributes.swift index 74a1352c..8e3d0ca9 100644 --- a/Sources/JavaKitJar/generated/Attributes.swift +++ b/Sources/JavaKitJar/generated/Attributes.swift @@ -133,57 +133,57 @@ extension Attributes { } } extension JavaClass { - @JavaStaticField + @JavaStaticField(isFinal: true) public var MANIFEST_VERSION: Attributes.Name? - @JavaStaticField + @JavaStaticField(isFinal: true) public var SIGNATURE_VERSION: Attributes.Name? - @JavaStaticField + @JavaStaticField(isFinal: true) public var CONTENT_TYPE: Attributes.Name? - @JavaStaticField + @JavaStaticField(isFinal: true) public var CLASS_PATH: Attributes.Name? - @JavaStaticField + @JavaStaticField(isFinal: true) public var MAIN_CLASS: Attributes.Name? - @JavaStaticField + @JavaStaticField(isFinal: true) public var SEALED: Attributes.Name? - @JavaStaticField + @JavaStaticField(isFinal: true) public var EXTENSION_LIST: Attributes.Name? - @JavaStaticField + @JavaStaticField(isFinal: true) public var EXTENSION_NAME: Attributes.Name? - @JavaStaticField + @JavaStaticField(isFinal: true) public var EXTENSION_INSTALLATION: Attributes.Name? - @JavaStaticField + @JavaStaticField(isFinal: true) public var IMPLEMENTATION_TITLE: Attributes.Name? - @JavaStaticField + @JavaStaticField(isFinal: true) public var IMPLEMENTATION_VERSION: Attributes.Name? - @JavaStaticField + @JavaStaticField(isFinal: true) public var IMPLEMENTATION_VENDOR: Attributes.Name? - @JavaStaticField + @JavaStaticField(isFinal: true) public var IMPLEMENTATION_VENDOR_ID: Attributes.Name? - @JavaStaticField + @JavaStaticField(isFinal: true) public var IMPLEMENTATION_URL: Attributes.Name? - @JavaStaticField + @JavaStaticField(isFinal: true) public var SPECIFICATION_TITLE: Attributes.Name? - @JavaStaticField + @JavaStaticField(isFinal: true) public var SPECIFICATION_VERSION: Attributes.Name? - @JavaStaticField + @JavaStaticField(isFinal: true) public var SPECIFICATION_VENDOR: Attributes.Name? - @JavaStaticField + @JavaStaticField(isFinal: true) public var MULTI_RELEASE: Attributes.Name? } diff --git a/Sources/JavaTypes/JavaType+JavaSource.swift b/Sources/JavaTypes/JavaType+JavaSource.swift index e1fa8129..ccb4e96b 100644 --- a/Sources/JavaTypes/JavaType+JavaSource.swift +++ b/Sources/JavaTypes/JavaType+JavaSource.swift @@ -32,7 +32,7 @@ extension JavaType { self = try JavaType(mangledName: name) case let className: - self = JavaType(canonicalClassName: className) + self = JavaType(className: className) } } } diff --git a/Sources/JavaTypes/JavaType.swift b/Sources/JavaTypes/JavaType.swift index 80364b5b..6c5f5357 100644 --- a/Sources/JavaTypes/JavaType.swift +++ b/Sources/JavaTypes/JavaType.swift @@ -31,9 +31,9 @@ public enum JavaType: Equatable, Hashable { /// A Java array. indirect case array(JavaType) - /// Given a canonical class name such as "java.lang.Object", split it into + /// Given a class name such as "java.lang.Object", split it into /// its package and class name to form a class instance. - public init(canonicalClassName name: some StringProtocol) { + public init(className name: some StringProtocol) { if let lastDot = name.lastIndex(of: ".") { self = .class( package: String(name[...self, + swiftTypeName: "MyJavaClass", + translatedClasses: [ + "java.lang.Object": ("JavaObject", nil, true), + "java.lang.String": ("JavaString", nil, true), + ], + expectedChunks: [ + "import JavaKit", + """ + @JavaClass("java.lang.Class") + public struct MyJavaClass { + """, + """ + @JavaStaticMethod + public func forName(_ arg0: JavaString) throws -> MyJavaClass? where ObjectType == MyJavaClass + """, + ] + ) + } + func testEnum() throws { try assertTranslatedClass( JavaMonth.self, @@ -124,6 +153,10 @@ class Java2SwiftTests: XCTestCase { "java.lang.ProcessBuilder$Redirect": ("ProcessBuilder.Redirect", nil, true), "java.lang.ProcessBuilder$Redirect$Type": ("ProcessBuilder.Redirect.Type", nil, true), ], + nestedClasses: [ + "java.lang.ProcessBuilder": [JavaClass().as(JavaClass.self)!], + "java.lang.ProcessBuilder$Redirect": [JavaClass().as(JavaClass.self)!], + ], expectedChunks: [ "import JavaKit", """ @@ -136,14 +169,60 @@ class Java2SwiftTests: XCTestCase { public struct Redirect { """, """ + public func redirectError() -> ProcessBuilder.Redirect? + """, + """ extension ProcessBuilder.Redirect { @JavaClass("java.lang.ProcessBuilder$Redirect$Type") public struct Type { + """, """ + @JavaMethod + public func type() -> ProcessBuilder.Redirect.`Type`? + """, ] ) } + func testNestedRenamedSubclasses() throws { + try assertTranslatedClass( + ProcessBuilder.self, + swiftTypeName: "ProcessBuilder", + translatedClasses: [ + "java.lang.ProcessBuilder": ("ProcessBuilder", nil, true), + "java.lang.ProcessBuilder$Redirect": ("ProcessBuilder.PBRedirect", nil, true), + "java.lang.ProcessBuilder$Redirect$Type": ("ProcessBuilder.PBRedirect.JavaType", nil, true), + ], + nestedClasses: [ + "java.lang.ProcessBuilder": [JavaClass().as(JavaClass.self)!], + "java.lang.ProcessBuilder$Redirect": [JavaClass().as(JavaClass.self)!], + ], + expectedChunks: [ + "import JavaKit", + """ + @JavaMethod + public func redirectInput() -> ProcessBuilder.PBRedirect? + """, + """ + extension ProcessBuilder { + @JavaClass("java.lang.ProcessBuilder$Redirect") + public struct PBRedirect { + """, + """ + public func redirectError() -> ProcessBuilder.PBRedirect? + """, + """ + extension ProcessBuilder.PBRedirect { + @JavaClass("java.lang.ProcessBuilder$Redirect$Type") + public struct JavaType { + """, + """ + @JavaMethod + public func type() -> ProcessBuilder.PBRedirect.JavaType? + """ + ] + ) + } } @JavaClass("java.util.ArrayList") @@ -162,6 +241,7 @@ func assertTranslatedClass( translatedClasses: [ String: (swiftType: String, swiftModule: String?, isOptional: Bool) ] = JavaTranslator.defaultTranslatedClasses, + nestedClasses: [String: [JavaClass]] = [:], expectedChunks: [String], file: StaticString = #filePath, line: UInt = #line @@ -174,8 +254,7 @@ func assertTranslatedClass( translator.translatedClasses = translatedClasses translator.translatedClasses[javaType.fullJavaClassName] = (swiftTypeName, nil, true) - - + translator.nestedClasses = nestedClasses translator.startNewFile() let translatedDecls = try translator.translateClass( JavaClass(