summaryrefslogtreecommitdiff
path: root/test/junit/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzerTest.scala
blob: c173bacd4616ac4f79a40f6a4ab1b510de6c7466 (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
221
222
223
224
package scala.tools.nsc
package backend.jvm
package analysis

import org.junit.Assert._
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

import scala.collection.JavaConverters._
import scala.tools.asm.tree.MethodNode
import scala.tools.nsc.backend.jvm.AsmUtils._
import scala.tools.nsc.backend.jvm.BTypes._
import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._
import scala.tools.testing.BytecodeTesting
import scala.tools.testing.BytecodeTesting._

@RunWith(classOf[JUnit4])
class NullnessAnalyzerTest extends BytecodeTesting {
  override def compilerArgs = "-opt:l:none"
  import compiler._
  import global.genBCode.bTypes.backendUtils._

  def newNullnessAnalyzer(methodNode: MethodNode, classInternalName: InternalName = "C") = new AsmAnalyzer(methodNode, classInternalName, new NullnessAnalyzer(global.genBCode.bTypes, methodNode))

  def testNullness(analyzer: AsmAnalyzer[NullnessValue], method: MethodNode, query: String, index: Int, nullness: NullnessValue): Unit = {
    for (i <- findInstrs(method, query)) {
      val r = analyzer.frameAt(i).getValue(index)
      assertTrue(s"Expected: $nullness, found: $r. At instr ${textify(i)}", nullness == r)
    }
  }

  // debug / helper for writing tests
  def showAllNullnessFrames(analyzer: AsmAnalyzer[NullnessValue], 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)
      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 m = compileAsmMethod("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 C.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 m = compileAsmMethod("def f = this.toString")
    val a = newNullnessAnalyzer(m)
    testNullness(a, m, "ALOAD 0", 0, NotNullValue)
  }

  @Test
  def instanceMethodCall(): Unit = {
    val m = compileAsmMethod("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 m = compileAsmMethod("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 m = compileAsmMethod("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 m = compileAsmMethod("""def f = { val a = "hi"; a.trim }""")
    val a = newNullnessAnalyzer(m)
    testNullness(a, m, "+ASTORE 1", 1, NotNullValue)
  }

  @Test
  def newArraynotNull() {
    val m = compileAsmMethod("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 m = compileAsmMethod(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 m = compileAsmMethod(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 m = compileAsmMethod(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)
  }
}