/* * Copyright 2008 Google Inc. * * 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.gwt.dev.jjs.test; import com.google.gwt.core.client.JavaScriptException; import com.google.gwt.junit.client.GWTTestCase; import junit.framework.Assert; import java.util.ArrayList; import java.util.EventListener; /** * Miscellaneous tests of the Java to JavaScript compiler. */ @SuppressWarnings("unused") public class CompilerTest extends GWTTestCase { interface MyMap { Object get(String key); } interface Silly { } interface SillyComparable<T extends Silly> extends Comparable<T> { int compareTo(T obj); } private abstract static class AbstractSuper { public static String foo() { if (FALSE) { // prevent inlining return foo(); } return "AbstractSuper"; } } private abstract static class Apple implements Fruit { } private abstract static class Bm2BaseEvent { } private abstract static class Bm2ComponentEvent extends Bm2BaseEvent { } private static class Bm2KeyNav<E extends Bm2ComponentEvent> implements Bm2Listener<E> { public int handleEvent(Bm2ComponentEvent ce) { return 5; } } private interface Bm2Listener<E extends Bm2BaseEvent> extends EventListener { int handleEvent(E be); } /** * Used in {@link #testSwitchOnEnumTypedThis()}. */ private static enum ChangeDirection { NEGATIVE, POSITIVE; public String getText() { switch (this) { case POSITIVE: return "POSITIVE"; case NEGATIVE: return "NEGATIVE"; default: throw new IllegalArgumentException("Unhandled change direction: " + this); } } } private static class ConcreteSub extends AbstractSuper { public static String foo() { if (FALSE) { // prevent inlining return foo(); } return "ConcreteSub"; } } private static interface Fruit { } private static class Fuji extends Apple { } private static class Granny extends Apple { } private static class NonSideEffectCauser { public static final String NOT_A_COMPILE_TIME_CONSTANT = null; } private static class SideEffectCauser { private static Object instance = new Object(); static { CompilerTest.sideEffectChecker++; } public static Object causeClinitSideEffect() { return instance; } } private static class SideEffectCauser2 { static { CompilerTest.sideEffectChecker++; } public static Object causeClinitSideEffect() { return null; } } private static class SideEffectCauser3 { static { CompilerTest.sideEffectChecker++; } public static void causeClinitSideEffect() { } } private static class SideEffectCauser4 { public static String causeClinitSideEffectOnRead = "foo"; static { CompilerTest.sideEffectChecker++; } } private static class SideEffectCauser5 { public static String causeClinitSideEffectOnRead = "bar"; static { CompilerTest.sideEffectChecker++; } } private static class SideEffectCauser6 extends SideEffectCauser6Super { public static String causeClinitSideEffectOnRead = "bar"; } private static class SideEffectCauser6Super { static { CompilerTest.sideEffectChecker++; } } /** * Ensures that a superclass's clinit is run before supercall arguments are * evaluated. */ private static class SideEffectCauser7 extends SideEffectCauser7Super { public SideEffectCauser7() { super(SideEffectCauser7Super.SHOULD_BE_TRUE); } } private static class SideEffectCauser7Super { public static boolean SHOULD_BE_TRUE = false; static { SHOULD_BE_TRUE = true; } public SideEffectCauser7Super(boolean should_be_true) { if (should_be_true) { CompilerTest.sideEffectChecker++; } } } /** * Used in test {@link #testPrivateOverride()}. */ private static class TpoChild extends TpoParent { @Override public int foo() { return callSuper(); } private int callSuper() { return super.foo(); } } /** * Used in test {@link #testPrivateOverride()}. */ private static class TpoGrandparent { public int foo() { return 0; } } /** * Used in test {@link #testPrivateOverride()}. */ private static class TpoJsniChild extends TpoJsniParent { @Override public native int foo() /*-{ return this.@com.google.gwt.dev.jjs.test.CompilerTest$TpoJsniChild::callSuper()(); }-*/; private int callSuper() { return super.foo(); } } /** * Used in test {@link #testPrivateOverride()}. */ private static class TpoJsniGrandparent { public int foo() { return 0; } } /** * Used in test {@link #testPrivateOverride()}. */ private static class TpoJsniParent extends TpoJsniGrandparent { @Override public native int foo() /*-{ // This should call callSuper in TpoJsniParent, not the one // in TpoJsniChild return this.@com.google.gwt.dev.jjs.test.CompilerTest$TpoJsniParent::callSuper()(); }-*/; private int callSuper() { return super.foo(); } } /** * Used in test {@link #testPrivateOverride()}. */ private static class TpoParent extends TpoGrandparent { @Override public int foo() { // This should call the callSuper in TpoJsniParent, not the one // in TpoJsniChild return callSuper(); } private int callSuper() { return super.foo(); } } private static final class UninstantiableType { public Object field; public int intField; private UninstantiableType() { } public int returnInt() { return intField; } public Object returnNull() { return null; } } private static volatile boolean FALSE = false; private static int sideEffectChecker; private static volatile int THREE = 3; private static volatile boolean TRUE = true; private static volatile boolean volatileBoolean; private static volatile int volatileInt; private static volatile long volatileLong; private static volatile UninstantiableType volatileUninstantiableType; private static native void accessUninstantiableField(UninstantiableType u) /*-{ u.@com.google.gwt.dev.jjs.test.CompilerTest$UninstantiableType::field.toString(); }-*/; private static native void accessUninstantiableMethod(UninstantiableType u) /*-{ u.@com.google.gwt.dev.jjs.test.CompilerTest$UninstantiableType::returnNull()(); }-*/; private static String barShouldInline() { return "bar"; } private static void foo(String string) { } private static void foo(Throwable throwable) { } private static native String jsniReadSideEffectCauser5() /*-{ return @com.google.gwt.dev.jjs.test.CompilerTest$SideEffectCauser5::causeClinitSideEffectOnRead; }-*/; private Integer boxedInteger = 0; @Override public String getModuleName() { return "com.google.gwt.dev.jjs.CompilerSuite"; } public void testArrayAccessSideEffect() { int index = 1; int[] array = null; try { // index should be set before exception is thrown array[index = 2]++; fail("null reference expected"); } catch (Exception e) { // expected } assertEquals(2, index); } public void testArrayStore() { Object[][] oaa; oaa = new Object[4][4]; oaa[0][0] = "foo"; assertEquals(oaa[0][0], "foo"); oaa = new Object[4][]; oaa[0] = new Object[4]; oaa[0][0] = "bar"; assertEquals(oaa[0][0], "bar"); Apple[] apple = TRUE ? new Granny[3] : new Apple[3]; Apple g = TRUE ? (Apple) new Granny() : (Apple) new Fuji(); Apple a = apple[0] = g; assertEquals(g, a); byte[] bytes = new byte[10]; bytes[0] = (byte) '1'; assertEquals(49, bytes[0]); } /** * Issue 3064: when the implementation of an interface comes from a * superclass, it can be necessary to add a bridge method that overrides the * interface method and calls the inherited method. */ public void testBridgeMethods1() { abstract class AbstractFoo { public int compareTo(AbstractFoo o) { return 0; } } class MyFoo extends AbstractFoo implements Comparable<AbstractFoo> { } /* * This subclass adds an extra curve ball: only one bridge method should be * created, in class MyFoo. MyFooSub should not get its own but instead use * the inherited one. Otherwise, two final methods with identical signatures * would override each other. */ class MyFooSub extends MyFoo { } Comparable<AbstractFoo> comparable1 = new MyFooSub(); assertEquals(0, comparable1.compareTo(new MyFoo())); Comparable<AbstractFoo> comparable2 = new MyFoo(); assertEquals(0, comparable2.compareTo(new MyFooSub())); } /** * Issue 3304. Doing superclasses first is tricky when the superclass is a raw * type. */ @SuppressWarnings("unchecked") public void testBridgeMethods2() { Bm2KeyNav<?> obs = new Bm2KeyNav() { }; assertEquals(5, obs.handleEvent(null)); } /** * When adding a bridge method, be sure to handle transitive overrides * relationships. */ public void testBridgeMethods3() { class AbstractFoo implements Silly { public int compareTo(AbstractFoo obj) { if (FALSE) { return compareTo(obj); } return 0; } } class MyFoo extends AbstractFoo implements SillyComparable<AbstractFoo> { } assertEquals(0, new MyFoo().compareTo(new MyFoo())); } /** * Issue 3517. Sometimes JDT adds a bridge method when a subclass's method * differs only by return type. In some versions of GWT, this has resulted in * a bridge method overriding its own target, and eventually TypeTightener * producing an infinite recursion. */ public void testBridgeMethods4() { abstract class MyMapAbstract<V> implements MyMap { public String get(String key) { return null; } } final class MyMapImpl<V> extends MyMapAbstract<V> { } MyMapImpl<String> mmap = new MyMapImpl<String>(); assertNull(mmap.get("foo")); } public void testCastOptimizer() { Granny g = new Granny(); Apple a = g; Fruit f = g; a = (Apple) f; g = (Granny) a; g = (Granny) f; } public void testClassLiterals() { if (Object.class.getName().startsWith("Class$")) { // If class metadata is disabled return; } assertEquals("void", void.class.toString()); assertEquals("int", int.class.toString()); assertEquals("class java.lang.String", String.class.toString()); assertEquals("class com.google.gwt.dev.jjs.test.CompilerTest", CompilerTest.class.toString()); assertEquals( "class com.google.gwt.dev.jjs.test.CompilerTest$UninstantiableType", UninstantiableType.class.toString()); assertEquals("interface com.google.gwt.dev.jjs.test.CompilerTest$Fruit", Fruit.class.toString()); assertEquals("class [I", int[].class.toString()); assertEquals("class [Ljava.lang.String;", String[].class.toString()); assertEquals("class [Lcom.google.gwt.dev.jjs.test.CompilerTest;", CompilerTest[].class.toString()); assertEquals( "class [Lcom.google.gwt.dev.jjs.test.CompilerTest$UninstantiableType;", UninstantiableType[].class.toString()); assertEquals("class [Lcom.google.gwt.dev.jjs.test.CompilerTest$Fruit;", Fruit[].class.toString()); } public void testClinitSideEffectInlining() { sideEffectChecker = 0; SideEffectCauser.causeClinitSideEffect(); assertEquals(1, sideEffectChecker); SideEffectCauser2.causeClinitSideEffect(); assertEquals(2, sideEffectChecker); SideEffectCauser3.causeClinitSideEffect(); assertEquals(3, sideEffectChecker); String foo = SideEffectCauser4.causeClinitSideEffectOnRead; assertEquals(4, sideEffectChecker); jsniReadSideEffectCauser5(); assertEquals(5, sideEffectChecker); foo = SideEffectCauser6.causeClinitSideEffectOnRead; assertEquals(6, sideEffectChecker); new SideEffectCauser7(); assertEquals(7, sideEffectChecker); String checkRescued = NonSideEffectCauser.NOT_A_COMPILE_TIME_CONSTANT; assertEquals(null, checkRescued); } public void testConditionals() { assertTrue(TRUE ? TRUE : FALSE); assertFalse(FALSE ? TRUE : FALSE); assertFalse(TRUE ? FALSE : TRUE); assertTrue(FALSE ? FALSE : TRUE); assertTrue(true ? TRUE : FALSE); assertFalse(false ? TRUE : FALSE); assertFalse(true ? FALSE : TRUE); assertTrue(false ? FALSE : TRUE); assertTrue(TRUE ? true : FALSE); assertFalse(FALSE ? true : FALSE); assertFalse(TRUE ? false : TRUE); assertTrue(FALSE ? false : TRUE); assertTrue(TRUE ? TRUE : false); assertFalse(FALSE ? TRUE : false); assertFalse(TRUE ? FALSE : true); assertTrue(FALSE ? FALSE : true); } public void testDeadCode() { while (returnFalse()) { break; } do { break; } while (false); do { break; } while (returnFalse()); for (; returnFalse();) { } boolean check = false; for (check = true; returnFalse(); fail()) { fail(); } assertTrue(check); if (returnFalse()) { fail(); } else { } if (!returnFalse()) { } else { fail(); } // For these following tests, make sure that side effects in conditions // get propagated, even if they cause introduction of dead code. // boolean b = false; if ((b = true) ? true : true) { } assertTrue(b); boolean c = true; int val = 0; for (val = 1; c = false; ++val) { } assertFalse(c); boolean d = true; while (d = false) { } assertFalse(d); boolean e = true; if (true | (e = false)) { } assertFalse(e); } /** * Test that code considered unreachable by the JDT does not crash the GWT * compiler. */ public void testDeadCode2() { class SillyList { } final SillyList outerLocalVariable = new SillyList(); new SillyList() { private void pretendToUse(SillyList x) { } void blah() { if (true) { throw new RuntimeException(); } /* * This code is unreachable, and so outerLocalVariable is never actually * read by reachable code. */ pretendToUse(outerLocalVariable); } }; } public void testDeadTypes() { if (false) { new Object() { }.toString(); class Foo { void a() { } } new Foo().a(); } } /** * Make sure that the compiler does not crash itself on user code that divides * by zero. The actual behavior varies by the numeric type and whether it is * Development Mode or Production Mode, but the important thing is that the * compiler does not crash. */ @SuppressWarnings("divzero") public void testDivByZero() { assertTrue(Double.isNaN(0.0 / 0.0)); try { // 0 / 0 is currently 0 in Production Mode. assertEquals(0, 0 / 0); } catch (ArithmeticException expected) { // expected in Development Mode } try { volatileLong = 0L / 0; fail("expected an ArithmeticException"); } catch (ArithmeticException expected) { // expected } assertTrue(Double.isNaN(0.0 % 0.0)); try { // 0 % 0 is currently NaN in Production Mode. assertTrue(Double.isNaN(0 % 0)); } catch (ArithmeticException expected) { // expected in Development Mode } try { volatileLong = 0L % 0; fail("expected an ArithmeticException"); } catch (ArithmeticException expected) { // expected } } public void testEmptyBlockStatements() { boolean b = false; while (b) { } do { } while (b); for (; b;) { } for (;;) { break; } if (b) { } if (b) { } else { b = false; } if (b) { } else { } } public native void testEmptyBlockStatementsNative() /*-{ var b = false; while (b) { } do { } while (b); for (; b; ) { } for (;;) { break; } if (b) { } if (b) { } else { b = false; } if (b) { } else { } }-*/; // CHECKSTYLE_OFF @SuppressWarnings("empty") public void testEmptyStatements() { boolean b = false; while (b); do; while (b); for (; b;); for (int i = 0; i < 10; ++i); for (int i : new int[10]); for (Integer i : new ArrayList<Integer>()); for (;;) break; if (b) ; if (b) ; else b = false; if (b) ; else ; } // CHECKSTYLE_ON public native void testEmptyStatementsNative() /*-{ var b = false; while (b); do; while (b); for (; b;); for (;;) break; if (b) ; if (b) ; else b = false; if (b) ; else ; }-*/; public void testEmptyTryBlock() { int x = 0; try { } finally { x = 1; } assertEquals(1, x); } public void testForStatement() { { int i; for (i = 0; i < 10; ++i) { } assertEquals(i, 10); } { int i, c; for (i = 0, c = 10; i < c; ++i) { } assertEquals(i, 10); assertEquals(c, 10); } { int j = 0; for (int i = 0; i < 10; ++i) { ++j; } assertEquals(j, 10); } { int j = 0; for (int i = 0, c = 10; i < c; ++i) { ++j; } assertEquals(j, 10); } } /** * Issue #615: Internal Compiler Error. */ public void testImplicitNull() { boolean b; String test = ((((b = true) ? null : null) + " ") + b); assertTrue(b); assertEquals("null true", test); } /** * Issue 2886: inlining should cope with local variables that do not have an * explicit declaration node. */ public void testInliningBoxedIncrement() { // should not actually inline, because it has a temp variable incrementBoxedInteger(); assertEquals((Integer) 1, boxedInteger); } public void testJavaScriptReservedWords() { boolean delete = TRUE; for (int in = 0; in < 10; ++in) { assertTrue(in < 10); assertTrue(delete); } } public void testLabels() { int i = 0, j = 0; outer : for (i = 0; i < 1; ++i) { inner : for (j = 0; j < 1; ++j) { break outer; } fail(); } assertEquals(0, i); assertEquals(0, j); outer : for (i = 0; i < 1; ++i) { inner : for (j = 0; j < 1; ++j) { continue outer; } fail(); } assertEquals(1, i); assertEquals(0, j); outer : for (i = 0; i < 1; ++i) { inner : for (j = 0; j < 1; ++j) { break inner; } } assertEquals(1, i); assertEquals(0, j); outer : for (i = 0; i < 1; ++i) { inner : for (j = 0; j < 1; ++j) { continue inner; } } assertEquals(1, i); assertEquals(1, j); /* * Issue 2069: a default with a break should not be stripped unless the * break has no label. */ outer : while (true) { switch (THREE) { case 0: case 1: break; default: break outer; } fail("should not be reached"); } /* * Issue 2770: labeled breaks in a switch statement with no default case * should not be pruned. */ outer : while (true) { switch (THREE) { case 0: case 1: case 2: case 3: break outer; case 4: case 5: break; } } } public void testLocalClasses() { class Foo { public Foo(int j) { assertEquals(1, j); }; } final int i; new Foo(i = 1) { { assertEquals(1, i); } }; assertEquals(1, i); } public void testLocalRefs() { final String foo = TRUE ? "foo" : "bar"; final String bar = TRUE ? "bar" : "foo"; String result = new Object() { private String a = foo; { a = foo; } @Override public String toString() { return new Object() { private static final String constantString = "wallawalla"; private String ai = foo; { ai = foo; } @Override public String toString() { // this line used to cause ICE due to no synthetic path to bar bar.valueOf(false); assertEquals("wallawalla", constantString); return foo + a + ai; } }.toString() + a; } }.toString(); assertEquals(result, "foofoofoofoo"); } public void testNotOptimizations() { assertFalse(!true); assertTrue(!false); assertTrue(!(TRUE == FALSE)); assertFalse(!(TRUE != FALSE)); assertFalse(!(3 < 4)); assertFalse(!(3 <= 4)); assertTrue(!(3 > 4)); assertTrue(!(3 >= 4)); assertTrue(!(4 < 3)); assertTrue(!(4 <= 3)); assertFalse(!(4 > 3)); assertFalse(!(4 >= 3)); assertTrue(!!TRUE); assertFalse(!!FALSE); } public void testNullFlow() { UninstantiableType f = null; try { f.returnNull().toString(); fail(); } catch (NullPointerException e) { // Development Mode } catch (JavaScriptException e) { // Production Mode } try { UninstantiableType[] fa = null; fa[4] = null; fail(); } catch (NullPointerException e) { // Development Mode } catch (JavaScriptException e) { // Production Mode } } public void testNullFlowArray() { UninstantiableType[] uta = new UninstantiableType[10]; assertEquals(uta.length, 10); assertEquals(uta[0], null); uta[1] = null; assertEquals(uta[1], null); } public void testNullFlowOverloads() { foo((Throwable) null); foo((String) null); } public void testNullFlowVsClassCastPrecedence() { try { ((UninstantiableType) new Object()).returnNull(); fail(); } catch (ClassCastException e) { // success } } public void testOuterSuperThisRefs() { new B(); } /** * Test that calling a private instance method does not accidentally call * another private method that appears to override it. Private methods don't * truly override each other. */ public void testPrivateOverride() { assertEquals(0, new TpoChild().foo()); assertEquals(0, new TpoJsniChild().foo()); } public void testReturnStatementInCtor() { class Foo { int i; Foo(int i) { this.i = i; if (i == 0) { return; } else if (i == 1) { return; } return; } } assertEquals(new Foo(0).i, 0); assertEquals(new Foo(1).i, 1); assertEquals(new Foo(2).i, 2); } public void testStaticMethodResolution() { // Issue 2922 assertEquals("AbstractSuper", AbstractSuper.foo()); } public void testStringOptimizations() { assertEquals("Herro, AJAX", "Hello, AJAX".replace('l', 'r')); assertEquals('J', "Hello, AJAX".charAt(8)); assertEquals(11, "Hello, AJAX".length()); assertFalse("Hello, AJAX".equals("me")); assertTrue("Hello, AJAX".equals("Hello, AJAX")); assertTrue("Hello, AJAX".equalsIgnoreCase("HELLO, ajax")); assertEquals("hello, ajax", "Hello, AJAX".toLowerCase()); assertEquals("foobar", "foo" + barShouldInline()); assertEquals("1bar", 1 + barShouldInline()); assertEquals("fbar", 'f' + barShouldInline()); assertEquals("truebar", true + barShouldInline()); assertEquals("3.5bar", 3.5 + barShouldInline()); assertEquals("3.5bar", 3.5f + barShouldInline()); assertEquals("27bar", 27L + barShouldInline()); assertEquals("nullbar", null + barShouldInline()); } public void testSubclassStaticInnerAndClinitOrdering() { new CheckSubclassStaticInnerAndClinitOrdering(); } public void testSwitchOnEnumTypedThis() { assertEquals("POSITIVE", ChangeDirection.POSITIVE.getText()); assertEquals("NEGATIVE", ChangeDirection.NEGATIVE.getText()); } public void testSwitchStatement() { switch (0) { case 0: // Once caused an ICE. int test; break; } } /** * Tests cases where the compiler will convert to a simple if or block. */ public void testSwitchStatementConversions() { int i = 1; switch (i) { case 1: i = 2; } assertEquals(2, i); switch (i) { case 1: i = 3; } assertEquals(2, i); switch (i) { default: i = 3; } assertEquals(3, i); } public void testSwitchStatementEmpty() { switch (0) { } } public void testSwitchStatementFallthroughs() { int i = 1; switch (i) { case 1: i = 2; // fallthrough case 2: break; case 3: fail("Shouldn't get here"); } assertEquals(2, i); switch (i) { case 1: break; case 2: i = 3; break; case 3: break; case 4: fail("Shouldn't get here"); break; } assertEquals(3, i); } public void testSwitchStatementWithUsefulDefault() { switch (1) { case 1: case 2: case 3: { break; } case 4: case 5: case 6: default: fail("Shouldn't get here"); break; } } public void testSwitchStatementWithUselessCases() { switch (1) { case 1: case 2: case 3: { break; } case 4: case 5: case 6: default: break; } } public void testSwitchStatementWontRemoveExpression() { class Foo { boolean setFlag; int setFlag() { setFlag = true; return 3; } } Foo foo = new Foo(); switch (foo.setFlag()) { case 3: break; } // Make sure that compiler didn't optimize away the switch statement's // expression assertTrue(foo.setFlag); } public void testUnaryPlus() { int x, y = -7; x = +y; assertEquals(-7, x); } public void testUninstantiableAccess() { UninstantiableType u = null; try { accessUninstantiableField(u); fail("Expected JavaScriptException"); } catch (JavaScriptException expected) { } try { accessUninstantiableMethod(u); fail("Expected JavaScriptException"); } catch (JavaScriptException expected) { } try { volatileUninstantiableType = (UninstantiableType) (new Object()); fail("Expected ClassCastException"); } catch (ClassCastException expected) { } try { volatileInt = u.intField++; fail("Expected NullPointerException (1)"); } catch (Exception expected) { } try { volatileInt = u.intField--; fail("Expected NullPointerException (2)"); } catch (Exception expected) { } try { volatileInt = ++u.intField; fail("Expected NullPointerException (3)"); } catch (Exception expected) { } try { volatileInt = --u.intField; fail("Expected NullPointerException (4)"); } catch (Exception expected) { } try { u.intField = 0; /* * Currently, null.nullField gets pruned rather than being allowed to * execute its side effect of tripping an NPE. */ // fail("Expected NullPointerException (5)"); } catch (Exception expected) { } try { volatileBoolean = u.intField == 0; fail("Expected NullPointerException (6)"); } catch (Exception expected) { } try { volatileBoolean = u.returnInt() == 0; fail("Expected NullPointerException (7)"); } catch (Exception expected) { } } private void incrementBoxedInteger() { // the following will need a temporary variable created boxedInteger++; } private boolean returnFalse() { return false; } } class A { public abstract class AA { } } class B extends A { { new AA() { }; } } // This construct used to cause an ICE class CheckSubclassStaticInnerAndClinitOrdering extends Outer.StaticInner { private static class Foo { } private static final Foo FOO = new Foo(); public CheckSubclassStaticInnerAndClinitOrdering() { this(FOO); } public CheckSubclassStaticInnerAndClinitOrdering(Foo foo) { // This used to be null due to clinit ordering issues Assert.assertNotNull(foo); } } class Outer { public static class StaticInner { } }