Skip to content

Split the Java2Swift tool out into a library target and add a test harness #73

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ let package = Package(

.executable(
name: "Java2Swift",
targets: ["Java2Swift"]
targets: ["Java2SwiftTool"]
),

// ==== jextract-swift (extract Java accessors from Swift interface files)
Expand Down Expand Up @@ -230,13 +230,12 @@ let package = Package(
]
),

.executableTarget(
name: "Java2Swift",
.target(
name: "Java2SwiftLib",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about the “lib” suffix tbh, just java2swift is fine

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd have to rename the executable target to drop the Lib suffix

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",
Expand All @@ -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: [
Expand Down Expand Up @@ -301,7 +320,15 @@ let package = Package(
.swiftLanguageMode(.v5)
]
),


.testTarget(
name: "Java2SwiftTests",
dependencies: ["Java2SwiftLib"],
swiftSettings: [
.swiftLanguageMode(.v5)
]
),

.testTarget(
name: "JExtractSwiftTests",
dependencies: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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<String> = JavaTranslator.defaultImportedSwiftModules
package var importedSwiftModules: Set<String> = 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
Expand All @@ -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
}

Expand All @@ -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),
]
Expand All @@ -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 {
Expand Down Expand Up @@ -166,7 +166,7 @@ extension JavaTranslator {
}

/// Translate a Java class into its corresponding Swift type name.
func getSwiftTypeName(_ javaClass: JavaClass<JavaObject>) throws -> (swiftName: String, isOptional: Bool) {
package func getSwiftTypeName(_ javaClass: JavaClass<JavaObject>) throws -> (swiftName: String, isOptional: Bool) {
let javaType = try JavaType(javaTypeName: javaClass.getName())
let isSwiftOptional = javaType.isSwiftOptional
return (
Expand Down Expand Up @@ -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<JavaObject>) -> [DeclSyntax] {
package func translateClass(_ javaClass: JavaClass<JavaObject>) -> [DeclSyntax] {
let fullName = javaClass.getCanonicalName()
let swiftTypeName = try! getSwiftTypeNameFromJavaClassName(fullName)

Expand Down Expand Up @@ -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<some AnyJavaObject>) throws -> DeclSyntax {
package func translateConstructor(_ javaConstructor: Constructor<some AnyJavaObject>) throws -> DeclSyntax {
let parameters = try translateParameters(javaConstructor.getParameters()) + ["environment: JNIEnvironment"]
let parametersStr = parameters.map { $0.description }.joined(separator: ", ")
let throwsStr = javaConstructor.throwsCheckedException ? "throws" : ""
Expand All @@ -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 = ""
Expand Down Expand Up @@ -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 """
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import ArgumentParser
import Foundation
import Java2SwiftLib
import JavaKit
import JavaKitJar
import JavaKitNetwork
Expand Down
93 changes: 93 additions & 0 deletions Tests/Java2SwiftTests/Java2SwiftTests.swift
Original file line number Diff line number Diff line change
@@ -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<JavaClassType: AnyJavaObject>(
_ 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<JavaObject>(
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)
}
}
Loading