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: NullnessValue): Unit = {
for (i <- findInstr(method, query)) {
val r = analyzer.frameAt(i, method).getValue(index)
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
// println(showAllNullnessFrames(newNullnessAnalyzer(m), m))
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, NotNullValue)
}
@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, UnknownValue1)
testNullness(a, m, "ARETURN", 1, NotNullValue)
}
@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.<init>: 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, UnknownValue1), // new value at slot 2 on the stack
("+DUP", 3, UnknownValue1),
("+INVOKESPECIAL java/lang/Object", 2, NotNullValue), // after calling the initializer on 3, the value at 2 becomes NotNull
("ASTORE 1", 1, UnknownValue1), // before the ASTORE 1, nullness of the value in local 1 is Unknown
("+ASTORE 1", 1, NotNullValue), // after storing the value at 2 in local 1, the local 1 is NotNull
("+ALOAD 1", 2, NotNullValue), // loading the value 1 puts a NotNull value on the stack (at 2)
("+INVOKEVIRTUAL java/lang/Object.toString", 2, UnknownValue1) // 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, NullValue),
("+ASTORE 1", 1, NullValue),
("+ALOAD 1", 2, NullValue)
)) 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, NotNullValue)
}
@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, NotNullValue) // new array on stack
testNullness(a, m, "+ASTORE 1", 1, NotNullValue) // local var (a)
}
@Test
def mergeNullNotNull(): Unit = {
val code =
"""def f(o: Object) = {
| var a: Object = o
| var c: Object = null
| if ("".trim eq "-") {
| c = o
| }
| a.toString
|}
""".stripMargin
val List(m) = compileMethods(noOptCompiler)(code)
val a = newNullnessAnalyzer(m)
val toSt = "+INVOKEVIRTUAL java/lang/Object.toString"
testNullness(a, m, toSt, 3, UnknownValue1)
}
@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, NotNullValue), // this
(trim, 1, UnknownValue1), // parameter o
(trim, 2, UnknownValue1), // a
(trim, 3, NullValue), // b
(trim, 4, NullValue), // c
(trim, 5, UnknownValue1), // d
(toSt, 2, UnknownValue1), // a, still the same
(toSt, 3, UnknownValue1), // b, was re-assinged in both branches to Unknown
(toSt, 4, UnknownValue1), // c, was re-assigned in one branch to Unknown
(toSt, 5, NullValue), // d, was assigned to null in both branches
(end, 2, NotNullValue), // a, NotNull (alias of b)
(end, 3, NotNullValue), // b, receiver of toString
(end, 4, UnknownValue1), // c, no change (not an alias of b)
(end, 5, NullValue) // 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, UnknownValue1), // a after INSTANCEOF
(instof, 2, UnknownValue1), // x after INSTANCEOF
(tost, 1, NotNullValue),
(tost, 2, NotNullValue),
(trim, 3, NotNullValue) // receiver at `trim`
)) testNullness(a, m, insn, index, nullness)
}
}