Skip to content

Commit 6828bc8

Browse files
authored
javakit-macros: Support wrapping generic arguments (#601)
* Add test for JavaOptional * Erase generic argument type in JNI call * Considering class scope generic parameter * Update List.swift partially * Fix test macro fixture * Avoid swift-syntax error log * Add comment and error details
1 parent f79ad46 commit 6828bc8

7 files changed

Lines changed: 316 additions & 38 deletions

File tree

Sources/SwiftJava/Optional+JavaOptional.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
extension Optional where Wrapped: AnyJavaObject {
1616
public func toJavaOptional() -> JavaOptional<Wrapped> {
17-
try! JavaClass<JavaOptional<Wrapped>>().ofNullable(self?.as(JavaObject.self)).as(JavaOptional<Wrapped>.self)!
17+
try! JavaClass<JavaOptional<Wrapped>>().ofNullable(self)
1818
}
1919

2020
public init(javaOptional: JavaOptional<Wrapped>?) {
@@ -29,7 +29,7 @@ extension Optional where Wrapped: AnyJavaObject {
2929
extension Optional where Wrapped == String {
3030
public func toJavaOptional() -> JavaOptional<JavaString> {
3131
if let self {
32-
return try! JavaClass<JavaOptional<JavaString>>().of(JavaString(self).as(JavaObject.self)).as(JavaOptional<JavaString>.self)!
32+
return try! JavaClass<JavaOptional<JavaString>>().of(JavaString(self))
3333
} else {
3434
return try! JavaClass<JavaOptional<JavaString>>().empty().as(JavaOptional<JavaString>.self)!
3535
}

Sources/SwiftJava/generated/JavaOptional.swift

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,51 +3,115 @@ import SwiftJavaJNICore
33

44
@JavaClass("java.util.Optional")
55
open class JavaOptional<T: AnyJavaObject>: JavaObject {
6-
@JavaMethod(typeErasedResult: "T")
6+
/// Java method `get`.
7+
///
8+
/// ### Java method signature
9+
/// ```java
10+
/// public T java.util.Optional.get()
11+
/// ```
12+
@JavaMethod(typeErasedResult: "T!")
713
open func get() -> T!
814

15+
/// Java method `equals`.
16+
///
17+
/// ### Java method signature
18+
/// ```java
19+
/// public boolean java.util.Optional.equals(java.lang.Object)
20+
/// ```
921
@JavaMethod
1022
open override func equals(_ arg0: JavaObject?) -> Bool
1123

24+
/// Java method `toString`.
25+
///
26+
/// ### Java method signature
27+
/// ```java
28+
/// public java.lang.String java.util.Optional.toString()
29+
/// ```
1230
@JavaMethod
1331
open override func toString() -> String
1432

33+
/// Java method `hashCode`.
34+
///
35+
/// ### Java method signature
36+
/// ```java
37+
/// public int java.util.Optional.hashCode()
38+
/// ```
1539
@JavaMethod
1640
open override func hashCode() -> Int32
1741

42+
/// Java method `isEmpty`.
43+
///
44+
/// ### Java method signature
45+
/// ```java
46+
/// public boolean java.util.Optional.isEmpty()
47+
/// ```
1848
@JavaMethod
1949
open func isEmpty() -> Bool
2050

21-
@JavaMethod
22-
open func isPresent() -> Bool
51+
/// Java method `orElse`.
52+
///
53+
/// ### Java method signature
54+
/// ```java
55+
/// public T java.util.Optional.orElse(T)
56+
/// ```
57+
@JavaMethod(typeErasedResult: "T!")
58+
open func orElse(_ arg0: T?) -> T!
2359

60+
/// Java method `isPresent`.
61+
///
62+
/// ### Java method signature
63+
/// ```java
64+
/// public boolean java.util.Optional.isPresent()
65+
/// ```
2466
@JavaMethod
25-
open func orElse(_ arg0: JavaObject?) -> JavaObject!
67+
open func isPresent() -> Bool
2668

27-
@JavaMethod
28-
open func orElseThrow() -> JavaObject!
69+
/// Java method `orElseThrow`.
70+
///
71+
/// ### Java method signature
72+
/// ```java
73+
/// public T java.util.Optional.orElseThrow()
74+
/// ```
75+
@JavaMethod(typeErasedResult: "T!")
76+
open func orElseThrow() -> T!
2977
}
3078
extension JavaClass {
79+
/// Java method `of`.
80+
///
81+
/// ### Java method signature
82+
/// ```java
83+
/// public static <T> java.util.Optional<T> java.util.Optional.of(T)
84+
/// ```
3185
@JavaStaticMethod
32-
public func of<T: AnyJavaObject>(_ arg0: JavaObject?) -> JavaOptional<JavaObject>! where ObjectType == JavaOptional<T>
86+
public func of<T: AnyJavaObject>(_ arg0: T?) -> JavaOptional<T>! where ObjectType == JavaOptional<T>
3387

34-
public func ofOptional<T: AnyJavaObject>(_ arg0: JavaObject?) -> JavaObject? where ObjectType == JavaOptional<T> {
88+
public func ofOptional<T: AnyJavaObject>(_ arg0: T?) -> T? where ObjectType == JavaOptional<T> {
3589
Optional(javaOptional: of(arg0))
3690
}
3791

92+
/// Java method `empty`.
93+
///
94+
/// ### Java method signature
95+
/// ```java
96+
/// public static <T> java.util.Optional<T> java.util.Optional.empty()
97+
/// ```
3898
@JavaStaticMethod
39-
public func empty<T: AnyJavaObject>() -> JavaOptional<JavaObject>! where ObjectType == JavaOptional<T>
99+
public func empty<T: AnyJavaObject>() -> JavaOptional<T>! where ObjectType == JavaOptional<T>
40100

41-
public func emptyOptional<T: AnyJavaObject>() -> JavaObject? where ObjectType == JavaOptional<T> {
101+
public func emptyOptional<T: AnyJavaObject>() -> T? where ObjectType == JavaOptional<T> {
42102
Optional(javaOptional: empty())
43103
}
44104

105+
/// Java method `ofNullable`.
106+
///
107+
/// ### Java method signature
108+
/// ```java
109+
/// public static <T> java.util.Optional<T> java.util.Optional.ofNullable(T)
110+
/// ```
45111
@JavaStaticMethod
46-
public func ofNullable<T: AnyJavaObject>(_ arg0: JavaObject?) -> JavaOptional<JavaObject>!
47-
where ObjectType == JavaOptional<T>
112+
public func ofNullable<T: AnyJavaObject>(_ arg0: T?) -> JavaOptional<T>! where ObjectType == JavaOptional<T>
48113

49-
public func ofNullableOptional<T: AnyJavaObject>(_ arg0: JavaObject?) -> JavaObject?
50-
where ObjectType == JavaOptional<T> {
114+
public func ofNullableOptional<T: AnyJavaObject>(_ arg0: T?) -> T? where ObjectType == JavaOptional<T> {
51115
Optional(javaOptional: ofNullable(arg0))
52116
}
53117
}

Sources/SwiftJava/generated/List.swift

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,23 @@ public struct List<E: AnyJavaObject> {
3333
@JavaMethod
3434
public func isEmpty() -> Bool
3535

36-
@JavaMethod
37-
public func add(_ arg0: JavaObject?) -> Bool
38-
39-
@JavaMethod
40-
public func add(_ arg0: Int32, _ arg1: JavaObject?)
36+
/// Java method `add`.
37+
///
38+
/// ### Java method signature
39+
/// ```java
40+
/// public abstract void java.util.List.add(int,E)
41+
/// ```
42+
@JavaMethod
43+
public func add(_ arg0: Int32, _ arg1: E?)
44+
45+
/// Java method `add`.
46+
///
47+
/// ### Java method signature
48+
/// ```java
49+
/// public abstract boolean java.util.List.add(E)
50+
/// ```
51+
@JavaMethod
52+
public func add(_ arg0: E?) -> Bool
4153

4254
@JavaMethod
4355
public func subList(_ arg0: Int32, _ arg1: Int32) -> List<JavaObject>!

Sources/SwiftJavaMacros/JavaMethodMacro.swift

Lines changed: 87 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ extension JavaMethodMacro: BodyMacro {
5050
fatalError("not a function: \(declaration)")
5151
}
5252

53+
var resultStatements: [CodeBlockItemSyntax] = []
54+
5355
let funcName =
5456
if case .argumentList(let arguments) = node.arguments,
5557
let argument = arguments.first,
@@ -65,7 +67,28 @@ extension JavaMethodMacro: BodyMacro {
6567

6668
let isStatic = node.attributeName.trimmedDescription == "JavaStaticMethod"
6769
let params = funcDecl.signature.parameterClause.parameters
68-
let paramNames = params.map { param in param.parameterName?.text ?? "" }.joined(separator: ", ")
70+
71+
var paramNames: [String] = []
72+
for param in params {
73+
guard let name = param.parameterName else {
74+
throw MacroErrors.parameterMustHaveName(method: funcName, paramSyntax: param.trimmedDescription)
75+
}
76+
if isJNIGenericParameter(param.type, funcDecl: funcDecl, in: context) {
77+
let erasedName: TokenSyntax = "\(name)$erased"
78+
if param.type.optionalUnwrappedType() != nil {
79+
resultStatements.append(
80+
"let \(erasedName) = \(name).map { JavaObject(javaHolder: $0.javaHolder) }"
81+
)
82+
} else {
83+
resultStatements.append(
84+
"let \(erasedName) = JavaObject(javaHolder: \(name).javaHolder)"
85+
)
86+
}
87+
paramNames.append(erasedName.text)
88+
} else {
89+
paramNames.append(name.text)
90+
}
91+
}
6992

7093
let genericResultType: String? =
7194
if case let .argumentList(arguments) = node.arguments,
@@ -106,7 +129,7 @@ extension JavaMethodMacro: BodyMacro {
106129
if paramNames.isEmpty {
107130
parametersAsArgs = ""
108131
} else {
109-
parametersAsArgs = ", arguments: \(paramNames)"
132+
parametersAsArgs = ", arguments: \(paramNames.joined(separator: ", "))"
110133
}
111134

112135
let canRethrowError = funcDecl.signature.effectSpecifiers?.throwsClause != nil
@@ -137,23 +160,60 @@ extension JavaMethodMacro: BodyMacro {
137160
"""
138161

139162
if let genericResultType {
140-
return [
163+
resultStatements.append(
141164
"""
142165
/* convert erased return value to \(raw: genericResultType) */
143166
let result$ = \(resultSyntax)
167+
"""
168+
)
169+
resultStatements.append(
170+
"""
144171
if let result$ {
145172
return \(raw: genericResultType)(javaThis: result$.javaThis, environment: try! JavaVirtualMachine.shared().environment())
146173
} else {
147174
return nil
148175
}
149176
"""
150-
]
177+
)
178+
} else {
179+
// no return type conversions
180+
resultStatements.append("return \(resultSyntax)")
151181
}
152182

153-
// no return type conversions
154-
return [
155-
"return \(resultSyntax)"
156-
]
183+
return resultStatements
184+
}
185+
186+
/// Determines whether an argument is generic in heuristic way.
187+
/// Since Optional does not appear in JNI signatures, it is removed before checking.
188+
/// FIXME: It might be preferable to explicitly specify the type from JavaClass, similar to `typeErasedResult`.
189+
private static func isJNIGenericParameter(
190+
_ type: TypeSyntax,
191+
funcDecl: FunctionDeclSyntax,
192+
in context: some MacroExpansionContext
193+
) -> Bool {
194+
let baseType = type.optionalUnwrappedType() ?? type
195+
guard let identifier = baseType.as(IdentifierTypeSyntax.self) else {
196+
return false
197+
}
198+
let typeName = identifier.name.text
199+
200+
if let genericParams = funcDecl.genericParameterClause?.parameters {
201+
if genericParams.contains(where: { $0.name.text == typeName }) {
202+
return true
203+
}
204+
}
205+
206+
for contextNode in context.lexicalContext {
207+
if let decl = contextNode.asProtocol(WithGenericParametersSyntax.self) {
208+
if decl.genericParameterClause?.parameters.contains(where: {
209+
$0.name.text == typeName
210+
}) == true {
211+
return true
212+
}
213+
}
214+
}
215+
216+
return false
157217
}
158218

159219
/// Bridge an initializer into a call to Java.
@@ -238,4 +298,23 @@ extension TypeSyntaxProtocol {
238298
var typeReferenceString: String {
239299
typeReference.description
240300
}
301+
302+
func optionalUnwrappedType() -> TypeSyntax? {
303+
if let optionalType = self.as(OptionalTypeSyntax.self) {
304+
return optionalType.wrappedType
305+
}
306+
307+
if let implicitlyUnwrappedType = self.as(ImplicitlyUnwrappedOptionalTypeSyntax.self) {
308+
return implicitlyUnwrappedType.wrappedType
309+
}
310+
311+
if let identifierType = self.as(IdentifierTypeSyntax.self),
312+
identifierType.name.text == "Optional",
313+
let genericArgumentClause = identifierType.genericArgumentClause
314+
{
315+
return genericArgumentClause.arguments.first?.argument.as(TypeSyntax.self)
316+
}
317+
318+
return nil
319+
}
241320
}

Sources/SwiftJavaMacros/MacroErrors.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ enum MacroErrors: Error {
2121
case methodNotOnFunction
2222
case missingEnvironment
2323
case macroOutOfContext(String)
24+
case parameterMustHaveName(method: String, paramSyntax: String)
2425
}

0 commit comments

Comments
 (0)