From 03f923ab1759da52389f47ca392d02d49d021cc0 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Fri, 25 Oct 2024 21:54:46 -0700 Subject: [PATCH 01/10] Java2Swift: Add a test for a generic static method --- Tests/Java2SwiftTests/Java2SwiftTests.swift | 22 +++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Tests/Java2SwiftTests/Java2SwiftTests.swift b/Tests/Java2SwiftTests/Java2SwiftTests.swift index 5a4e8e33..c2998760 100644 --- a/Tests/Java2SwiftTests/Java2SwiftTests.swift +++ b/Tests/Java2SwiftTests/Java2SwiftTests.swift @@ -51,6 +51,28 @@ class Java2SwiftTests: XCTestCase { ) } + func testJavaLangClassMapping() throws { + try assertTranslatedClass( + JavaClass.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, From bd0c20d3638efc19f70c3c62b4f26141fb35fc14 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Fri, 25 Oct 2024 22:02:06 -0700 Subject: [PATCH 02/10] Drop the "canonicalClassName" from JavaTypes because it is now incorrect We're using the "normal" name now that uses $ for nested classes. --- Sources/JavaKit/Optional+JavaObject.swift | 2 +- Sources/JavaTypes/JavaType+JavaSource.swift | 2 +- Sources/JavaTypes/JavaType.swift | 4 ++-- Sources/JavaTypes/Mangling.swift | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) 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/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[.. Date: Fri, 25 Oct 2024 22:18:13 -0700 Subject: [PATCH 03/10] Remove dead code behind a `#if false` --- Sources/Java2Swift/JavaToSwift.swift | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Sources/Java2Swift/JavaToSwift.swift b/Sources/Java2Swift/JavaToSwift.swift index 86d3cf8d..9728bf45 100644 --- a/Sources/Java2Swift/JavaToSwift.swift +++ b/Sources/Java2Swift/JavaToSwift.swift @@ -202,17 +202,8 @@ struct JavaToSwift: ParsableCommand { 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 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 { From d62a012a66099e25060a996dc67036a8cb0f5e84 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Fri, 25 Oct 2024 23:06:28 -0700 Subject: [PATCH 04/10] Teach the Java2Swift plugin to match the nested type naming scheme for Java2Swift --- Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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. From 70cd2def7736994d452342106ed802c1e2d5c4c7 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sat, 26 Oct 2024 07:53:43 -0700 Subject: [PATCH 05/10] Regenerate java.util.jar.Attributes with the "isFinal" change --- Sources/JavaKitJar/generated/Attributes.swift | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) 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? } From 5ef5527e41435a652128938c3d7e3b46ab461ae7 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sat, 26 Oct 2024 08:41:55 -0700 Subject: [PATCH 06/10] 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. --- Sources/Java2Swift/JavaToSwift.swift | 95 ++++++++++++++------- Sources/Java2SwiftLib/JavaTranslator.swift | 21 +++-- Tests/Java2SwiftTests/Java2SwiftTests.swift | 18 +++- 3 files changed, 92 insertions(+), 42 deletions(-) diff --git a/Sources/Java2Swift/JavaToSwift.swift b/Sources/Java2Swift/JavaToSwift.swift index 9728bf45..60a45185 100644 --- a/Sources/Java2Swift/JavaToSwift.swift +++ b/Sources/Java2Swift/JavaToSwift.swift @@ -201,7 +201,7 @@ struct JavaToSwift: ParsableCommand { // Add the configuration for this module. translator.addConfiguration(config, forSwiftModule: moduleName) - // Load all of the requested classes. + // Load all of the explicitly-requested classes. let classLoader = try JavaClass(environment: environment) .getSystemClassLoader()! var javaClasses: [JavaClass] = [] @@ -211,29 +211,50 @@ 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 + } + + // Find all of the nested classes that weren't explicitly translated + // already. + let currentClass = allClassesToVisit[currentClassIndex] + let nestedClasses: [JavaClass] = currentClass.getClasses().compactMap { nestedClass in + guard let nestedClass else { return nil } + + // If this is a local class, we're done. + let javaClassName = nestedClass.getName() + if javaClassName.isLocalJavaClass { + 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 class has been explicitly mentioned, we're done. + if translator.translatedClasses[javaClassName] != nil { + return nil } + + // Record this as a translated class. + let swiftName = javaClassName.defaultSwiftNameForJavaClass + 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. @@ -276,7 +297,7 @@ struct JavaToSwift: ParsableCommand { javaClassName = javaClassNameOpt } - return (javaClassName, swiftName) + return (javaClassName, swiftName.javaClassNameToCanonicalName) } mutating func writeContents(_ contents: String, to filename: String, description: String) throws { @@ -317,12 +338,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: ".") @@ -365,10 +383,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 } - return self + return javaClassNameToCanonicalName } } @@ -382,3 +400,22 @@ 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 + } +} diff --git a/Sources/Java2SwiftLib/JavaTranslator.swift b/Sources/Java2SwiftLib/JavaTranslator.swift index e5890e2a..5535e27c 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), ] } @@ -391,14 +396,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) diff --git a/Tests/Java2SwiftTests/Java2SwiftTests.swift b/Tests/Java2SwiftTests/Java2SwiftTests.swift index c2998760..3bcab0ac 100644 --- a/Tests/Java2SwiftTests/Java2SwiftTests.swift +++ b/Tests/Java2SwiftTests/Java2SwiftTests.swift @@ -25,8 +25,15 @@ var jvm: JavaVirtualMachine { @JavaClass("java.time.Month") public struct JavaMonth {} + @JavaClass("java.lang.ProcessBuilder") -struct ProcessBuilder {} +struct ProcessBuilder { + @JavaClass("java.lang.ProcessBuilder$Redirect") + struct Redirect { + @JavaClass("java.lang.ProcessBuilder$Redirect$Type") + struct JavaType { } + } +} class Java2SwiftTests: XCTestCase { func testJavaLangObjectMapping() throws { @@ -146,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", """ @@ -165,7 +176,6 @@ class Java2SwiftTests: XCTestCase { ] ) } - } @JavaClass("java.util.ArrayList") @@ -184,6 +194,7 @@ func assertTranslatedClass( translatedClasses: [ String: (swiftType: String, swiftModule: String?, isOptional: Bool) ] = JavaTranslator.defaultTranslatedClasses, + nestedClasses: [String: [JavaClass]] = [:], expectedChunks: [String], file: StaticString = #filePath, line: UInt = #line @@ -196,8 +207,7 @@ func assertTranslatedClass( translator.translatedClasses = translatedClasses translator.translatedClasses[javaType.fullJavaClassName] = (swiftTypeName, nil, true) - - + translator.nestedClasses = nestedClasses translator.startNewFile() let translatedDecls = try translator.translateClass( JavaClass( From b304f73025f8ae9a1b8f6f03bf5a26b3feca2364 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sat, 26 Oct 2024 10:42:48 -0700 Subject: [PATCH 07/10] Update JavaSieve example now that we're handling nested classes --- Samples/JavaSieve/Sources/JavaSieve/Java2Swift.config | 1 + 1 file changed, 1 insertion(+) 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", From b402bf51a67c147b32912e7ec4699338ffb21b13 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sat, 26 Oct 2024 17:43:40 -0700 Subject: [PATCH 08/10] Java2Swift: Use the parent's Swift type name to form nested type names --- Sources/Java2Swift/JavaToSwift.swift | 11 +++++- Tests/Java2SwiftTests/Java2SwiftTests.swift | 43 +++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/Sources/Java2Swift/JavaToSwift.swift b/Sources/Java2Swift/JavaToSwift.swift index 60a45185..7b37827b 100644 --- a/Sources/Java2Swift/JavaToSwift.swift +++ b/Sources/Java2Swift/JavaToSwift.swift @@ -224,9 +224,14 @@ struct JavaToSwift: ParsableCommand { 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 currentClass = allClassesToVisit[currentClassIndex] let nestedClasses: [JavaClass] = currentClass.getClasses().compactMap { nestedClass in guard let nestedClass else { return nil } @@ -242,7 +247,9 @@ struct JavaToSwift: ParsableCommand { } // Record this as a translated class. - let swiftName = javaClassName.defaultSwiftNameForJavaClass + let swiftUnqualifiedName = javaClassName.javaClassNameToCanonicalName + .defaultSwiftNameForJavaClass + let swiftName = "\(currentSwiftName).\(swiftUnqualifiedName)" translator.translatedClasses[javaClassName] = (swiftName, nil, true) return nestedClass } diff --git a/Tests/Java2SwiftTests/Java2SwiftTests.swift b/Tests/Java2SwiftTests/Java2SwiftTests.swift index 3bcab0ac..ef75cea4 100644 --- a/Tests/Java2SwiftTests/Java2SwiftTests.swift +++ b/Tests/Java2SwiftTests/Java2SwiftTests.swift @@ -169,9 +169,52 @@ class Java2SwiftTests: XCTestCase { public struct Redirect { """, """ + public func redirectError() -> ProcessBuilder.Redirect? + """, + """ extension ProcessBuilder.Redirect { @JavaClass("java.lang.ProcessBuilder$Redirect$Type") public struct 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? """ ] ) From 60457bf61574fafbd9d3031686eb4c8372be2b9b Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sat, 26 Oct 2024 18:00:47 -0700 Subject: [PATCH 09/10] Escape member types named "Type" --- Sources/Java2SwiftLib/JavaTranslator.swift | 42 +++++++++++++++++++-- Tests/Java2SwiftTests/Java2SwiftTests.swift | 4 ++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/Sources/Java2SwiftLib/JavaTranslator.swift b/Sources/Java2SwiftLib/JavaTranslator.swift index 5535e27c..9a9a5b4d 100644 --- a/Sources/Java2SwiftLib/JavaTranslator.swift +++ b/Sources/Java2SwiftLib/JavaTranslator.swift @@ -170,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 } @@ -197,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 @@ -636,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/Tests/Java2SwiftTests/Java2SwiftTests.swift b/Tests/Java2SwiftTests/Java2SwiftTests.swift index ef75cea4..e1224746 100644 --- a/Tests/Java2SwiftTests/Java2SwiftTests.swift +++ b/Tests/Java2SwiftTests/Java2SwiftTests.swift @@ -176,6 +176,10 @@ class Java2SwiftTests: XCTestCase { @JavaClass("java.lang.ProcessBuilder$Redirect$Type") public struct Type { """, + """ + @JavaMethod + public func type() -> ProcessBuilder.Redirect.`Type`? + """, ] ) } From 968a5808676f98bd1082a039745646f1dbf92011 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sat, 26 Oct 2024 18:06:44 -0700 Subject: [PATCH 10/10] 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. --- Sources/Java2Swift/JavaToSwift.swift | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Sources/Java2Swift/JavaToSwift.swift b/Sources/Java2Swift/JavaToSwift.swift index 7b37827b..205c6a43 100644 --- a/Sources/Java2Swift/JavaToSwift.swift +++ b/Sources/Java2Swift/JavaToSwift.swift @@ -249,6 +249,8 @@ struct JavaToSwift: ParsableCommand { // Record this as a translated class. let swiftUnqualifiedName = javaClassName.javaClassNameToCanonicalName .defaultSwiftNameForJavaClass + + let swiftName = "\(currentSwiftName).\(swiftUnqualifiedName)" translator.translatedClasses[javaClassName] = (swiftName, nil, true) return nestedClass @@ -390,10 +392,10 @@ extension String { fileprivate var defaultSwiftNameForJavaClass: String { if let dotLoc = lastIndex(of: ".") { let afterDot = index(after: dotLoc) - return String(self[afterDot...]).javaClassNameToCanonicalName + return String(self[afterDot...]).javaClassNameToCanonicalName.adjustedSwiftTypeName } - return javaClassNameToCanonicalName + return javaClassNameToCanonicalName.adjustedSwiftTypeName } } @@ -425,4 +427,12 @@ extension String { 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 + } + } }