/* * Copyright 2011 Google Inc. All Rights Reserved. * * 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.translate; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.devtools.j2objc.GenerationTest; import com.google.devtools.j2objc.ast.AbstractTypeDeclaration; import com.google.devtools.j2objc.ast.CompilationUnit; import com.google.devtools.j2objc.ast.TreeUtil; import com.google.devtools.j2objc.ast.TypeDeclaration; import com.google.devtools.j2objc.ast.VariableDeclarationFragment; import com.google.devtools.j2objc.util.ElementUtil; import com.google.devtools.j2objc.util.NameTable; import java.io.IOException; import java.util.List; /** * Unit tests for {@link AnonymousClassConverter}. * * @author Tom Ball */ public class AnonymousClassConverterTest extends GenerationTest { protected List<TypeDeclaration> translateClassBody(String testSource) { String source = "public class Test { " + testSource + " }"; CompilationUnit unit = translateType("Test", source); return Lists.newArrayList(Iterables.filter(unit.getTypes(), TypeDeclaration.class)); } public void testAnonymousClassNaming() throws IOException { String source = "import java.util.*; public class Test { " + "Set keySet() { return new AbstractSet() { " + " public int size() { return 0; }" + " public Iterator iterator() { return new Iterator() {" + " public boolean hasNext() { return false; } " + " public Object next() { return null; }" + " public void remove() {}};}};}" + "Collection values() { return new AbstractCollection() {" + " public int size() { return 0; }" + " public Iterator iterator() { return new Iterator() {" + " public boolean hasNext() { return false; } " + " public Object next() { return null; }" + " public void remove() {}};}};}" + "}"; String impl = translateSourceFile(source, "Test", "Test.m"); assertTranslation(impl, "@interface Test_1_1 : NSObject < JavaUtilIterator >"); assertTranslation(impl, "@interface Test_1 : JavaUtilAbstractSet"); assertTranslation(impl, "@interface Test_2_1 : NSObject < JavaUtilIterator >"); assertTranslation(impl, "@interface Test_2 : JavaUtilAbstractCollection"); } public void testFinalArrayInnerAccess() throws IOException { String source = "public class Test { void foo() { " + "final boolean[] bar = new boolean[1];" + "Runnable r = new Runnable() { public void run() { bar[0] = true; }}; }}"; String impl = translateSourceFile(source, "Test", "Test.m"); assertTranslation(impl, "IOSBooleanArray *val$bar_;"); assertTranslation(impl, "- (instancetype)initWithBooleanArray:(IOSBooleanArray *)capture$0;"); assertTranslation(impl, "IOSBooleanArray *bar = [IOSBooleanArray arrayWithLength:1];"); assertTranslation(impl, "create_Test_1_initWithBooleanArray_(bar)"); assertTranslation(impl, "*IOSBooleanArray_GetRef(nil_chk(val$bar_), 0) = true;"); } /** * Verify that an anonymous class is moved to the compilation unit's types list. */ public void testAnonymousClassExtracted() { List<TypeDeclaration> types = translateClassBody( "Object test() { return new java.util.Enumeration<Object>() { " + "public boolean hasMoreElements() { return false; } " + "public Object nextElement() { return null; } }; }"); assertEquals(2, types.size()); TypeDeclaration type = types.get(1); ElementUtil elementUtil = TreeUtil.getCompilationUnit(type).getEnv().elementUtil(); assertEquals("Test$1", elementUtil.getBinaryName(type.getTypeElement())); } /** * Regression test: verify that a class passed in the constructor of an * anonymous class is converted. */ public void testAnonymousClassWithTypeArgParameter() throws IOException { String translation = translateSourceFile( "class Test { public Test(Class c) {} static Test t = " + "new Test(Test.class) { @Override public int hashCode() { return 1; } }; }", "Test", "Test.m"); assertTranslatedLines(translation, "+ (void)initialize {", "if (self == [Test class]) {", "JreStrongAssignAndConsume(&Test_t, new_Test_1_initWithIOSClass_(Test_class_()));"); } public void testFinalParameter() throws IOException { String translation = translateSourceFile( "class Test { void test(final Object test) {" + " Runnable r = new Runnable() {" + " public void run() {" + " System.out.println(test.toString());" + " } }; } }", "Test", "Test.m"); assertTranslation(translation, "id<JavaLangRunnable> r = create_Test_1_initWithId_(test);"); assertTranslatedLines(translation, "void Test_1_initWithId_(Test_1 *self, id capture$0) {", " JreStrongAssign(&self->val$test_, capture$0);", " NSObject_init(self);", "}"); assertTranslation(translation, "[nil_chk(val$test_) description]"); } public void testFinalLocalVariable() throws IOException { String translation = translateSourceFile( "class Test { void test() {" + " final Object foo = new Object();" + " Runnable r = new Runnable() {" + " public void run() {" + " System.out.println(foo.toString());" + " } }; } }", "Test", "Test.m"); assertTranslation(translation, "id<JavaLangRunnable> r = create_Test_1_initWithId_(foo);"); assertTranslatedLines(translation, "void Test_1_initWithId_(Test_1 *self, id capture$0) {", " JreStrongAssign(&self->val$foo_, capture$0);", " NSObject_init(self);", "}"); assertTranslation(translation, "[nil_chk(val$foo_) description]"); } public void testAnonymousClassInvokingOuterMethod() throws IOException { String translation = translateSourceFile( "class Test { public int getCount() { return 0; } " + "Object test() { return new Object() { " + "int getCount() { return Test.this.getCount(); } }; } }", "Test", "Test.m"); assertTranslatedLines(translation, "- (jint)getCount {", "return [this$0_ getCount];"); } public void testAnonymousClassAsInitializer() throws IOException { String translation = translateSourceFile( "import java.util.*; public class Test {" + "private static final Enumeration<?> EMPTY_ENUMERATION = new Enumeration<Object>() {" + " public boolean hasMoreElements() { return false; }" + " public Object nextElement() { throw new NoSuchElementException(); }}; }", "Test", "Test.m"); assertTranslation(translation, "id<JavaUtilEnumeration> Test_EMPTY_ENUMERATION;"); assertTranslatedLines(translation, "+ (void)initialize {", "if (self == [Test class]) {", "JreStrongAssignAndConsume(&Test_EMPTY_ENUMERATION, new_Test_1_init());"); } public void testFinalParameterAccess() throws IOException { String source = "class Test { Object bar; void foo(final Object bar_) {" + "Runnable r = new Runnable() { public void run() { log(1, bar_); }};" + "log(2, bar_); }" + "private void log(int i, Object o) {}}"; String translation = translateSourceFile(source, "Test", "Test.m"); // Test.foo(): since the bar_ parameter shadows a field, the parameter // gets renamed to bar_Arg. assertTranslation(translation, "- (void)fooWithId:(id)bar_Arg {"); assertTranslation(translation, "Test_logWithInt_withId_(self, 2, bar_Arg);"); // Test_$: since bar_ is an unshadowed field, the parameter name is // unchanged. assertTranslation(translation, "Test_logWithInt_withId_(this$0_, 1, val$bar__);"); assertTranslation(translation, "JreStrongAssign(&self->val$bar__, capture$0);"); } public void testExternalReferenceAsQualifier() throws IOException { String translation = translateSourceFile( "class Test {" + " class Foo { int i = 0; } " + " void bar() { " + " final Foo foo = new Foo(); " + " Runnable run = new Runnable() { public void run() { int j = foo.i; } }; } }", "Test", "Test.m"); assertTranslation(translation, "int j = ((Test_Foo *) nil_chk(val$foo_))->i_"); } public void testMultipleReferencesToSameVar() throws IOException { String translation = translateSourceFile( "class Test {" + " void bar() { " + " final Integer i = new Integer(0); " + " Runnable run = new Runnable() { public void run() { int j = i + i; } }; } }", "Test", "Test.m"); assertTranslation(translation, "initWithJavaLangInteger:(JavaLangInteger *)capture$0 {"); } public void testFinalVarInEnhancedForStatement() throws IOException { String source = "public class Test { " + "void foo(java.util.List<String> strings) { " + " for (final String s : strings) { " + " Runnable r = new Runnable() {" + " public void run() {" + " System.out.println(s);" + " }" + " }; " + " }}}"; String translation = translateSourceFile(source, "Test", "Test.m"); assertTranslation(translation, "[((JavaIoPrintStream *) nil_chk(JreLoadStatic(JavaLangSystem, out))) " + "printlnWithNSString:val$s_];"); } public void testMethodVarInNestedAnonymousClass() throws IOException { String source = "class Test { " + " void bar() { " + " Runnable r1 = new Runnable() { " + " public void run() { " + " final Integer i = 1; " + " Runnable r2 = new Runnable() { " + " public void run() { int j = i + 1; } }; } }; } }"; // Verify method var in r1.run() isn't mistakenly made a field in r1. CompilationUnit unit = translateType("Test", source); NameTable nameTable = unit.getEnv().nameTable(); List<AbstractTypeDeclaration> types = unit.getTypes(); AbstractTypeDeclaration r1 = types.get(1); assertEquals("Test_1", nameTable.getFullName(r1.getTypeElement())); for (VariableDeclarationFragment var : TreeUtil.getAllFields(r1)) { if (ElementUtil.getName(var.getVariableElement()).equals("val$i")) { fail("found field that shouldn't be declared"); } } // Method var in r1.run() becomes a field in r2. AbstractTypeDeclaration r2 = types.get(2); assertEquals("Test_1_1", nameTable.getFullName(r2.getTypeElement())); boolean found = false; for (VariableDeclarationFragment var : TreeUtil.getAllFields(r2)) { if (ElementUtil.getName(var.getVariableElement()).equals("val$i")) { found = true; } } assertTrue("required field not found", found); // Verify constructor takes both outer field and var. String translation = generateFromUnit(unit, "Test.m"); assertTranslation(translation, "r2 = create_Test_1_1_initWithJavaLangInteger_(i)"); } public void testMethodVarInAnonymousClass() throws IOException { String source = "class Test { " + " boolean debug;" + " void foo() { " + " if (true) {" + " if (debug) {" + " final Integer i = 1;" + " Runnable r = new Runnable() { " + " public void run() { int j = i + 1; } }; }}}}"; // Verify method var in r1.run() isn't mistakenly made a field in r1. CompilationUnit unit = translateType("Test", source); NameTable nameTable = unit.getEnv().nameTable(); List<AbstractTypeDeclaration> types = unit.getTypes(); AbstractTypeDeclaration r1 = types.get(1); assertEquals("Test_1", nameTable.getFullName(r1.getTypeElement())); boolean found = false; for (VariableDeclarationFragment var : TreeUtil.getAllFields(r1)) { if (ElementUtil.getName(var.getVariableElement()).equals("val$i")) { found = true; } } assertTrue("required field not found", found); // Verify method var is passed to constructor. String translation = generateFromUnit(unit, "Test.m"); assertTranslation(translation, "r = create_Test_1_initWithJavaLangInteger_(i)"); } public void testMethodVarInSwitch() throws IOException { String source = "class Test { " + " enum E { ONE, TWO };" + " void foo(E e) { " + " switch (e) {" + " case ONE: {" + " final Integer i = 1;" + " Runnable r = new Runnable() { " + " public void run() { int j = i + 1; } }; }}}}"; // Verify method var in r1.run() isn't mistakenly made a field in r1. CompilationUnit unit = translateType("Test", source); NameTable nameTable = unit.getEnv().nameTable(); List<AbstractTypeDeclaration> types = unit.getTypes(); AbstractTypeDeclaration r1 = types.get(2); assertEquals("Test_1", nameTable.getFullName(r1.getTypeElement())); boolean found = false; for (VariableDeclarationFragment var : TreeUtil.getAllFields(r1)) { if (ElementUtil.getName(var.getVariableElement()).equals("val$i")) { found = true; } } assertTrue("required field not found", found); // Verify method var is passed to constructor. String translation = generateFromUnit(unit, "Test.m"); assertTranslation(translation, "r = create_Test_1_initWithJavaLangInteger_(i)"); } public void testAnonymousClassField() throws IOException { String source = "class Test { " + " void bar() { " + " Runnable r1 = new Runnable() { " + " int j = 0; " + " public void run() { " + " final Integer i = 1; " + " Runnable r2 = new Runnable() { " + " public void run() { j = i + 1; } }; } }; } }"; String translation = translateSourceFile(source, "Test", "Test.m"); assertTranslation(translation, "this$0_->j_ = [((JavaLangInteger *) nil_chk(val$i_)) intValue] + 1;"); } public void testEnumConstantAnonymousClassNaming() throws IOException { String source = "public enum Test { " + "UP { public boolean isUp() { return true; }}," + "DOWN { public boolean isUp() { return false; }};" + "public abstract boolean isUp(); }"; String impl = translateSourceFile(source, "Test", "Test.m"); assertTranslation(impl, "@interface Test_1 : Test"); assertTranslation(impl, "@interface Test_2 : Test"); assertTranslation(impl, "Test_initWithNSString_withInt_(self, __name, __ordinal);"); assertTranslation(impl, "Test_1_initWithNSString_withInt_(e, @\"UP\", 0);"); assertTranslation(impl, "Test_2_initWithNSString_withInt_(e, @\"DOWN\", 1);"); } public void testTwoOutersInAnonymousSubClassOfInner() throws IOException { String translation = translateSourceFile("class Test { " + " class B { class Inner { Inner(int i) { } } } " + " class A {" + " B outerB;" + " public B.Inner foo(final B b) {" + " return b.new Inner(1) { public boolean bar() { return b.equals(outerB); } }; } } " + "}", "Test", "Test.m"); assertTranslation(translation, "create_Test_A_1_initWithTest_A_withTest_B_withTest_B_withInt_(self, b, b, 1)"); String param0 = options.isJDT() ? "param0" : "i"; String superOuter = options.isJDT() ? "superOuter$" : "x0"; assertTranslatedLines(translation, "void Test_A_1_initWithTest_A_withTest_B_withTest_B_withInt_(" + "Test_A_1 *self, Test_A *outer$, Test_B *capture$0, Test_B *" + superOuter + ", " + "jint " + param0 + ") {", " JreStrongAssign(&self->this$1_, outer$);", " JreStrongAssign(&self->val$b_, capture$0);", " Test_B_Inner_initWithTest_B_withInt_(self, nil_chk(" + superOuter + "), " + param0 + ");", "}"); } public void testAnonymousClassInStaticBlock() throws IOException { String translation = translateSourceFile("class Test { " + " static class A {" + " static abstract class Inner { Inner(int i) { } abstract int foo(); } } " + " static A.Inner inner = new A.Inner(1) { int foo() { return 2; } }; }", "Test", "Test.m"); // This is probably not the right output - but it compiles and works. assertTranslation(translation, "new_Test_1_initWithInt_(1)"); } public void testAnonymousClassObjectParameter() throws IOException { String translation = translateSourceFile("class Test {" + " abstract static class A { " + " A(Object o) { } " + " abstract void foo();" + " } " + " void bar(Object o) { A a = new A(o) { void foo() { } }; } }", "Test", "Test.h"); assertTranslation(translation, "- (instancetype)initWithId:"); } public void testEnumWithParametersAndInnerClasses() throws IOException { String impl = translateSourceFile( "public enum Color { " + "RED(42) { public int getRGB() { return 0xF00; }}, " + "GREEN(-1) { public int getRGB() { return 0x0F0; }}, " + "BLUE(666) { public int getRGB() { return 0x00F; }};" + "Color(int n) {} public int getRGB() { return 0; }}", "Color", "Color.m"); // Verify Color constructor. assertTranslatedLines(impl, "void Color_initWithInt_withNSString_withInt_(" + "Color *self, jint n, NSString *__name, jint __ordinal) {", " JavaLangEnum_initWithNSString_withInt_(self, __name, __ordinal);", "}"); String param0 = options.isJDT() ? "param0" : "n"; // Verify Color_1 constructor. assertTranslatedLines(impl, "void Color_1_initWithInt_withNSString_withInt_(" + "Color_1 *self, jint " + param0 + ", NSString *__name, jint __ordinal) {", " Color_initWithInt_withNSString_withInt_(self, " + param0 + ", __name, __ordinal);", "}"); // Verify constant initialization. assertTranslation(impl, "Color_1_initWithInt_withNSString_withInt_(e, 42, @\"RED\", 0)"); } public void testEnumWithInnerEnum() throws IOException { String impl = translateSourceFile( "public enum OuterValue {\n" + " VALUE1, VALUE2, VALUE3;\n" + " public enum InnerValue {\n" + " VALUE1, VALUE2, VALUE3;\n" + " }\n" + "}\n", "OuterValue", "OuterValue.m"); // Verify OuterValue constant initialization. assertTranslation(impl, "OuterValue_initWithNSString_withInt_(e, names[i], i);"); // Verify InnerValue constant initialization. assertTranslation(impl, "OuterValue_InnerValue_initWithNSString_withInt_(e, names[i], i);"); } // Tests a field initialized with an anonymous class and multiple // constructors. Field initialization is moved to the constructors, // duplicating the initialization statement, but we do not want to duplicate // the implementation. public void testAnonymousClassNotDuplicated() throws IOException { String impl = translateSourceFile( "public class A { " + " interface I { public int getInt(); } " + " private I my_i = new I() { public int getInt() { return 42; } }; " + " A() {} " + " A(String foo) {} }", "A", "A.m"); assertOccurrences(impl, "@implementation A_1", 1); assertOccurrences(impl, "JreStrongAssignAndConsume(&self->my_i_, new_A_1_init());", 2); } public void testNestedAnonymousClasses() throws IOException { String impl = translateSourceFile( "class Test { void test(final int i) { Runnable r = new Runnable() { " + "public void run() { Runnable r2 = new Runnable() { public void run() { " + "int i2 = i; } }; } }; } }", "Test", "Test.m"); assertTranslation(impl, "int i2 = this$0_->val$i_;"); } // Verify that an anonymous class can be defined with a null constructor // parameter. public void testDefaultConstructorWithNullParameter() throws IOException { translateSourceFile( "class Test {" + " static Test instance = new Test(null) {};" + " protected Test(String s) {} }", "Test", "Test.m"); // The test is successful if the above doesn't throw an NPE. } public void testAnonymousClassWithGenericConstructor() throws IOException { String translation = translateSourceFile( "class Test<T> { Test(T t) {} void test() { new Test<String>(\"foo\") {}; } }", "Test", "Test.m"); String param0 = options.isJDT() ? "param0" : "t"; assertTranslation(translation, "create_Test_1_initWithNSString_(@\"foo\")"); assertTranslation(translation, "- (instancetype)initWithNSString:(NSString *)" + param0 + " {"); assertTranslatedLines(translation, "void Test_1_initWithNSString_(Test_1 *self, NSString *" + param0 + ") {", " Test_initWithId_(self, " + param0 + ");", "}"); } public void testAnonymousClassWithVarargsConstructor() throws IOException { String translation = translateSourceFile( "class Test { Test(String fmt, Object... args) {} " + " void test() { new Test(\"%s %s\", \"1\", \"2\") {}; } " + " void test2() { new Test(\"foo\") {}; } }", "Test", "Test.m"); // check the invocations. assertTranslation(translation, "create_Test_1_initWithNSString_withNSObjectArray_(@\"%s %s\", " + "[IOSObjectArray arrayWithObjects:(id[]){ @\"1\", @\"2\" } count:2 " + "type:NSObject_class_()]);"); assertTranslation(translation, "create_Test_2_initWithNSString_withNSObjectArray_(@\"foo\", " + "[IOSObjectArray arrayWithLength:0 type:NSObject_class_()]);"); String param0 = options.isJDT() ? "param0" : "fmt"; String param1 = options.isJDT() ? "param1" : "args"; // check the generated constructors. assertTranslatedLines(translation, "void Test_1_initWithNSString_withNSObjectArray_(" + "Test_1 *self, NSString *" + param0 + ", IOSObjectArray *" + param1 + ") {", " Test_initWithNSString_withNSObjectArray_(self, " + param0 + ", " + param1 + ");", "}"); assertTranslatedLines(translation, "void Test_2_initWithNSString_withNSObjectArray_(" + "Test_2 *self, NSString *" + param0 + ", IOSObjectArray *" + param1 + ") {", " Test_initWithNSString_withNSObjectArray_(self, " + param0 + ", " + param1 + ");", "}"); } public void testAnonymousClassWithinLambdaWithSuperOuterParam() throws IOException { String translation = translateSourceFile( "class Test { interface I { A get(); } class A {} " + "static class B { String s; I test(Test t, int i) { " + "return () -> t.new A() { public String toString() { return s + i; } }; } } }", "Test", "Test.m"); String superOuter = options.isJDT() ? "superOuter$" : "x0"; assertTranslation(translation, "static Test_B_1 *create_Test_B_1_initWithTest_B_withInt_withTest_(" + "Test_B *outer$, jint capture$0, Test *" + superOuter + ");"); assertTranslation(translation, "return create_Test_B_1_initWithTest_B_withInt_withTest_(this$0_, val$i_, val$t_);"); // The super outer must be nil_chk'ed in the anonymous constructor. assertTranslation(translation, "Test_A_initWithTest_(self, nil_chk(" + superOuter + "));"); } public void testSuperclassHasCapturedVariables() throws IOException { String translation = translateSourceFile( "class Test { static Object test(int i) { class Local { int foo() { return i; } } " + "return new Local() { int bar() { return i; } }; } }", "Test", "Test.m"); // Test that the anonymous class captures i and passes it to Local's constructor. assertTranslatedLines(translation, "void Test_1_initWithInt_(Test_1 *self, jint capture$0) {", " self->val1$i_ = capture$0;", " Test_1Local_initWithInt_(self, capture$0);", "}"); } public void testGenericConstructorCalledByAnonymousClass() throws IOException { if (!options.isJDT()) { // JDT fails with "could not find constructor". String translation = translateSourceFile( "class Test { <T> Test(T t) {} Test create() { return new Test(\"foo\") {}; } }", "Test", "Test.m"); assertTranslatedLines(translation, "void Test_1_initWithId_(Test_1 *self, id t) {", " Test_initWithId_(self, t);", "}"); } } }