summaryrefslogtreecommitdiff
path: root/test/junit/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzerTest.scala
blob: 94e776aadb67dd7fa5eb65f3513643665c03a12e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
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.<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, 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)
  }
}