/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.devtools.j2objc.gen; import com.google.devtools.j2objc.GenerationTest; import com.google.devtools.j2objc.ast.CompilationUnit; import com.google.devtools.j2objc.ast.MethodDeclaration; import com.google.devtools.j2objc.ast.TreeVisitor; import com.google.devtools.j2objc.util.ElementUtil; import com.google.devtools.j2objc.util.ErrorUtil; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; /** * Tests for {@link TypeDeclarationGenerator}. * * @author Keith Stanger */ public class TypeDeclarationGeneratorTest extends GenerationTest { public void testAnonymousClassDeclaration() throws IOException { String translation = translateSourceFile( "public class Example { Runnable run = new Runnable() { public void run() {} }; }", "Example", "Example.m"); assertTranslation(translation, "@interface Example_1 : NSObject < JavaLangRunnable >"); assertTranslation(translation, "- (void)run;"); // Outer reference is not required. assertNotInTranslation(translation, "Example *this"); assertNotInTranslation(translation, "- (id)initWithExample:"); } public void testAnonymousConcreteSubclassOfGenericAbstractType() throws IOException { String translation = translateSourceFile( "public class Test {" + " interface FooInterface<T> { public void foo1(T t); public void foo2(); }" + " abstract static class Foo<T> implements FooInterface<T> { public void foo2() { } }" + " Foo<Integer> foo = new Foo<Integer>() {" + " public void foo1(Integer i) { } }; }", "Test", "Test.m"); assertTranslation(translation, "foo1WithId:(JavaLangInteger *)i"); } public void testAccessorForStaticPrimitiveConstant() throws IOException { // Even though it's safe to access the define directly, we should add an // accessor to be consistent with other static variables. String translation = translateSourceFile( "class Test { static final int FOO = 1; }", "Test", "Test.h"); assertTranslation(translation, "#define Test_FOO 1"); assertTranslation(translation, "J2OBJC_STATIC_FIELD_CONSTANT(Test, FOO, jint)"); } // Verify that accessor methods for static vars and constants are generated on request. public void testStaticFieldAccessorMethods() throws IOException { options.setStaticAccessorMethods(true); String source = "class Test { " + "static String ID; " + "private static int i; " + "static final Test DEFAULT = new Test(); " + "static boolean DEBUG; }"; String translation = translateSourceFile(source, "Test", "Test.h"); assertTranslation(translation, "+ (NSString *)ID;"); assertTranslation(translation, "+ (void)setID:(NSString *)value;"); assertTranslation(translation, "+ (Test *)DEFAULT;"); assertTranslation(translation, "+ (jboolean)DEBUG_;"); assertTranslation(translation, "+ (void)setDEBUG_:(jboolean)value"); assertNotInTranslation(translation, "+ (jint)i"); assertNotInTranslation(translation, "+ (void)setI:(jint)value"); assertNotInTranslation(translation, "+ (void)setDEFAULT:(Test *)value"); } // Verify that accessor methods for static vars and constants aren't generated by default. public void testNoStaticFieldAccessorMethods() throws IOException { String source = "class Test { " + "static String ID; " + "private static int i; " + "static final Test DEFAULT = new Test(); }"; String translation = translateSourceFile(source, "Test", "Test.h"); assertNotInTranslation(translation, "+ (NSString *)ID"); assertNotInTranslation(translation, "+ (void)setID:(NSString *)value"); assertNotInTranslation(translation, "+ (Test *)DEFAULT"); assertNotInTranslation(translation, "+ (jint)i"); assertNotInTranslation(translation, "+ (void)setI:(jint)value"); assertNotInTranslation(translation, "+ (void)setDEFAULT:(Test *)value"); } // Verify that accessor methods for enum constants are generated on request. public void testEnumConstantAccessorMethods() throws IOException { options.setStaticAccessorMethods(true); String source = "enum Test { ONE, TWO, EOF }"; // EOF is a reserved name. String translation = translateSourceFile(source, "Test", "Test.h"); assertTranslation(translation, "+ (Test *)ONE;"); assertTranslation(translation, "+ (Test *)TWO;"); assertTranslation(translation, "+ (Test *)EOF_;"); } // Verify that accessor methods for enum constants are generated by --swift-friendly flag. public void testSwiftFriendlyEnumConstantAccessorMethods() throws IOException { options.setSwiftFriendly(true); String source = "enum Test { ONE, TWO, EOF }"; // EOF is a reserved name. String translation = translateSourceFile(source, "Test", "Test.h"); assertTranslation(translation, "+ (Test * __nonnull)ONE;"); assertTranslation(translation, "+ (Test * __nonnull)TWO;"); assertTranslation(translation, "+ (Test * __nonnull)EOF_;"); } // Verify that accessor methods for enum constants are not generated by default. public void testNoEnumConstantAccessorMethods() throws IOException { String source = "enum Test { ONE, TWO, EOF_ }"; String translation = translateSourceFile(source, "Test", "Test.h"); assertNotInTranslation(translation, "+ (TestEnum *)ONE"); assertNotInTranslation(translation, "+ (TestEnum *)TWO"); assertNotInTranslation(translation, "+ (TestEnum *)EOF"); } public void testNoStaticFieldAccessorForPrivateInnerType() throws IOException { options.setStaticAccessorMethods(true); String translation = translateSourceFile( "class Test { private static class Inner1 { " + "public static class Inner2 { static String ID; } } }", "Test", "Test.m"); assertNotInTranslation(translation, "+ (NSString *)ID"); assertNotInTranslation(translation, "+ (void)setID:"); } public void testStaticFieldAccessorInInterfaceType() throws IOException { options.setStaticAccessorMethods(true); String translation = translateSourceFile( "interface Test { public static final boolean FOO = true; }", "Test", "Test.h"); // The static accessor must go in the companion class, not the @protocol. assertTranslatedLines(translation, "@interface Test : NSObject", "", "+ (jboolean)FOO;"); } public void testProperties() throws IOException { String source = "import com.google.j2objc.annotations.Property; " + "public class FooBar {" + " @Property(\"readonly, nonatomic\") private int fieldBar, fieldBaz;" + " @Property(\"readwrite\") private String fieldCopy;" + " @Property private boolean fieldBool;" + " @Property(\"nonatomic, readonly, weak\") private int fieldReorder;" + " public int getFieldBaz() { return 1; }" + " public void setFieldNonAtomic(int value) { }" + " public void setFieldBaz(int value, int option) { }" + " public boolean isFieldBool() { return fieldBool; }" + "}"; String translation = translateSourceFile(source, "FooBar", "FooBar.h"); assertTranslation(translation, "@property (readonly, nonatomic) jint fieldBar;"); // Should split out fieldBaz and include the declared getter. assertTranslation(translation, "@property (readonly, nonatomic, getter=getFieldBaz) jint fieldBaz;"); // Set copy for strings and drop readwrite. assertTranslation(translation, "@property (copy) NSString *fieldCopy;"); // Test boolean getter. assertTranslation(translation, "@property (nonatomic, getter=isFieldBool) jboolean fieldBool;"); // Reorder property attributes and pass setter through. assertTranslation(translation, "@property (weak, readonly, nonatomic) jint fieldReorder;"); } public void testSynchronizedPropertyGetter() throws IOException { String source = "import com.google.j2objc.annotations.Property; " + "public class FooBar {" + " @Property(\"getter=getfieldBar\") private int fieldBar;" + " public synchronized int getFieldBar() { return fieldBar; }" + "}"; String translation = translateSourceFile(source, "FooBar", "FooBar.h"); assertTranslation(translation, "@property (getter=getfieldBar) jint fieldBar;"); } public void testBadPropertyAttribute() throws IOException { String source = "import com.google.j2objc.annotations.Property; " + "public class FooBar {" + " @Property(\"cause_exception\") private int fieldBar;" + "}"; translateSourceFile(source, "FooBar", "FooBar.h"); assertErrorCount(1); } public void testBadPropertySetterSelector() throws IOException { String source = "import com.google.j2objc.annotations.Property; " + "public class FooBar {" + " @Property(\"setter=needs_colon\") private int fieldBar;" + "}"; translateSourceFile(source, "FooBar", "FooBar.h"); assertErrorCount(1); } public void testNonexistentPropertySetter() throws IOException { String source = "import com.google.j2objc.annotations.Property; " + "public class FooBar {" + " @Property(\"setter=nonexistent:\") private int fieldBar;" + "}"; translateSourceFile(source, "FooBar", "FooBar.h"); assertErrorCount(1); } public void testPropertyWeakAssignment() throws IOException { String source = "import com.google.j2objc.annotations.Property; " + "import com.google.j2objc.annotations.Weak; " + "public class Foo {" + " @Property(\"weak\") Foo barA;" + " @Property(\"readonly\") @Weak Foo barB;" + " @Property(\"weak, readonly\") @Weak Foo barC;" + "}"; String translation = translateSourceFile(source, "Foo", "Foo.h"); // Add __weak instance variable assertTranslation(translation, "__unsafe_unretained Foo *barA_;"); assertTranslation(translation, "@property (weak) Foo *barA;"); assertNotInTranslation(translation, "J2OBJC_FIELD_SETTER(Foo, barA_, Foo *)"); // Add weak property attribute assertTranslation(translation, "__unsafe_unretained Foo *barB_;"); assertTranslation(translation, "@property (weak, readonly) Foo *barB;"); assertNotInTranslation(translation, "J2OBJC_FIELD_SETTER(Foo, barB_, Foo *)"); // Works with both assertTranslation(translation, "__unsafe_unretained Foo *barC_;"); assertTranslation(translation, "@property (weak, readonly) Foo *barC;"); assertNotInTranslation(translation, "J2OBJC_FIELD_SETTER(Foo, barC_, Foo *)"); } public void testWeakPropertyWithStrongAttribute() throws IOException { String source = "import com.google.j2objc.annotations.Property; " + "import com.google.j2objc.annotations.Weak; " + "public class Foo {" + " @Property(\"strong\") @Weak Foo barA;" + "}"; translateSourceFile(source, "Foo", "Foo.h"); assertErrorCount(1); } public void testClassProperties() throws IOException { options.setStaticAccessorMethods(true); String translation = translateSourceFile( "import com.google.j2objc.annotations.Property; " + "public class Test { " + "@Property static int test; " + "@Property(\"nonatomic\") static double d; }", "Test", "Test.h"); assertTranslatedLines(translation, "@property (class) jint test;", "@property (nonatomic, class) jdouble d;"); // Verify class attributes aren't assigned to instance fields. translateSourceFile( "import com.google.j2objc.annotations.Property; " + "public class Test { " + "@Property(\"class\") int test; }", "Test", "Test.h"); assertErrorCount(1); // Verify static accessor generation must be enabled for class properties. ErrorUtil.reset(); options.setStaticAccessorMethods(false); translateSourceFile( "import com.google.j2objc.annotations.Property; " + "public class Test { " + "@Property static int test; }", "Test", "Test.h"); assertErrorCount(1); } public void testNullabilityAttributes() throws IOException { String source = "import javax.annotation.*; " + "@ParametersAreNonnullByDefault public class Test {" + " @Nullable String test(@Nonnull String msg, Object var) { " + " return msg.isEmpty() ? null : msg; }" + " String test2() { " + " return \"\"; }" + " @Nonnull String test3() { " + " return \"\"; }" + "}"; options.setNullability(true); String translation = translateSourceFile(source, "Test", "Test.h"); // Verify return type and parameters are all annotated. assertTranslatedLines(translation, "- (NSString * __nullable)testWithNSString:(NSString * __nonnull)msg", // var is also nonnull because of the default annotation on the class. "withId:(id __nonnull)var;"); // Verify return type isn't annotated, as only parameters should be by default. assertTranslatedLines(translation, "- (NSString *)test2;"); // Verify return type is annotated. assertTranslation(translation, "- (NSString * __nonnull)test3;"); } // Verify ParametersAreNonnullByDefault sets unspecified parameter as non-null. public void testDefaultNonnullParameters() throws IOException { String source = "package foo.bar; import javax.annotation.*; " + "@ParametersAreNonnullByDefault public class Test {" + " @Nullable String test(@Nullable String msg, Object var, int count) { " + " return msg.isEmpty() ? null : msg; }" + "}"; options.setNullability(true); String translation = translateSourceFile(source, "foo.bar.Test", "foo/bar/Test.h"); // var is also nonnull because of the default annotation on the class. assertTranslatedLines(translation, // Verify parameter isn't affected by default. "- (NSString * __nullable)testWithNSString:(NSString * __nullable)msg", // Verify default nonnull is specified. "withId:(id __nonnull)var", // Default should not apply to primitive type. "withInt:(jint)count;"); } // Verify a ParametersAreNonnullByDefault package annotation sets unspecified // parameter as non-null. public void testDefaultNonnullParametersPackage() throws IOException { addSourceFile("@ParametersAreNonnullByDefault package foo.bar; " + "import javax.annotation.ParametersAreNonnullByDefault;", "foo/bar/package-info.java"); String source = "package foo.bar; import javax.annotation.*; " + "public class Test {" + " @Nullable String test(@Nullable String msg, Object var, int count) { " + " return msg.isEmpty() ? null : msg; }" + "}"; options.setNullability(true); String translation = translateSourceFile(source, "foo.bar.Test", "foo/bar/Test.h"); // var is also nonnull because of the default annotation on the class. assertTranslatedLines(translation, // Verify parameter isn't affected by default. "- (NSString * __nullable)testWithNSString:(NSString * __nullable)msg", // Verify default nonnull is specified. "withId:(id __nonnull)var", // Default should not apply to primitive type. "withInt:(jint)count;"); } public void testNullabilityPragmas() throws IOException { String source = "package foo.bar; import javax.annotation.*; " + "public class Test {" + " String test(@Nullable String msg, Object var, int count) { " + " return msg.isEmpty() ? null : msg; }" + "}"; options.setNullability(true); String translation = translateSourceFile(source, "foo.bar.Test", "foo/bar/Test.h"); assertTranslatedLines(translation, "#if __has_feature(nullability)", "#pragma clang diagnostic push", "#pragma GCC diagnostic ignored \"-Wnullability-completeness\"", "#endif"); assertTranslatedLines(translation, "#if __has_feature(nullability)", "#pragma clang diagnostic pop", "#endif"); } public void testPrivateNullabilityPragmas() throws IOException { String source = "package foo.bar; import javax.annotation.*; " + "public class Test {" + " private static class Inner {" + " String test(@Nullable String msg, Object var, int count) { " + " return msg.isEmpty() ? null : msg;" + " }" + " }" + "}"; options.setNullability(true); String translation = translateSourceFile(source, "foo.bar.Test", "foo/bar/Test.h"); assertTranslatedLines(translation, "#if __has_feature(nullability)", "#pragma clang diagnostic push", "#pragma GCC diagnostic ignored \"-Wnullability-completeness\"", "#endif"); assertTranslatedLines(translation, "#if __has_feature(nullability)", "#pragma clang diagnostic pop", "#endif"); } // Verify that enums always have nullability completeness suppressed. public void testEnumNullabilityPragmas() throws IOException { options.setNullability(true); String translation = translateSourceFile("enum Test { A, B, C; }", "Test", "Test.h"); assertTranslatedLines(translation, "#if __has_feature(nullability)", "#pragma clang diagnostic push", "#pragma GCC diagnostic ignored \"-Wnullability-completeness\"", "#endif"); assertTranslatedLines(translation, "#if __has_feature(nullability)", "#pragma clang diagnostic pop", "#endif"); } public void testPropertyNullability() throws IOException { String source = "import javax.annotation.*;" + "import com.google.j2objc.annotations.Property;" + "@ParametersAreNonnullByDefault public class Test {" + " @Nullable @Property String test;" + " @Property String test2;" + " @Property @Nonnull String test3;" + " @Property(\"nonatomic\") String test4;" + " @Property(\"null_resettable\") String test5;" + " @Property(\"null_unspecified\") String test6;" + " @Property int test7;" + " @Property (\"readonly, nonatomic\") double test8;" + "}"; options.setNullability(true); String translation = translateSourceFile(source, "Test", "Test.h"); assertTranslatedLines(translation, "@property (copy, nullable) NSString *test;", "@property (copy, null_resettable) NSString *test2;", "@property (copy, nonnull) NSString *test3;", "@property (copy, nonatomic, null_resettable) NSString *test4;"); // Verify explicit nullability parameters override default. assertTranslatedLines(translation, "@property (copy, null_resettable) NSString *test5;", "@property (copy, null_unspecified) NSString *test6;"); // Verify primitive properties don't have nullability parameters. assertTranslatedLines(translation, "@property jint test7;", "@property (readonly, nonatomic) jdouble test8;"); } public void testFieldWithIntersectionType() throws IOException { String translation = translateSourceFile( "class Test <T extends Comparable & Runnable> { T foo; }", "Test", "Test.h"); // Test that J2OBJC_ARG is used to wrap the type containing a comma. assertTranslation(translation, "J2OBJC_FIELD_SETTER(Test, foo_, J2OBJC_ARG(id<JavaLangComparable, JavaLangRunnable>))"); } public void testSortMethods() throws IOException { String source = "class A {" + "void zebra() {}" + "void gnu(String s, int i, Runnable r) {}" + "A(int i) {}" + "void gnu() {}" + "void gnu(int i, Runnable r) {}" + "void yak() {}" + "A(String s) {}" + "A() {}" + "A(int i, Runnable r) {}" + "void gnu(String s, int i) {}}"; CompilationUnit unit = translateType("A", source); final ArrayList<MethodDeclaration> methods = new ArrayList<>(); unit.accept(new TreeVisitor() { @Override public void endVisit(MethodDeclaration node) { if (!ElementUtil.isSynthetic(node.getExecutableElement())) { methods.add(node); } } }); Collections.sort(methods, TypeDeclarationGenerator.METHOD_DECL_ORDER); assertTrue(methods.get(0).toString().startsWith("<init>()")); assertTrue(methods.get(1).toString().startsWith("<init>(int i)")); assertTrue(methods.get(2).toString().startsWith("<init>(int i,java.lang.Runnable r)")); assertTrue(methods.get(3).toString().startsWith("<init>(java.lang.String s)")); assertTrue(methods.get(4).toString().startsWith("void gnu()")); assertTrue(methods.get(5).toString().startsWith("void gnu(int i,java.lang.Runnable r)")); assertTrue(methods.get(6).toString().startsWith("void gnu(java.lang.String s,int i)")); assertTrue(methods.get(7).toString().startsWith( "void gnu(java.lang.String s,int i,java.lang.Runnable r)")); assertTrue(methods.get(8).toString().startsWith("void yak()")); assertTrue(methods.get(9).toString().startsWith("void zebra()")); } // Verify that an empty statement following a type declaration is ignored. // The JDT parser discards them, while javac includes them in the compilation unit. public void testEmptyStatementsIgnored() throws IOException { String source = "public interface A { void bar(); };"; CompilationUnit unit = translateType("A", source); assertEquals(1, unit.getTypes().size()); } // Verify that a boolean constant initialized with a constant expression like // "true || false" does not cause class initialization code to be generated // for it. public void testBooleanExpressionConstants() throws IOException { String translation = translateSourceFile("class Test {" + " static final boolean FOO = true && false;" + " static final boolean BAR = true || false;" + "}", "Test", "Test.h"); // Verify boolean expressions are simplified. assertTranslation(translation, "#define Test_FOO false"); assertTranslation(translation, "#define Test_BAR true"); // This class should not have an initialize method or reinitialize the constants. assertTranslation(translation, "J2OBJC_EMPTY_STATIC_INIT(Test)"); translation = getTranslatedFile("Test.m"); assertNotInTranslation(translation, "+ (void)initialize {"); assertNotInTranslation(translation, "Test_FOO = false;"); assertNotInTranslation(translation, "Test_BAR = true;"); } }