/*
* 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;");
}
}