diff --git a/translator/src/main/java/com/google/devtools/j2objc/translate/ObjectiveCKmpMethodTranslator.java b/translator/src/main/java/com/google/devtools/j2objc/translate/ObjectiveCKmpMethodTranslator.java index 79c896bdc8..741f709d1e 100644 --- a/translator/src/main/java/com/google/devtools/j2objc/translate/ObjectiveCKmpMethodTranslator.java +++ b/translator/src/main/java/com/google/devtools/j2objc/translate/ObjectiveCKmpMethodTranslator.java @@ -652,12 +652,16 @@ private ParameterMapping processParameters() { List 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( @@ -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 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); @@ -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. * @@ -820,6 +838,8 @@ private boolean isTypeSupported(TypeMirror typeMirror) { *
  • {@code List} becomes {@code NSArray *} *
  • {@code Map} becomes {@code NSDictionary *} *
  • {@code Set>} becomes {@code NSSet *> *} + *
  • {@code JavaInterface} becomes {@code id} + *
  • {@code List} becomes {@code NSArray> *} * */ private class NativeTypeVisitor extends SimpleTypeVisitor9 { @@ -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 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; } @@ -937,4 +968,7 @@ abstract AdapterLookup.ConverterMatch findParamConverter( abstract TypeMirror getReturnCastType( TypeMirror originalMethodReturnType, TypeMirror adapterReturnType); } + + private record PrintableNativeType( + String nativeTypeName, boolean isUntranslatedInterface, boolean nullable) {} } diff --git a/translator/src/test/java/com/google/devtools/j2objc/translate/ObjectiveCKmpMethodTranslatorTest.java b/translator/src/test/java/com/google/devtools/j2objc/translate/ObjectiveCKmpMethodTranslatorTest.java index 412d3397ae..b46fc84939 100644 --- a/translator/src/test/java/com/google/devtools/j2objc/translate/ObjectiveCKmpMethodTranslatorTest.java +++ b/translator/src/test/java/com/google/devtools/j2objc/translate/ObjectiveCKmpMethodTranslatorTest.java @@ -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; @@ -1595,4 +1596,200 @@ public void setListSet(Set> 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)getInterface; + """); + assertInTranslation( + header, + """ + - (void)setInterface:(id)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 getListOfInterface() { + return null; + } + + @ObjectiveCKmpMethod(selector="setListOfInterface:", adapter=Adapter.class) + public void setListOfInterface(List list) { + return; + } + } + """, + "ListOfInterface.java"); + + String header = translateSourceFile("ListOfInterface", "ListOfInterface.h"); + assertInTranslation( + header, + """ + - (NSArray> *)getListOfInterface; + """); + assertInTranslation( + header, + """ + - (void)setListOfInterface:(NSArray> *)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 getNullableListOfInterface() { + return null; + } + + @ObjectiveCKmpMethod(selector="getNullableListOfClass", adapter=Adapter.class) + public @Nullable List getNullableListOfClass() { + return null; + } + + @ObjectiveCKmpMethod(selector="getMapOfNullableInterface", adapter=Adapter.class) + public Map getMapOfNullableInterface() { + return ImmutableMap.of(); + } + + @ObjectiveCKmpMethod(selector="getMapOfNullableClass", adapter=Adapter.class) + public Map getMapOfNullableClass() { + return ImmutableMap.of(); + } + + @ObjectiveCKmpMethod(selector="getNullableMapOfInterface", adapter=Adapter.class) + public @Nullable Map getNullableMapOfInterface() { + return null; + } + + @ObjectiveCKmpMethod(selector="getNullableMapOfClass", adapter=Adapter.class) + public @Nullable Map 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 map) { + return null; + } + } + """, + "NullableTypes.java"); + + String header = translateSourceFile("NullableTypes", "NullableTypes.h"); + assertInTranslation( + header, + """ + - (id _Nullable)getNullableInterface; + """); + assertInTranslation( + header, + """ + - (Bar * _Nullable)getNullableClass; + """); + assertInTranslation( + header, + """ + - (NSArray _Nullable> *)getListOfNullableInterface; + """); + assertInTranslation( + header, + """ + - (NSArray *)getListOfNullableClass; + """); + assertInTranslation( + header, + """ + - (NSArray> * _Nullable)getNullableListOfInterface; + """); + assertInTranslation( + header, + """ + - (NSArray * _Nullable)getNullableListOfClass; + """); + assertInTranslation( + header, + """ + - (NSDictionary _Nullable> *)getMapOfNullableInterface; + """); + assertInTranslation( + header, + """ + - (NSDictionary *)getMapOfNullableClass; + """); + assertInTranslation( + header, + """ + - (NSDictionary> * _Nullable)getNullableMapOfInterface; + """); + assertInTranslation( + header, + """ + - (NSDictionary * _Nullable)getNullableMapOfClass; + """); + assertInTranslation( + header, + """ + - (Foo * _Nullable)setWithNullableBar:(Bar * _Nullable)bar + withNullableBaz:(id _Nullable)baz + withNullableList:(NSArray * _Nullable)list + withNullableMap:(NSDictionary _Nullable> * _Nullable)map; + """); + } }