Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -652,12 +652,16 @@ private ParameterMapping processParameters() {
List<VariableElement> adapterParameters = new ArrayList<>();
for (VariableElement parameter : originalMethodParameters) {
TypeMirror parameterType = parameter.asType();
if (!isTypeSupported(parameterType)) {
// If the type is not supported (a.k.a mapped to a native type), but is an interface or
// nullable, we still go through the computation of the type as we will be adding more
// information than just the type name (id brackets and nullability marker).
if (!isTypeSupported(parameterType)
&& !isNullable(parameterType)
&& !TypeUtil.isInterface(parameterType)) {
adaptingArguments.add(new SimpleName(parameter));
adapterParameters.add(parameter);
continue;
}

NativeType nativeType = calculateNativeType(parameterType, originalMethodExecutable);
adapterParameters.add(
GeneratedVariableElement.newParameter(
Expand Down Expand Up @@ -787,17 +791,19 @@ private NativeType calculateNativeType(
return new NativeType(builder.toString(), null, null, referencedTypes);
}

private String toNativeType(
private PrintableNativeType toNativeType(
TypeMirror typeMirror, ExecutableElement methodExecutable, List<TypeMirror> referencedTypes) {
String fullyQualifiedNameJavaName = TypeUtil.getQualifiedName(typeMirror);
boolean isNullable = isNullable(typeMirror);
String nativeType = JAVA_TO_NATIVE_TYPE_MAP.get(fullyQualifiedNameJavaName);
if (nativeType != null) {
return nativeType;
return new PrintableNativeType(nativeType, false, isNullable);
}
TypeElement typeElement = TypeUtil.asTypeElement(typeMirror);
if (typeElement != null) {
referencedTypes.add(typeMirror);
return nameTable.getFullName(typeElement);
return new PrintableNativeType(
nameTable.getFullName(typeElement), TypeUtil.isInterface(typeMirror), isNullable);
}
throw new IllegalArgumentException(
"Unsupported type: " + fullyQualifiedNameJavaName + " in method: " + methodExecutable);
Expand All @@ -808,6 +814,18 @@ private boolean isTypeSupported(TypeMirror typeMirror) {
TypeUtil.getQualifiedName(typeUtil.erasure(typeMirror)));
}

private boolean isNullable(TypeMirror typeMirror) {
return typeMirror.getAnnotationMirrors().stream()
.anyMatch(
annotation ->
annotation
.getAnnotationType()
.asElement()
.getSimpleName()
.toString()
.contains("Nullable"));
}

/**
* Recursively visits parameterized types to build a string representation.
*
Expand All @@ -820,6 +838,8 @@ private boolean isTypeSupported(TypeMirror typeMirror) {
* <li>{@code List<String>} becomes {@code NSArray<NSString *> *}
* <li>{@code Map<Integer, String>} becomes {@code NSDictionary<NSNumber *, NSString *> *}
* <li>{@code Set<List<String>>} becomes {@code NSSet<NSArray<NSString *> *> *}
* <li>{@code JavaInterface} becomes {@code id<JavaInterface>}
* <li>{@code List<JavaInterface>} becomes {@code NSArray<id<JavaInterface>> *}
* </ul>
*/
private class NativeTypeVisitor extends SimpleTypeVisitor9<Void, StringBuilder> {
Expand All @@ -835,13 +855,24 @@ private NativeTypeVisitor(

@Override
public Void visitDeclared(DeclaredType type, StringBuilder builder) {
builder.append(toNativeType(type, methodExecutable, referencedTypes));
PrintableNativeType nativeType = toNativeType(type, methodExecutable, referencedTypes);
if (nativeType.isUntranslatedInterface) {
builder.append("id<");
}
builder.append(nativeType.nativeTypeName);
List<? extends TypeMirror> typeArguments = type.getTypeArguments();
if (options.asObjCGenericDecl() && !typeArguments.isEmpty()) {
String typeArgsString = buildTypeArgumentString(typeArguments);
builder.append("<").append(typeArgsString).append(">");
}
builder.append(" *");
if (nativeType.isUntranslatedInterface) {
builder.append(">");
} else {
builder.append(" *");
}
if (nativeType.nullable) {
builder.append(" _Nullable");
}
return null;
}

Expand Down Expand Up @@ -937,4 +968,7 @@ abstract AdapterLookup.ConverterMatch findParamConverter(
abstract TypeMirror getReturnCastType(
TypeMirror originalMethodReturnType, TypeMirror adapterReturnType);
}

private record PrintableNativeType(
String nativeTypeName, boolean isUntranslatedInterface, boolean nullable) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public int size() {
""",
"com/google/common/collect/ImmutableList.java");
addSourceFile("public class Foo {}", "Foo.java");
addSourceFile("public class Bar {}", "Bar.java");
addSourceFile("public class Bar implements Baz {}", "Bar.java");
addSourceFile("public interface Baz {}", "Baz.java");
addSourceFile(
"""
import java.util.List;
Expand Down Expand Up @@ -1595,4 +1596,200 @@ public void setListSet(Set<List<String>> set) {}
assertThrows(Throwable.class, () -> translateSourceFile("FailList", "FailList.m"));
assertTrue(e.getMessage().contains("Exact converter required for mapped type"));
}

public void testInterfaceTypes() throws IOException {
addSourceFile(
"""
import com.google.j2objc.annotations.ObjectiveCKmpMethod;
import java.util.Set;
import java.util.List;
public class InterfaceUsage {
@ObjectiveCKmpMethod(selector="getInterface", adapter=Adapter.class)
public Baz getInterface() {
return null;
}

@ObjectiveCKmpMethod(selector="setInterface:", adapter=Adapter.class)
public void setInterface(Baz value) {
return;
}
}
""",
"InterfaceUsage.java");

String header = translateSourceFile("InterfaceUsage", "InterfaceUsage.h");
assertInTranslation(
header,
"""
- (id<Baz>)getInterface;
""");
assertInTranslation(
header,
"""
- (void)setInterface:(id<Baz>)value;
""");
}

public void testListOfInterface() throws IOException {
addSourceFile(
"""
import com.google.j2objc.annotations.ObjectiveCKmpMethod;
import java.util.List;
public class ListOfInterface {
@ObjectiveCKmpMethod(selector="getListOfInterface", adapter=Adapter.class)
public List<Baz> getListOfInterface() {
return null;
}

@ObjectiveCKmpMethod(selector="setListOfInterface:", adapter=Adapter.class)
public void setListOfInterface(List<Baz> list) {
return;
}
}
""",
"ListOfInterface.java");

String header = translateSourceFile("ListOfInterface", "ListOfInterface.h");
assertInTranslation(
header,
"""
- (NSArray<id<Baz>> *)getListOfInterface;
""");
assertInTranslation(
header,
"""
- (void)setListOfInterface:(NSArray<id<Baz>> *)list;
""");
}

public void testNullableTypes() throws IOException {
addSourceFile(
"""
import com.google.j2objc.annotations.ObjectiveCKmpMethod;
import java.util.List;
import java.util.Map;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.jspecify.annotations.Nullable;

public class NullableTypes {
@ObjectiveCKmpMethod(selector="getNullableInterface", adapter=Adapter.class)
public @Nullable Baz getNullableInterface() {
return null;
}

@ObjectiveCKmpMethod(selector="getNullableClass", adapter=Adapter.class)
public @Nullable Bar getNullableClass() {
return null;
}

@ObjectiveCKmpMethod(selector="getListOfNullableInterface", adapter=Adapter.class)
public List<@Nullable Baz> getListOfNullableInterface() {
return ImmutableList.of();
}

@ObjectiveCKmpMethod(selector="getListOfNullableClass", adapter=Adapter.class)
public List<@Nullable Bar> getListOfNullableClass() {
return ImmutableList.of();
}

@ObjectiveCKmpMethod(selector="getNullableListOfInterface", adapter=Adapter.class)
public @Nullable List<Baz> getNullableListOfInterface() {
return null;
}

@ObjectiveCKmpMethod(selector="getNullableListOfClass", adapter=Adapter.class)
public @Nullable List<Bar> getNullableListOfClass() {
return null;
}

@ObjectiveCKmpMethod(selector="getMapOfNullableInterface", adapter=Adapter.class)
public Map<String, @Nullable Baz> getMapOfNullableInterface() {
return ImmutableMap.of();
}

@ObjectiveCKmpMethod(selector="getMapOfNullableClass", adapter=Adapter.class)
public Map<String, @Nullable Bar> getMapOfNullableClass() {
return ImmutableMap.of();
}

@ObjectiveCKmpMethod(selector="getNullableMapOfInterface", adapter=Adapter.class)
public @Nullable Map<String, Baz> getNullableMapOfInterface() {
return null;
}

@ObjectiveCKmpMethod(selector="getNullableMapOfClass", adapter=Adapter.class)
public @Nullable Map<String, Bar> getNullableMapOfClass() {
return null;
}

@ObjectiveCKmpMethod(selector="setWithNullableBar:withNullableBaz:withNullableList:withNullableMap:", adapter=Adapter.class)
public @Nullable Foo set(@Nullable Bar bar, @Nullable Baz baz, @Nullable List<@Nullable Bar> list,
@Nullable Map<String, @Nullable Baz> map) {
return null;
}
}
""",
"NullableTypes.java");

String header = translateSourceFile("NullableTypes", "NullableTypes.h");
assertInTranslation(
header,
"""
- (id<Baz> _Nullable)getNullableInterface;
""");
assertInTranslation(
header,
"""
- (Bar * _Nullable)getNullableClass;
""");
assertInTranslation(
header,
"""
- (NSArray<id<Baz> _Nullable> *)getListOfNullableInterface;
""");
assertInTranslation(
header,
"""
- (NSArray<Bar * _Nullable> *)getListOfNullableClass;
""");
assertInTranslation(
header,
"""
- (NSArray<id<Baz>> * _Nullable)getNullableListOfInterface;
""");
assertInTranslation(
header,
"""
- (NSArray<Bar *> * _Nullable)getNullableListOfClass;
""");
assertInTranslation(
header,
"""
- (NSDictionary<NSString *, id<Baz> _Nullable> *)getMapOfNullableInterface;
""");
assertInTranslation(
header,
"""
- (NSDictionary<NSString *, Bar * _Nullable> *)getMapOfNullableClass;
""");
assertInTranslation(
header,
"""
- (NSDictionary<NSString *, id<Baz>> * _Nullable)getNullableMapOfInterface;
""");
assertInTranslation(
header,
"""
- (NSDictionary<NSString *, Bar *> * _Nullable)getNullableMapOfClass;
""");
assertInTranslation(
header,
"""
- (Foo * _Nullable)setWithNullableBar:(Bar * _Nullable)bar
withNullableBaz:(id<Baz> _Nullable)baz
withNullableList:(NSArray<Bar * _Nullable> * _Nullable)list
withNullableMap:(NSDictionary<NSString *, id<Baz> _Nullable> * _Nullable)map;
""");
}
}