diff --git a/translator/src/main/java/com/google/devtools/j2objc/gen/PropertyGenerator.java b/translator/src/main/java/com/google/devtools/j2objc/gen/PropertyGenerator.java index ae700b0a6f..96de135a89 100644 --- a/translator/src/main/java/com/google/devtools/j2objc/gen/PropertyGenerator.java +++ b/translator/src/main/java/com/google/devtools/j2objc/gen/PropertyGenerator.java @@ -44,7 +44,18 @@ public static Optional generate( NameTable nameTable, TypeUtil typeUtil, boolean parametersNonnullByDefault) { - return new PropertyGenerator(fragment, options, nameTable, typeUtil, parametersNonnullByDefault) + return generate(fragment, options, nameTable, typeUtil, parametersNonnullByDefault, false); + } + + public static Optional generate( + VariableDeclarationFragment fragment, + Options options, + NameTable nameTable, + TypeUtil typeUtil, + boolean parametersNonnullByDefault, + boolean staticToInstance) { + return new PropertyGenerator( + fragment, options, nameTable, typeUtil, parametersNonnullByDefault, staticToInstance) .build(); } @@ -58,18 +69,21 @@ public static Optional generate( private final TypeMirror varType; private final String propertyName; private final FieldDeclaration declaration; + private final boolean staticToInstance; private PropertyGenerator( VariableDeclarationFragment fragment, Options options, NameTable nameTable, TypeUtil typeUtil, - boolean parametersNonnullByDefault) { + boolean parametersNonnullByDefault, + boolean staticToInstance) { this.fragment = fragment; this.options = options; this.nameTable = nameTable; this.typeUtil = typeUtil; this.parametersNonnullByDefault = parametersNonnullByDefault; + this.staticToInstance = staticToInstance; declaration = (FieldDeclaration) fragment.getParent(); PropertyAnnotation annotation = (PropertyAnnotation) TreeUtil.getAnnotation(Property.class, declaration.getAnnotations()); @@ -154,7 +168,7 @@ private void processAccessorAttributes(Set attributes) { } private void processClassAttribute(Set attributes) { - if (ElementUtil.isStatic(varElement)) { + if (ElementUtil.isStatic(varElement) && !staticToInstance) { attributes.add("class"); } else if (attributes.contains("class")) { ErrorUtil.error(fragment, "Only static fields can be translated to class properties"); diff --git a/translator/src/main/java/com/google/devtools/j2objc/gen/TypeDeclarationGenerator.java b/translator/src/main/java/com/google/devtools/j2objc/gen/TypeDeclarationGenerator.java index 6cfa6dc5c1..85e2a70319 100644 --- a/translator/src/main/java/com/google/devtools/j2objc/gen/TypeDeclarationGenerator.java +++ b/translator/src/main/java/com/google/devtools/j2objc/gen/TypeDeclarationGenerator.java @@ -141,6 +141,11 @@ protected void generateInitialDeclaration() { printMethodDeclaration((MethodDeclaration) declaration, false, true); } } + for (VariableDeclarationFragment fragment : getStaticFields()) { + PropertyGenerator.generate( + fragment, options, nameTable, typeUtil, parametersNonnullByDefault, true) + .ifPresent(this::println); + } println("\n@end\n"); } @@ -477,6 +482,12 @@ protected void printCompanionClassDeclaration() { printProperties(); printStaticInterfaceMethods(); printStaticAccessors(); + if (needsKotlinCompanionClass()) { + printf("\n#pragma clang diagnostic push\n"); + printf("#pragma clang diagnostic ignored \"-Wincompatible-property-type\"\n"); + printf("@property (readonly, class) id<%sCompanion> companion;\n", typeName); + printf("#pragma clang diagnostic pop\n"); + } println("\n@end"); } @@ -753,17 +764,17 @@ private void printMethodDeclaration( TypeElement typeElement = ElementUtil.getDeclaringClass(methodElement); boolean allowGenerics = !typeUtil.isProtoClass(typeElement.asType()); - if (typeElement.getKind().isInterface()) { + if (isKotlinCompanion) { + if (!ElementUtil.isStatic(methodElement)) { + return; + } + } else if (typeElement.getKind().isInterface()) { // isCompanion and isStatic must be both false (i.e. this prints a non-static method decl // in @protocol) or must both be true (i.e. this prints a static method decl in the // companion class' @interface). if (isCompanionClass != ElementUtil.isStatic(methodElement)) { return; } - } else if (isKotlinCompanion) { - if (!ElementUtil.isStatic(methodElement)) { - return; - } } newline(); diff --git a/translator/src/main/java/com/google/devtools/j2objc/gen/TypeGenerator.java b/translator/src/main/java/com/google/devtools/j2objc/gen/TypeGenerator.java index b136e8784d..e8278c8022 100644 --- a/translator/src/main/java/com/google/devtools/j2objc/gen/TypeGenerator.java +++ b/translator/src/main/java/com/google/devtools/j2objc/gen/TypeGenerator.java @@ -262,6 +262,10 @@ private boolean hasStaticMethods() { Iterables.filter(ElementUtil.getMethods(typeElement), ElementUtil::isStatic)); } + private boolean hasStaticFields() { + return !Iterables.isEmpty(getStaticFields()); + } + protected boolean needsKotlinCompanionClass() { if (!ElementUtil.hasAnnotation(typeElement, GenerateObjCCompanion.class)) { return false; @@ -269,12 +273,9 @@ protected boolean needsKotlinCompanionClass() { if (typeNode.hasPrivateDeclaration()) { throw new IllegalStateException("@GenerateObjCCompanion not supported for private classes."); } - if (typeElement.getKind().isInterface()) { - throw new IllegalStateException("@GenerateObjCCompanion not supported for interfaces."); - } - if (!hasStaticMethods()) { + if (!hasStaticMethods() && !hasStaticFields()) { throw new IllegalStateException( - "@GenerateObjCCompanion not supported for types without static methods."); + "@GenerateObjCCompanion not supported for types without static members."); } return true; } @@ -286,7 +287,8 @@ protected boolean needsPublicCompanionClass() { return hasInitializeMethod() || hasStaticAccessorMethods() || ElementUtil.isGeneratedAnnotation(typeElement) - || hasStaticMethods(); + || hasStaticMethods() + || needsKotlinCompanionClass(); } protected boolean needsCompanionClass() { diff --git a/translator/src/main/java/com/google/devtools/j2objc/gen/TypeImplementationGenerator.java b/translator/src/main/java/com/google/devtools/j2objc/gen/TypeImplementationGenerator.java index 95eaea7c23..92d8debeb2 100644 --- a/translator/src/main/java/com/google/devtools/j2objc/gen/TypeImplementationGenerator.java +++ b/translator/src/main/java/com/google/devtools/j2objc/gen/TypeImplementationGenerator.java @@ -117,7 +117,7 @@ protected void generate() { syncLineNumbers(typeNode.getName()); // avoid doc-comment printf("@implementation %s\n", typeName); printProperties(); - if (!typeElement.getKind().isInterface() && needsKotlinCompanionClass()) { + if (needsKotlinCompanionClass()) { printf("\n+ (id<%sCompanion>)companion {", typeName); printf("\n return (id<%sCompanion>)self;\n}\n", typeName); } diff --git a/translator/src/test/java/com/google/devtools/j2objc/gen/GenerateObjCCompanionTest.java b/translator/src/test/java/com/google/devtools/j2objc/gen/GenerateObjCCompanionTest.java index 14ff928b52..e244cebf13 100644 --- a/translator/src/test/java/com/google/devtools/j2objc/gen/GenerateObjCCompanionTest.java +++ b/translator/src/test/java/com/google/devtools/j2objc/gen/GenerateObjCCompanionTest.java @@ -79,4 +79,68 @@ public static ThreadLocal identity(ThreadLocal val) { "- (JavaLangThreadLocal" + " *)identityWithJavaLangThreadLocal:(JavaLangThreadLocal *)val;"); } + + public void testCompanionPropertiesForStaticFields() throws IOException { + options.setClassProperties(true); + String source = + """ + @com.google.j2objc.annotations.GenerateObjCCompanion + public class Foo { + public static final int CONSTANT_VALUE = 1; + public static int mutableStaticField = 2; + private static int privateStaticField = 3; + } + """; + String header = translateSourceFile(source, "Foo", "Foo.h"); + assertInTranslation(header, "@protocol FooCompanion"); + // Should generate instance property for CONSTANT_VALUE in companion protocol. + assertInTranslation( + header, "@property (readonly) int32_t CONSTANT_VALUE NS_SWIFT_NAME(CONSTANT_VALUE);"); + // Should generate instance property for mutableStaticField in companion protocol. + assertInTranslation( + header, "@property int32_t mutableStaticField NS_SWIFT_NAME(mutableStaticField);"); + // Should NOT generate property for privateStaticField. + assertNotInTranslation(header, "privateStaticField"); + } + + public void testCompanionWithOnlyStaticFields() throws IOException { + options.setClassProperties(true); + String source = + """ + @com.google.j2objc.annotations.GenerateObjCCompanion + public class Foo { + public static final int CONSTANT_VALUE = 1; + } + """; + // This should compile successfully without throwing "types without static methods" exception. + String header = translateSourceFile(source, "Foo", "Foo.h"); + assertInTranslation(header, "@protocol FooCompanion"); + assertInTranslation( + header, "@property (readonly) int32_t CONSTANT_VALUE NS_SWIFT_NAME(CONSTANT_VALUE);"); + } + + public void testInterfaceCompanion() throws IOException { + options.setClassProperties(true); + String source = + """ + @com.google.j2objc.annotations.GenerateObjCCompanion + public interface Foo { + public static final int CONSTANT_VALUE = 1; + public static void doSomething() {} + } + """; + String header = translateSourceFile(source, "Foo", "Foo.h"); + assertInTranslation(header, "@protocol FooCompanion"); + assertInTranslation( + header, "@property (readonly) int32_t CONSTANT_VALUE NS_SWIFT_NAME(CONSTANT_VALUE);"); + assertInTranslation(header, "- (void)doSomething;"); + assertInTranslation(header, "@interface Foo : NSObject"); + assertInTranslation(header, "@property (readonly, class) id companion;"); + + String impl = translateSourceFile(source, "Foo", "Foo.m"); + assertInTranslation(impl, "@implementation Foo"); + assertInTranslation(impl, "+ (id)companion {"); + assertInTranslation(impl, "return (id)self;"); + } } +