Skip to content

Commit 2abc1d9

Browse files
committed
wrap-java: prune not used generic parameters from methods
1 parent 45a9aea commit 2abc1d9

File tree

4 files changed

+322
-140
lines changed

4 files changed

+322
-140
lines changed

Sources/SwiftJavaToolLib/JavaClassTranslator.swift

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,16 @@ struct JavaClassTranslator {
9999
}
100100

101101
/// The generic parameter clause for the Swift version of the Java class.
102-
var genericParameterClause: String {
102+
var genericParameters: [String] {
103103
if javaTypeParameters.isEmpty {
104-
return ""
104+
return []
105105
}
106106

107107
let genericParameters = javaTypeParameters.map { param in
108108
"\(param.getName()): AnyJavaObject"
109109
}
110110

111-
return "<\(genericParameters.joined(separator: ", "))>"
111+
return genericParameters
112112
}
113113

114114
/// Prepare translation for the given Java class (or interface).
@@ -387,6 +387,13 @@ extension JavaClassTranslator {
387387
interfacesStr = ", \(prefix): \(swiftInterfaces.map { "\($0).self" }.joined(separator: ", "))"
388388
}
389389

390+
let genericParameterClause =
391+
if genericParameters.isEmpty {
392+
""
393+
} else {
394+
"<\(genericParameters.joined(separator: ", "))>"
395+
}
396+
390397
// Emit the struct declaration describing the java class.
391398
let classOrInterface: String = isInterface ? "JavaInterface" : "JavaClass";
392399
let introducer = translateAsClass ? "open class" : "public struct"
@@ -439,7 +446,7 @@ extension JavaClassTranslator {
439446
}
440447

441448
let genericArgumentClause = "<\(genericParameterNames.joined(separator: ", "))>"
442-
staticMemberWhereClause = " where ObjectType == \(swiftTypeName)\(genericArgumentClause)"
449+
staticMemberWhereClause = " where ObjectType == \(swiftTypeName)\(genericArgumentClause)" // FIXME: move the 'where ...' part into the render bit
443450
} else {
444451
staticMemberWhereClause = ""
445452
}
@@ -461,7 +468,7 @@ extension JavaClassTranslator {
461468
do {
462469
return try renderMethod(
463470
method, implementedInSwift: /*FIXME:*/false,
464-
genericParameterClause: genericParameterClause,
471+
genericParameters: genericParameters,
465472
whereClause: staticMemberWhereClause
466473
)
467474
} catch {
@@ -478,7 +485,7 @@ extension JavaClassTranslator {
478485

479486
// Specify the specialization arguments when needed.
480487
let extSpecialization: String
481-
if genericParameterClause.isEmpty {
488+
if genericParameters.isEmpty {
482489
extSpecialization = "<\(swiftTypeName)>"
483490
} else {
484491
extSpecialization = ""
@@ -574,24 +581,51 @@ extension JavaClassTranslator {
574581
"""
575582
}
576583

584+
func genericParameterIsUsedInSignature(_ typeParam: TypeVariable<Method>, in method: Method) -> Bool {
585+
// --- Return type
586+
// Is the return type exactly the type param
587+
// FIXME: make this equals based?
588+
if method.getGenericReturnType().getTypeName() == typeParam.getTypeName() {
589+
return true
590+
}
591+
592+
if let parameterizedReturnType = method.getGenericReturnType().as(ParameterizedType.self) {
593+
for actualTypeParam in parameterizedReturnType.getActualTypeArguments() {
594+
guard let actualTypeParam else { continue }
595+
if actualTypeParam.isEqualTo(typeParam.as(Type.self)) {
596+
return true
597+
}
598+
}
599+
}
600+
601+
return false
602+
}
603+
577604
/// Translates the given Java method into a Swift declaration.
578605
package func renderMethod(
579606
_ javaMethod: Method,
580607
implementedInSwift: Bool,
581-
genericParameterClause __genericParameterClause: String = "", // FIXME: why is this a string, fix this...
608+
genericParameters: [String] = [],
582609
whereClause: String = ""
583610
) throws -> DeclSyntax {
584611
// Map the generic params on the method.
612+
var allGenericParameters = genericParameters
585613
let typeParameters = javaMethod.getTypeParameters()
586-
var genericParameterClauseStr = __genericParameterClause
587-
if typeParameters.count(where: {$0 != nil }) > 0 {
588-
genericParameterClauseStr = "<"
589-
genericParameterClauseStr += typeParameters.map { typeParam in
590-
// FIXME: determine if it is some other constraint
591-
"\(typeParam!.getTypeName()): AnyJavaObject"
592-
}.joined(separator: ", ")
593-
genericParameterClauseStr += ">"
614+
if typeParameters.contains(where: {$0 != nil }) {
615+
allGenericParameters += typeParameters.compactMap { typeParam in
616+
guard let typeParam else { return nil }
617+
guard genericParameterIsUsedInSignature(typeParam, in: javaMethod) else {
618+
return nil
619+
}
620+
return "\(typeParam.getTypeName()): AnyJavaObject"
621+
}
594622
}
623+
let genericParameterClauseStr =
624+
if allGenericParameters.isEmpty {
625+
""
626+
} else {
627+
"<\(allGenericParameters.joined(separator: ", "))>"
628+
}
595629

596630
// Map the parameters.
597631
let parameters = try translateJavaParameters(javaMethod)
@@ -617,6 +651,7 @@ extension JavaClassTranslator {
617651
resultTypeStr = ""
618652
}
619653

654+
// --- Handle other effects
620655
let throwsStr = javaMethod.throwsCheckedException ? "throws" : ""
621656
let swiftMethodName = javaMethod.getName().escapedSwiftName
622657
let methodAttribute: AttributeSyntax = implementedInSwift
@@ -642,11 +677,12 @@ extension JavaClassTranslator {
642677

643678
let resultOptional: String = resultType.optionalWrappedType() ?? resultType
644679
let baseBody: ExprSyntax = "\(raw: javaMethod.throwsCheckedException ? "try " : "")\(raw: swiftMethodName)(\(raw: parameters.map(\.passedArg).joined(separator: ", ")))"
645-
let body: ExprSyntax = if let optionalType = resultType.optionalWrappedType() {
646-
"Optional(javaOptional: \(baseBody))"
647-
} else {
648-
baseBody
649-
}
680+
let body: ExprSyntax =
681+
if resultType.optionalWrappedType() != nil {
682+
"Optional(javaOptional: \(baseBody))"
683+
} else {
684+
baseBody
685+
}
650686

651687

652688
return """
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
@_spi(Testing) import SwiftJava
16+
import SwiftJavaToolLib
17+
import JavaUtilJar
18+
import SwiftJavaShared
19+
import SwiftJavaConfigurationShared
20+
import _Subprocess
21+
import XCTest // NOTE: Workaround for https://github.com/swiftlang/swift-java/issues/43
22+
23+
fileprivate func createTemporaryDirectory(in directory: URL) throws -> URL {
24+
let uuid = UUID().uuidString
25+
let resolverDirectoryURL = directory.appendingPathComponent("swift-java-testing-\(uuid)")
26+
27+
try FileManager.default.createDirectory(at: resolverDirectoryURL, withIntermediateDirectories: true, attributes: nil)
28+
29+
return resolverDirectoryURL
30+
}
31+
32+
/// Returns the directory that should be added to the classpath of the JVM to analyze the sources.
33+
func compileJava(_ sourceText: String) async throws -> URL {
34+
let sourceFile = try TempFile.create(suffix: "java", sourceText)
35+
36+
let classesDirectory = try createTemporaryDirectory(in: FileManager.default.temporaryDirectory)
37+
38+
let javacProcess = try await _Subprocess.run(
39+
.path("/usr/bin/javac"),
40+
arguments: [
41+
"-d", classesDirectory.path, // output directory for .class files
42+
sourceFile.path
43+
],
44+
output: .string(limit: Int.max, encoding: UTF8.self),
45+
error: .string(limit: Int.max, encoding: UTF8.self)
46+
)
47+
48+
// Check if compilation was successful
49+
guard javacProcess.terminationStatus.isSuccess else {
50+
let outString = javacProcess.standardOutput ?? ""
51+
let errString = javacProcess.standardError ?? ""
52+
fatalError("javac '\(sourceFile)' failed (\(javacProcess.terminationStatus));\n" +
53+
"OUT: \(outString)\n" +
54+
"ERROR: \(errString)")
55+
}
56+
57+
print("Compiled java sources to: \(classesDirectory)")
58+
return classesDirectory
59+
}
60+
61+
func withJavaTranslator(
62+
javaClassNames: [String],
63+
classpath: [URL],
64+
body: (JavaTranslator) throws -> (),
65+
function: String = #function,
66+
file: StaticString = #filePath,
67+
line: UInt = #line
68+
) throws {
69+
let jvm = try JavaVirtualMachine.shared(
70+
classpath: classpath.map(\.path),
71+
replace: false
72+
)
73+
74+
var config = Configuration()
75+
config.minimumInputAccessLevelMode = .package
76+
77+
let environment = try jvm.environment()
78+
let translator = JavaTranslator(
79+
config: config,
80+
swiftModuleName: "SwiftModule",
81+
environment: environment,
82+
translateAsClass: true)
83+
84+
try body(translator)
85+
}
86+
87+
/// Translate a Java class and assert that the translated output contains
88+
/// each of the expected "chunks" of text.
89+
func assertWrapJavaOutput(
90+
javaClassNames: [String],
91+
classpath: [URL],
92+
expectedChunks: [String],
93+
function: String = #function,
94+
file: StaticString = #filePath,
95+
line: UInt = #line
96+
) throws {
97+
let jvm = try JavaVirtualMachine.shared(
98+
classpath: classpath.map(\.path),
99+
replace: false
100+
)
101+
102+
var config = Configuration()
103+
config.minimumInputAccessLevelMode = .package
104+
105+
let environment = try jvm.environment()
106+
let translator = JavaTranslator(
107+
config: config,
108+
swiftModuleName: "SwiftModule",
109+
environment: environment,
110+
translateAsClass: true)
111+
112+
let classLoader = try! JavaClass<JavaClassLoader>(environment: environment)
113+
.getSystemClassLoader()!
114+
115+
116+
// FIXME: deduplicate this
117+
translator.startNewFile()
118+
119+
var swiftCompleteOutputText = ""
120+
121+
var javaClasses: [JavaClass<JavaObject>] = []
122+
for javaClassName in javaClassNames {
123+
guard let javaClass = try classLoader.loadClass(javaClassName) else {
124+
fatalError("Could not load Java class '\(javaClassName)' in test \(function) @ \(file):\(line)!")
125+
}
126+
javaClasses.append(javaClass)
127+
128+
// FIXME: deduplicate this with SwiftJava.WrapJavaCommand.runCommand !!!
129+
// TODO: especially because nested classes
130+
// WrapJavaCommand().<TODO>
131+
132+
let swiftUnqualifiedName = javaClassName.javaClassNameToCanonicalName
133+
.defaultSwiftNameForJavaClass
134+
translator.translatedClasses[javaClassName] =
135+
.init(module: nil, name: swiftUnqualifiedName)
136+
137+
try translator.validateClassConfiguration()
138+
139+
let swiftClassDecls = try translator.translateClass(javaClass)
140+
let importDecls = translator.getImportDecls()
141+
142+
let swiftFileText =
143+
"""
144+
// ---------------------------------------------------------------------------
145+
// Auto-generated by Java-to-Swift wrapper generator.
146+
\(importDecls.map { $0.description }.joined())
147+
\(swiftClassDecls.map { $0.description }.joined(separator: "\n"))
148+
\n
149+
"""
150+
swiftCompleteOutputText += swiftFileText
151+
}
152+
153+
for expectedChunk in expectedChunks {
154+
if swiftCompleteOutputText.contains(expectedChunk) {
155+
continue
156+
}
157+
158+
XCTFail("Expected chunk: \n" +
159+
"\(expectedChunk.yellow)" +
160+
"\n" +
161+
"not found in:\n" +
162+
"\(swiftCompleteOutputText)",
163+
file: file, line: line)
164+
}
165+
166+
print(swiftCompleteOutputText)
167+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import JavaUtilJar
16+
@_spi(Testing) import SwiftJava
17+
import SwiftJavaConfigurationShared
18+
import SwiftJavaShared
19+
import SwiftJavaToolLib
20+
import XCTest // NOTE: Workaround for https://github.com/swiftlang/swift-java/issues/43
21+
import _Subprocess
22+
23+
class JavaTranslatorTests: XCTestCase {
24+
25+
func translateGenericMethodParameters() async throws {
26+
let classpathURL = try await compileJava(
27+
"""
28+
package com.example;
29+
30+
class Item<T> {
31+
final T value;
32+
Item(T item) {
33+
this.value = item;
34+
}
35+
}
36+
class Pair<First, Second> { }
37+
38+
class ExampleSimpleClass {
39+
<KeyType, ValueType> Pair<KeyType, ValueType> getPair(
40+
String name,
41+
Item<KeyType> key,
42+
Item<ValueType> value
43+
) { return null; }
44+
}
45+
""")
46+
47+
try withJavaTranslator(
48+
javaClassNames: [
49+
"com.example.Item",
50+
"com.example.Pair",
51+
"com.example.ExampleSimpleClass",
52+
],
53+
classpath: [classpathURL],
54+
) { translator in
55+
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)