package scala.tools.nsc package backend.jvm package analysis import org.junit.runner.RunWith import org.junit.runners.JUnit4 import org.junit.Test import scala.tools.asm.Opcodes._ import org.junit.Assert._ import CodeGenTools._ import scala.tools.asm.tree.{AbstractInsnNode, MethodNode} import scala.tools.nsc.backend.jvm.BTypes._ import scala.tools.partest.ASMConverters import ASMConverters._ import scala.tools.testing.ClearAfterClass import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ import AsmUtils._ import scala.collection.convert.decorateAsScala._ object NullnessAnalyzerTest extends ClearAfterClass.Clearable { var noOptCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:none") def clear(): Unit = { noOptCompiler = null } } @RunWith(classOf[JUnit4]) class NullnessAnalyzerTest extends ClearAfterClass { ClearAfterClass.stateToClear = NullnessAnalyzerTest val noOptCompiler = NullnessAnalyzerTest.noOptCompiler def newNullnessAnalyzer(methodNode: MethodNode, classInternalName: InternalName = "C"): NullnessAnalyzer = { val nullnessAnalyzer = new NullnessAnalyzer nullnessAnalyzer.analyze(classInternalName, methodNode) nullnessAnalyzer } def testNullness(analyzer: NullnessAnalyzer, method: MethodNode, query: String, index: Int, nullness: Nullness): Unit = { for (i <- findInstr(method, query)) { val r = analyzer.frameAt(i, method).getValue(index).nullness assertTrue(s"Expected: $nullness, found: $r. At instr ${textify(i)}", nullness == r) } } // debug / helper for writing tests def showAllNullnessFrames(analyzer: NullnessAnalyzer, method: MethodNode): String = { val instrLength = method.instructions.iterator.asScala.map(textify(_).length).max val lines = for (i <- method.instructions.iterator.asScala) yield { val f = analyzer.frameAt(i, method) val frameString = { if (f == null) "null" else (0 until (f.getLocals + f.getStackSize)).iterator .map(f.getValue(_).toString) .map(s => "%8s".format(s)) .zipWithIndex.map({case (s, i) => s"$i: $s"}) .mkString(", ") } ("%"+ instrLength +"s: %s").format(textify(i), frameString) } lines.mkString("\n") } @Test def showNullnessFramesTest(): Unit = { val List(m) = compileMethods(noOptCompiler)("def f = this.toString") // NOTE: the frame for an instruction represents the state *before* executing that instr. // So in the frame for `ALOAD 0`, the stack is still empty. val res = """ L0: 0: NotNull | LINENUMBER 1 L0: 0: NotNull | ALOAD 0: 0: NotNull |INVOKEVIRTUAL java/lang/Object.toString ()Ljava/lang/String;: 0: NotNull, 1: NotNull | ARETURN: 0: NotNull, 1: Unknown1 | L0: null""".stripMargin assertEquals(showAllNullnessFrames(newNullnessAnalyzer(m), m), res) } @Test def thisNonNull(): Unit = { val List(m) = compileMethods(noOptCompiler)("def f = this.toString") val a = newNullnessAnalyzer(m) testNullness(a, m, "ALOAD 0", 0, NotNull) } @Test def instanceMethodCall(): Unit = { val List(m) = compileMethods(noOptCompiler)("def f(a: String) = a.trim") val a = newNullnessAnalyzer(m) testNullness(a, m, "INVOKEVIRTUAL java/lang/String.trim", 1, Unknown) testNullness(a, m, "ARETURN", 1, NotNull) } @Test def constructorCall(): Unit = { val List(m) = compileMethods(noOptCompiler)("def f = { val a = new Object; a.toString }") val a = newNullnessAnalyzer(m) // for reference, the output of showAllNullnessFrames(a, m) - note that the frame represents the state *before* executing the instr. // NEW java/lang/Object: 0: NotNull, 1: Unknown // DUP: 0: NotNull, 1: Unknown, 2: Unknown // INVOKESPECIAL java/lang/Object.: 0: NotNull, 1: Unknown, 2: Unknown, 3: Unknown // ASTORE 1: 0: NotNull, 1: Unknown, 2: NotNull // ALOAD 1: 0: NotNull, 1: NotNull // INVOKEVIRTUAL java/lang/Object.toString: 0: NotNull, 1: NotNull, 2: NotNull // ARETURN: 0: NotNull, 1: NotNull, 2: Unknown for ((insn, index, nullness) <- List( ("+NEW", 2, Unknown), // new value at slot 2 on the stack ("+DUP", 3, Unknown), ("+INVOKESPECIAL java/lang/Object", 2, NotNull), // after calling the initializer on 3, the value at 2 becomes NotNull ("ASTORE 1", 1, Unknown), // before the ASTORE 1, nullness of the value in local 1 is Unknown ("+ASTORE 1", 1, NotNull), // after storing the value at 2 in local 1, the local 1 is NotNull ("+ALOAD 1", 2, NotNull), // loading the value 1 puts a NotNull value on the stack (at 2) ("+INVOKEVIRTUAL java/lang/Object.toString", 2, Unknown) // nullness of value returned by `toString` is Unknown )) testNullness(a, m, insn, index, nullness) } @Test def explicitNull(): Unit = { val List(m) = compileMethods(noOptCompiler)("def f = { var a: Object = null; a }") val a = newNullnessAnalyzer(m) for ((insn, index, nullness) <- List( ("+ACONST_NULL", 2, Null), ("+ASTORE 1", 1, Null), ("+ALOAD 1", 2, Null) )) testNullness(a, m, insn, index, nullness) } @Test def stringLiteralsNotNull(): Unit = { val List(m) = compileMethods(noOptCompiler)("""def f = { val a = "hi"; a.trim }""") val a = newNullnessAnalyzer(m) testNullness(a, m, "+ASTORE 1", 1, NotNull) } @Test def newArraynotNull() { val List(m) = compileMethods(noOptCompiler)("def f = { val a = new Array[Int](2); a(0) }") val a = newNullnessAnalyzer(m) testNullness(a, m, "+NEWARRAY T_INT", 2, NotNull) // new array on stack testNullness(a, m, "+ASTORE 1", 1, NotNull) // local var (a) } @Test def aliasBranching(): Unit = { val code = """def f(o: Object) = { | var a: Object = o // a and o are aliases | var b: Object = null | var c: Object = null | var d: Object = o | if ("".trim == "") { | b = o | c = o // a, o, b, aliases | d = null | } else { | b = a // a, o, b aliases | d = null | } | b.toString // a, o, b aliases (so they become NotNull), but not c | // d is null here, assinged in both branches. |} """.stripMargin val List(m) = compileMethods(noOptCompiler)(code) val a = newNullnessAnalyzer(m) val trim = "INVOKEVIRTUAL java/lang/String.trim" val toSt = "INVOKEVIRTUAL java/lang/Object.toString" val end = s"+$toSt" for ((insn, index, nullness) <- List( (trim, 0, NotNull), // this (trim, 1, Unknown), // parameter o (trim, 2, Unknown), // a (trim, 3, Null), // b (trim, 4, Null), // c (trim, 5, Unknown), // d (toSt, 2, Unknown), // a, still the same (toSt, 3, Unknown), // b, was re-assinged in both branches to Unknown (toSt, 4, Unknown), // c, was re-assigned in one branch to Unknown (toSt, 5, Null), // d, was assigned to null in both branches (end, 2, NotNull), // a, NotNull (alias of b) (end, 3, NotNull), // b, receiver of toString (end, 4, Unknown), // c, no change (not an alias of b) (end, 5, Null) // d, no change )) testNullness(a, m, insn, index, nullness) } @Test def testInstanceOf(): Unit = { val code = """def f(a: Object) = { | val x = a | x.isInstanceOf[Throwable] // x and a remain unknown - INSTANCEOF doesn't throw a NPE on null | x.toString // x and a are not null | a.asInstanceOf[String].trim // the stack value (LOAD of local a) is still not-null after the CHECKCAST |} """.stripMargin val List(m) = compileMethods(noOptCompiler)(code) val a = newNullnessAnalyzer(m) val instof = "+INSTANCEOF" val tost = "+INVOKEVIRTUAL java/lang/Object.toString" val trim = "INVOKEVIRTUAL java/lang/String.trim" for ((insn, index, nullness) <- List( (instof, 1, Unknown), // a after INSTANCEOF (instof, 2, Unknown), // x after INSTANCEOF (tost, 1, NotNull), (tost, 2, NotNull), (trim, 3, NotNull) // receiver at `trim` )) testNullness(a, m, insn, index, nullness) } }