diff --git a/Package.swift b/Package.swift index a89997f0..2bd95713 100644 --- a/Package.swift +++ b/Package.swift @@ -83,7 +83,7 @@ let package = Package( .executable( name: "Java2Swift", - targets: ["Java2Swift"] + targets: ["Java2SwiftTool"] ), // ==== jextract-swift (extract Java accessors from Swift interface files) @@ -230,13 +230,12 @@ let package = Package( ] ), - .executableTarget( - name: "Java2Swift", + .target( + name: "Java2SwiftLib", dependencies: [ .product(name: "SwiftBasicFormat", package: "swift-syntax"), .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), - .product(name: "ArgumentParser", package: "swift-argument-parser"), "JavaKit", "JavaKitJar", "JavaKitReflection", @@ -250,6 +249,26 @@ let package = Package( ] ), + .executableTarget( + name: "Java2SwiftTool", + dependencies: [ + .product(name: "SwiftBasicFormat", package: "swift-syntax"), + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + "JavaKit", + "JavaKitJar", + "JavaKitNetwork", + "JavaKitVM", + "Java2SwiftLib", + ], + + swiftSettings: [ + .swiftLanguageMode(.v5), + .enableUpcomingFeature("BareSlashRegexLiterals") + ] + ), + .target( name: "JExtractSwift", dependencies: [ @@ -301,7 +320,15 @@ let package = Package( .swiftLanguageMode(.v5) ] ), - + + .testTarget( + name: "Java2SwiftTests", + dependencies: ["Java2SwiftLib"], + swiftSettings: [ + .swiftLanguageMode(.v5) + ] + ), + .testTarget( name: "JExtractSwiftTests", dependencies: [ diff --git a/Sources/Java2Swift/JavaTranslator+TranslationManifest.swift b/Sources/Java2SwiftLib/JavaTranslator+TranslationManifest.swift similarity index 91% rename from Sources/Java2Swift/JavaTranslator+TranslationManifest.swift rename to Sources/Java2SwiftLib/JavaTranslator+TranslationManifest.swift index 244cefd4..896666d0 100644 --- a/Sources/Java2Swift/JavaTranslator+TranslationManifest.swift +++ b/Sources/Java2SwiftLib/JavaTranslator+TranslationManifest.swift @@ -17,7 +17,7 @@ import Foundation extension JavaTranslator { /// Load the manifest file with the given name to populate the known set of /// translated Java classes. - func loadTranslationManifest(from url: URL) throws { + package func loadTranslationManifest(from url: URL) throws { let contents = try Data(contentsOf: url) let manifest = try JSONDecoder().decode(TranslationManifest.self, from: contents) for (javaClassName, swiftName) in manifest.translatedClasses { @@ -30,7 +30,7 @@ extension JavaTranslator { } /// Emit the translation manifest for this source file - func encodeTranslationManifest() throws -> String { + package func encodeTranslationManifest() throws -> String { let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys] var contents = String(data: try encoder.encode(manifest), encoding: .utf8)! diff --git a/Sources/Java2Swift/JavaTranslator.swift b/Sources/Java2SwiftLib/JavaTranslator.swift similarity index 94% rename from Sources/Java2Swift/JavaTranslator.swift rename to Sources/Java2SwiftLib/JavaTranslator.swift index 78f3cd87..3a911d48 100644 --- a/Sources/Java2Swift/JavaTranslator.swift +++ b/Sources/Java2SwiftLib/JavaTranslator.swift @@ -21,7 +21,7 @@ import SwiftSyntaxBuilder /// Utility that translates Java classes into Swift source code to access /// those Java classes. -class JavaTranslator { +package class JavaTranslator { /// The name of the Swift module that we are translating into. let swiftModuleName: String @@ -36,18 +36,18 @@ class JavaTranslator { /// which is absolutely not scalable. We need a better way to be able to /// discover already-translated Java classes to get their corresponding /// Swift types and modules. - var translatedClasses: [String: (swiftType: String, swiftModule: String?, isOptional: Bool)] = + package var translatedClasses: [String: (swiftType: String, swiftModule: String?, isOptional: Bool)] = defaultTranslatedClasses /// The set of Swift modules that need to be imported to make the generated /// code compile. Use `getImportDecls()` to format this into a list of /// import declarations. - var importedSwiftModules: Set = JavaTranslator.defaultImportedSwiftModules + package var importedSwiftModules: Set = JavaTranslator.defaultImportedSwiftModules /// The manifest for the module being translated. - var manifest: TranslationManifest + package var manifest: TranslationManifest - init( + package init( swiftModuleName: String, environment: JNIEnvironment, format: BasicFormat = JavaTranslator.defaultFormat @@ -59,7 +59,7 @@ class JavaTranslator { } /// Clear out any per-file state when we want to start a new file. - func startNewFile() { + package func startNewFile() { importedSwiftModules = Self.defaultImportedSwiftModules } @@ -83,7 +83,7 @@ extension JavaTranslator { /// The default set of translated classes that do not come from JavaKit /// itself. This should only be used to refer to types that are built-in to /// JavaKit and therefore aren't captured in any manifest. - private static let defaultTranslatedClasses: [String: (swiftType: String, swiftModule: String?, isOptional: Bool)] = [ + package static let defaultTranslatedClasses: [String: (swiftType: String, swiftModule: String?, isOptional: Bool)] = [ "java.lang.Class": ("JavaClass", "JavaKit", true), "java.lang.String": ("String", "JavaKit", false), ] @@ -92,7 +92,7 @@ extension JavaTranslator { // MARK: Import translation extension JavaTranslator { /// Retrieve the import declarations. - func getImportDecls() -> [DeclSyntax] { + package func getImportDecls() -> [DeclSyntax] { importedSwiftModules.filter { $0 != swiftModuleName }.sorted().map { @@ -166,7 +166,7 @@ extension JavaTranslator { } /// Translate a Java class into its corresponding Swift type name. - func getSwiftTypeName(_ javaClass: JavaClass) throws -> (swiftName: String, isOptional: Bool) { + package func getSwiftTypeName(_ javaClass: JavaClass) throws -> (swiftName: String, isOptional: Bool) { let javaType = try JavaType(javaTypeName: javaClass.getName()) let isSwiftOptional = javaType.isSwiftOptional return ( @@ -195,7 +195,7 @@ extension JavaTranslator { /// Translates the given Java class into the corresponding Swift type. This /// can produce multiple declarations, such as a separate extension of /// JavaClass to house static methods. - func translateClass(_ javaClass: JavaClass) -> [DeclSyntax] { + package func translateClass(_ javaClass: JavaClass) -> [DeclSyntax] { let fullName = javaClass.getCanonicalName() let swiftTypeName = try! getSwiftTypeNameFromJavaClassName(fullName) @@ -409,7 +409,7 @@ extension JavaTranslator { // MARK: Method and constructor translation extension JavaTranslator { /// Translates the given Java constructor into a Swift declaration. - func translateConstructor(_ javaConstructor: Constructor) throws -> DeclSyntax { + package func translateConstructor(_ javaConstructor: Constructor) throws -> DeclSyntax { let parameters = try translateParameters(javaConstructor.getParameters()) + ["environment: JNIEnvironment"] let parametersStr = parameters.map { $0.description }.joined(separator: ", ") let throwsStr = javaConstructor.throwsCheckedException ? "throws" : "" @@ -421,7 +421,7 @@ extension JavaTranslator { } /// Translates the given Java method into a Swift declaration. - func translateMethod( + package func translateMethod( _ javaMethod: Method, genericParameterClause: String = "", whereClause: String = "" @@ -449,7 +449,7 @@ extension JavaTranslator { """ } - func translateField(_ javaField: Field) throws -> DeclSyntax { + package func translateField(_ javaField: Field) throws -> DeclSyntax { let typeName = try getSwiftTypeNameAsString(javaField.getGenericType()!, outerOptional: true) let fieldAttribute: AttributeSyntax = javaField.isStatic ? "@JavaStaticField" : "@JavaField"; return """ diff --git a/Sources/Java2Swift/StringExtras.swift b/Sources/Java2SwiftLib/StringExtras.swift similarity index 100% rename from Sources/Java2Swift/StringExtras.swift rename to Sources/Java2SwiftLib/StringExtras.swift diff --git a/Sources/Java2Swift/TranslationError.swift b/Sources/Java2SwiftLib/TranslationError.swift similarity index 100% rename from Sources/Java2Swift/TranslationError.swift rename to Sources/Java2SwiftLib/TranslationError.swift diff --git a/Sources/Java2Swift/TranslationManifest.swift b/Sources/Java2SwiftLib/TranslationManifest.swift similarity index 94% rename from Sources/Java2Swift/TranslationManifest.swift rename to Sources/Java2SwiftLib/TranslationManifest.swift index aa03eae6..564af422 100644 --- a/Sources/Java2Swift/TranslationManifest.swift +++ b/Sources/Java2SwiftLib/TranslationManifest.swift @@ -14,7 +14,7 @@ /// Manifest describing the a Swift module containing translations of /// Java classes into Swift types. -struct TranslationManifest: Codable { +package struct TranslationManifest: Codable { /// The Swift module name. var swiftModule: String diff --git a/Sources/Java2Swift/JavaToSwift.swift b/Sources/Java2SwiftTool/JavaToSwift.swift similarity index 99% rename from Sources/Java2Swift/JavaToSwift.swift rename to Sources/Java2SwiftTool/JavaToSwift.swift index ad9cf4f3..17fef0f7 100644 --- a/Sources/Java2Swift/JavaToSwift.swift +++ b/Sources/Java2SwiftTool/JavaToSwift.swift @@ -14,6 +14,7 @@ import ArgumentParser import Foundation +import Java2SwiftLib import JavaKit import JavaKitJar import JavaKitNetwork diff --git a/Tests/Java2SwiftTests/Java2SwiftTests.swift b/Tests/Java2SwiftTests/Java2SwiftTests.swift new file mode 100644 index 00000000..bac88d47 --- /dev/null +++ b/Tests/Java2SwiftTests/Java2SwiftTests.swift @@ -0,0 +1,93 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JavaKit +import Java2SwiftLib +import JavaKitVM +import XCTest // NOTE: Workaround for https://github.com/swiftlang/swift-java/issues/43 + +/// Handy reference to the JVM abstraction. +var jvm: JavaVirtualMachine { + get throws { + try .shared() + } +} + +class Java2SwiftTests: XCTestCase { + func testJavaLangObjectMapping() async throws { + try assertTranslatedClass( + JavaObject.self, + swiftTypeName: "MyJavaObject", + expectedChunks: [ + "import JavaKit", + """ + @JavaClass("java.lang.Object") + public struct MyJavaObject { + """, + """ + @JavaMethod + public func toString() -> String + """, + """ + @JavaMethod + public func wait() throws + """ + ] + ) + } +} + +/// Translate a Java class and assert that the translated output contains +/// each of the expected "chunks" of text. +func assertTranslatedClass( + _ javaType: JavaClassType.Type, + swiftTypeName: String, + translatedClasses: [ + String: (swiftType: String, swiftModule: String?, isOptional: Bool) + ] = JavaTranslator.defaultTranslatedClasses, + expectedChunks: [String], + file: StaticString = #filePath, + line: UInt = #line +) throws { + let environment = try jvm.environment() + let translator = JavaTranslator( + swiftModuleName: "SwiftModule", + environment: environment + ) + + translator.translatedClasses = translatedClasses + translator.translatedClasses[javaType.fullJavaClassName] = (swiftTypeName, nil, true) + + translator.startNewFile() + let translatedDecls = translator.translateClass( + try JavaClass( + javaThis: javaType.getJNIClass(in: environment), + environment: environment) + ) + let importDecls = translator.getImportDecls() + + let swiftFileText = """ + // Auto-generated by Java-to-Swift wrapper generator. + \(importDecls.map { $0.description }.joined()) + \(translatedDecls.map { $0.description }.joined(separator: "\n")) + """ + + for expectedChunk in expectedChunks { + if swiftFileText.contains(expectedChunk) { + continue + } + + XCTFail("Expected chunk '\(expectedChunk)' not found in '\(swiftFileText)'", file: file, line: line) + } +}