From 0a33b74087c6e6cde95fc8da51d693a234d7cb27 Mon Sep 17 00:00:00 2001 From: J2ObjC Team Date: Thu, 25 Jun 2026 02:11:07 -0700 Subject: [PATCH] BEGIN_PUBLIC Add SwiftName option to ObjectiveCKmpMethod that takes precedence over `@SwiftName` annotation. END_PUBLIC PiperOrigin-RevId: 937845988 --- .../annotations/ObjectiveCKmpMethod.java | 6 + .../devtools/j2objc/util/NameTable.java | 83 +++++++--- .../ObjectiveCKmpMethodTranslatorTest.java | 153 ++++++++++++++++++ 3 files changed, 221 insertions(+), 21 deletions(-) diff --git a/annotations/src/main/java/com/google/j2objc/annotations/ObjectiveCKmpMethod.java b/annotations/src/main/java/com/google/j2objc/annotations/ObjectiveCKmpMethod.java index 2a61760400..634028bcf6 100644 --- a/annotations/src/main/java/com/google/j2objc/annotations/ObjectiveCKmpMethod.java +++ b/annotations/src/main/java/com/google/j2objc/annotations/ObjectiveCKmpMethod.java @@ -80,4 +80,10 @@ * The class to map types on the method to Kotlin/Native interop types. This field is required. */ Class adapter(); + + /** + * The Swift name to use for the adapter method. If not specified, a default Swift name will be + * generated based on the selector. + */ + String swiftName() default ""; } diff --git a/translator/src/main/java/com/google/devtools/j2objc/util/NameTable.java b/translator/src/main/java/com/google/devtools/j2objc/util/NameTable.java index 73b44d3a01..4f92cfc82f 100644 --- a/translator/src/main/java/com/google/devtools/j2objc/util/NameTable.java +++ b/translator/src/main/java/com/google/devtools/j2objc/util/NameTable.java @@ -32,6 +32,7 @@ import com.google.devtools.j2objc.types.NativeType; import com.google.devtools.j2objc.types.PointerType; import com.google.j2objc.annotations.ObjectiveCAdapterMethod; +import com.google.j2objc.annotations.ObjectiveCKmpMethod; import com.google.j2objc.annotations.ObjectiveCName; import com.google.j2objc.annotations.ObjectiveCNativeEnumName; import com.google.j2objc.annotations.SwiftName; @@ -560,7 +561,10 @@ public String getMethodNameFromAnnotation(ExecutableElement method) { public @Nullable String getSwiftMethodNameFromAnnotation(MethodDeclaration method) { // Check if the method has the annotation - String annotationName = swiftNameFromAnnotation(method.getExecutableElement()); + String annotationName = swiftNameFromObjectiveCKmpMethodAnnotation(method.getExecutableElement()); + if (annotationName == null) { + annotationName = swiftNameFromAnnotation(method.getExecutableElement()); + } if (annotationName != null) { return annotationName; } @@ -637,9 +641,19 @@ public String getMethodNameFromAnnotation(ExecutableElement method) { return null; } + boolean hasKmpSwiftName = false; + AnnotationMirror kmpAnnotation = ElementUtil.getAnnotation(method, ObjectiveCKmpMethod.class); + if (kmpAnnotation != null) { + String kmpSwiftName = (String) ElementUtil.getAnnotationValue(kmpAnnotation, "swiftName"); + if (kmpSwiftName != null && !kmpSwiftName.isEmpty()) { + hasKmpSwiftName = true; + } + } + if (!packageHasSwiftNameAnnotation(owner) && !elementHasSwiftNameAnnotation(owner) - && !elementHasSwiftNameAnnotation(method)) { + && !elementHasSwiftNameAnnotation(method) + && !hasKmpSwiftName) { return null; } @@ -655,29 +669,45 @@ public String getMethodNameFromAnnotation(ExecutableElement method) { StringBuilder sb = new StringBuilder(); - String annotationName = function.getExecutableElement().getSimpleName().toString(); - - // Constructors are `` - annotationName = annotationName.replace("<", "").replace(">", ""); - - if (annotationName.contains(":")) { - annotationName = annotationName.replace(":", ""); + String customSwiftName = swiftNameFromObjectiveCKmpMethodAnnotation(method); + boolean isKmpSwiftName = customSwiftName != null; + if (customSwiftName == null) { + customSwiftName = swiftNameFromAnnotation(method); } - annotationName = addSwiftParamNames(function.getParameters(), annotationName, ':'); - if (!functionName.contains("_init") && annotationName != null) { - sb.append(" NS_SWIFT_NAME("); - sb.append(className).append(".").append(annotationName); - sb.append(")"); - } else if (functionName.contains("new_")) { - sb.append(" NS_SWIFT_NAME("); - - if (annotationName != null) { - sb.append(className).append(".").append(annotationName); - } else { + if (customSwiftName != null) { + if (functionName.contains("new_")) { + if (isKmpSwiftName) { + sb.append(" NS_SWIFT_NAME("); + sb.append(className).append(".").append(customSwiftName); + sb.append(")"); + } else { + sb.append(" NS_SWIFT_NAME("); + sb.append(addSwiftParamNames(function.getParameters(), className + ".init", ':')); + sb.append(")"); + } + } else if (!functionName.contains("_init")) { + sb.append(" NS_SWIFT_NAME("); + sb.append(className).append(".").append(customSwiftName); + sb.append(")"); + } + } else { + String derivedName = method.getSimpleName().toString(); + derivedName = derivedName.replace("<", "").replace(">", ""); + if (derivedName.contains(":")) { + derivedName = derivedName.replace(":", ""); + } + derivedName = addSwiftParamNames(function.getParameters(), derivedName, ':'); + + if (!functionName.contains("_init")) { + sb.append(" NS_SWIFT_NAME("); + sb.append(className).append(".").append(derivedName); + sb.append(")"); + } else if (functionName.contains("new_")) { + sb.append(" NS_SWIFT_NAME("); sb.append(addSwiftParamNames(function.getParameters(), className + ".init", ':')); + sb.append(")"); } - sb.append(")"); } return sb.toString(); @@ -691,6 +721,17 @@ public String getMethodNameFromAnnotation(ExecutableElement method) { return null; } + public static @Nullable String swiftNameFromObjectiveCKmpMethodAnnotation(Element element) { + AnnotationMirror annotation = ElementUtil.getAnnotation(element, ObjectiveCKmpMethod.class); + if (annotation != null) { + String value = (String) ElementUtil.getAnnotationValue(annotation, "swiftName"); + if (value != null && !value.isEmpty()) { + return value; + } + } + return null; + } + public Boolean packageHasSwiftNameAnnotation(TypeElement classType) { if (options.swiftNaming()) { return true; 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 b81dd1408a..9e1629dafe 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 @@ -1993,5 +1993,158 @@ public void verifyList(List>> list) {} } """); } + + public void testSwiftName() throws IOException { + addSourceFile( + """ + import com.google.j2objc.annotations.ObjectiveCKmpMethod; + import java.util.List; + + public class SwiftNameTest { + @ObjectiveCKmpMethod( + selector="initWithList:", + adapter=Adapter.class, + swiftName="init(myList:)") + public SwiftNameTest(List list) {} + + @ObjectiveCKmpMethod( + selector="setList:", + adapter=Adapter.class, + swiftName="set(myList:)") + public void setList(List list) {} + + @ObjectiveCKmpMethod( + selector="staticSetList:", + adapter=Adapter.class, + swiftName="staticSet(myList:)") + public static void staticSetList(List list) {} + } + """, + "SwiftNameTest.java"); + + String header = translateSourceFile("SwiftNameTest", "SwiftNameTest.h"); + // ObjC methods should have NS_SWIFT_NAME + assertInTranslation( + header, + "- (instancetype)initWithList:(NSArray *)list NS_SWIFT_NAME(init(myList:));"); + assertInTranslation( + header, "- (void)setList:(NSArray *)list NS_SWIFT_NAME(set(myList:));"); + assertInTranslation( + header, + "+ (void)staticSetList:(NSArray *)list NS_SWIFT_NAME(staticSet(myList:));"); + + // C functions should NOT have NS_SWIFT_NAME when wrapper methods are enabled + assertInTranslation( + header, + "FOUNDATION_EXPORT SwiftNameTest *new_SwiftNameTest_initWithList_(NSArray" + + " *list);"); + assertNotInTranslation( + header, "new_SwiftNameTest_initWithList_(NSArray *list) NS_SWIFT_NAME"); + assertInTranslation( + header, "FOUNDATION_EXPORT void SwiftNameTest_staticSetList_(NSArray *list);"); + assertNotInTranslation( + header, "SwiftNameTest_staticSetList_(NSArray *list) NS_SWIFT_NAME"); + } + + public void testSwiftNameNoWrappers() throws IOException { + options.load(new String[] {"--no-wrapper-methods"}); + addSourceFile( + """ + import com.google.j2objc.annotations.ObjectiveCKmpMethod; + import java.util.List; + + public class SwiftNameTestNoWrappers { + @ObjectiveCKmpMethod( + selector="initWithList:", + adapter=Adapter.class, + swiftName="init(myList:)") + public SwiftNameTestNoWrappers(List list) {} + + @ObjectiveCKmpMethod( + selector="setList:", + adapter=Adapter.class, + swiftName="set(myList:)") + public void setList(List list) {} + + @ObjectiveCKmpMethod( + selector="staticSetList:", + adapter=Adapter.class, + swiftName="staticSet(myList:)") + public static void staticSetList(List list) {} + } + """, + "SwiftNameTestNoWrappers.java"); + + String header = translateSourceFile("SwiftNameTestNoWrappers", "SwiftNameTestNoWrappers.h"); + // ObjC instance methods still exist and should have NS_SWIFT_NAME + assertInTranslation( + header, "- (void)setList:(NSArray *)list NS_SWIFT_NAME(set(myList:));"); + + // ObjC class methods for static methods do NOT exist when wrapper methods are disabled + assertNotInTranslation(header, "+ (void)staticSetList"); + + // C functions SHOULD have NS_SWIFT_NAME when wrapper methods are disabled + assertInTranslation( + header, + "FOUNDATION_EXPORT SwiftNameTestNoWrappers" + + " *new_SwiftNameTestNoWrappers_initWithList_(NSArray *list) " + + "NS_SWIFT_NAME(SwiftNameTestNoWrappers.init(myList:));"); + assertInTranslation( + header, + "FOUNDATION_EXPORT void SwiftNameTestNoWrappers_staticSetList_(NSArray *list) " + + "NS_SWIFT_NAME(SwiftNameTestNoWrappers.staticSet(myList:));"); + } + + public void testPrecedenceOfObjectiveCKmpMethodSwiftName() throws IOException { + addSourceFile( + """ + import com.google.j2objc.annotations.ObjectiveCKmpMethod; + import com.google.j2objc.annotations.SwiftName; + import java.util.List; + + public class PrecedenceTest { + @ObjectiveCKmpMethod( + selector="setList:", + adapter=Adapter.class, + swiftName="kmpSwiftName(myList:)") + @SwiftName("standaloneSwiftName(myList:)") + public void setList(List list) {} + } + """, + "PrecedenceTest.java"); + + String header = translateSourceFile("PrecedenceTest", "PrecedenceTest.h"); + assertInTranslation( + header, "- (void)setList:(NSArray *)list NS_SWIFT_NAME(kmpSwiftName(myList:));"); + assertNotInTranslation(header, "standaloneSwiftName"); + } + + public void testObjectiveCKmpMethodWithSwiftNameFallback() throws IOException { + addSourceFile( + """ + import com.google.j2objc.annotations.ObjectiveCKmpMethod; + import com.google.j2objc.annotations.SwiftName; + import java.util.List; + + public class FallbackTest { + @ObjectiveCKmpMethod(selector="initWithList:", adapter=Adapter.class) + @SwiftName("init(myListFallback:)") + public FallbackTest(List list) {} + + @ObjectiveCKmpMethod(selector="setList:", adapter=Adapter.class) + @SwiftName("set(myListFallback:)") + public void setList(List list) {} + } + """, + "FallbackTest.java"); + + String header = translateSourceFile("FallbackTest", "FallbackTest.h"); + assertInTranslation( + header, + "- (instancetype)initWithList:(NSArray *)list" + + " NS_SWIFT_NAME(init(myListFallback:));"); + assertInTranslation( + header, "- (void)setList:(NSArray *)list NS_SWIFT_NAME(set(myListFallback:));"); + } }