Skip to content

Commit 9d67389

Browse files
authored
Merge pull request scala#6485 from eed3si9n/wip/null-in-extractor2
Stop passing null to extractor pattern [ci: last-only]
2 parents 635b349 + a60eaeb commit 9d67389

File tree

16 files changed

+203
-78
lines changed

16 files changed

+203
-78
lines changed

spec/08-pattern-matching.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,9 @@ a case class, the stable identifier $x$ denotes an object which has a
192192
member method named `unapply` or `unapplySeq` that matches
193193
the pattern.
194194

195+
An extractor pattern cannot match the value `null`. The implementation
196+
ensures that the `unapply`/`unapplySeq` method is not applied to `null`.
197+
195198
An `unapply` method in an object $x$ _matches_ the pattern
196199
$x(p_1 , \ldots , p_n)$ if it takes exactly one argument and one of
197200
the following applies:

src/compiler/scala/tools/nsc/transform/patmat/MatchAnalysis.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ trait MatchApproximation extends TreeAndTypeAnalysis with ScalaLogic with MatchT
344344
case AlternativesTreeMaker(_, altss, _) => \/(altss map (alts => /\(alts map this)))
345345
case ProductExtractorTreeMaker(testedBinder, None) => uniqueNonNullProp(binderToUniqueTree(testedBinder))
346346
case SubstOnlyTreeMaker(_, _) => True
347+
case NonNullTestTreeMaker(prevBinder, _, _) => uniqueNonNullProp(binderToUniqueTree(prevBinder))
347348
case GuardTreeMaker(guard) =>
348349
guard.tpe match {
349350
case ConstantTrue => True

src/compiler/scala/tools/nsc/transform/patmat/MatchTranslation.scala

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -111,20 +111,28 @@ trait MatchTranslation {
111111
val (makers, unappBinder) = {
112112
val paramType = extractor.expectedExtractedType
113113
// Statically conforms to paramType
114-
if (tpe <:< paramType) (treeMakers(binder, false, pos), binder)
114+
if (tpe <:< paramType) {
115+
// enforce all extractor patterns to be non-null
116+
val nonNullTest = NonNullTestTreeMaker(binder, paramType, pos)
117+
val unappBinder = nonNullTest.nextBinder
118+
(nonNullTest :: treeMakers(unappBinder, pos), unappBinder)
119+
}
115120
else {
116121
// chain a type-testing extractor before the actual extractor call
117122
// it tests the type, checks the outer pointer and casts to the expected type
118123
// TODO: the outer check is mandated by the spec for case classes, but we do it for user-defined unapplies as well [SPEC]
119124
// (the prefix of the argument passed to the unapply must equal the prefix of the type of the binder)
120125
val typeTest = TypeTestTreeMaker(binder, binder, paramType, paramType)(pos, extractorArgTypeTest = true)
121126
val binderKnownNonNull = typeTest impliesBinderNonNull binder
122-
123-
// check whether typetest implies binder is not null,
124-
// even though the eventual null check will be on typeTest.nextBinder
125-
// it'll be equal to binder casted to paramType anyway (and the type test is on binder)
126-
val unappBinder = typeTest.nextBinder
127-
(typeTest :: treeMakers(unappBinder, binderKnownNonNull, pos), unappBinder)
127+
// skip null test if it's implied
128+
if (binderKnownNonNull) {
129+
val unappBinder = typeTest.nextBinder
130+
(typeTest :: treeMakers(unappBinder, pos), unappBinder)
131+
} else {
132+
val nonNullTest = NonNullTestTreeMaker(typeTest.nextBinder, paramType, pos)
133+
val unappBinder = nonNullTest.nextBinder
134+
(typeTest :: nonNullTest :: treeMakers(unappBinder, pos), unappBinder)
135+
}
128136
}
129137
}
130138

@@ -384,11 +392,8 @@ trait MatchTranslation {
384392

385393
abstract class ExtractorCall(fun: Tree, args: List[Tree]) extends ExtractorAlignment(fun, args)(context) {
386394
/** Create the TreeMaker that embodies this extractor call
387-
*
388-
* `binderKnownNonNull` indicates whether the cast implies `binder` cannot be null
389-
* when `binderKnownNonNull` is `true`, `ProductExtractorTreeMaker` does not do a (redundant) null check on binder
390395
*/
391-
def treeMakers(binder: Symbol, binderKnownNonNull: Boolean, pos: Position): List[TreeMaker]
396+
def treeMakers(binder: Symbol, pos: Position): List[TreeMaker]
392397

393398
// `subPatBinders` are the variables bound by this pattern in the following patterns
394399
// subPatBinders are replaced by references to the relevant part of the extractor's result (tuple component, seq element, the result as-is)
@@ -484,10 +489,8 @@ trait MatchTranslation {
484489
/** Create the TreeMaker that embodies this extractor call
485490
*
486491
* `binder` has been casted to `paramType` if necessary
487-
* `binderKnownNonNull` indicates whether the cast implies `binder` cannot be null
488-
* when `binderKnownNonNull` is `true`, `ProductExtractorTreeMaker` does not do a (redundant) null check on binder
489492
*/
490-
def treeMakers(binder: Symbol, binderKnownNonNull: Boolean, pos: Position): List[TreeMaker] = {
493+
def treeMakers(binder: Symbol, pos: Position): List[TreeMaker] = {
491494
val paramAccessors = expectedExtractedType.typeSymbol.constrParamAccessors
492495
val numParams = paramAccessors.length
493496
def paramAccessorAt(subPatIndex: Int) = paramAccessors(math.min(subPatIndex, numParams - 1))
@@ -508,7 +511,7 @@ trait MatchTranslation {
508511
)
509512

510513
// checks binder ne null before chaining to the next extractor
511-
ProductExtractorTreeMaker(binder, lengthGuard(binder))(subPatBinders, subPatRefs(binder), mutableBinders, binderKnownNonNull, ignoredSubPatBinders) :: Nil
514+
ProductExtractorTreeMaker(binder, lengthGuard(binder))(subPatBinders, subPatRefs(binder), mutableBinders, ignoredSubPatBinders) :: Nil
512515
}
513516

514517
// reference the (i-1)th case accessor if it exists, otherwise the (i-1)th tuple component
@@ -535,14 +538,13 @@ trait MatchTranslation {
535538
/** Create the TreeMaker that embodies this extractor call
536539
*
537540
* `binder` has been casted to `paramType` if necessary
538-
* `binderKnownNonNull` is not used in this subclass
539541
*
540542
* TODO: implement review feedback by @retronym:
541543
* Passing the pair of values around suggests:
542544
* case class Binder(sym: Symbol, knownNotNull: Boolean).
543545
* Perhaps it hasn't reached critical mass, but it would already clean things up a touch.
544546
*/
545-
def treeMakers(patBinderOrCasted: Symbol, binderKnownNonNull: Boolean, pos: Position): List[TreeMaker] = {
547+
def treeMakers(patBinderOrCasted: Symbol, pos: Position): List[TreeMaker] = {
546548
// the extractor call (applied to the binder bound by the flatMap corresponding
547549
// to the previous (i.e., enclosing/outer) pattern)
548550
val (extractorApply, needsSubst) = spliceApply(pos, patBinderOrCasted)

src/compiler/scala/tools/nsc/transform/patmat/MatchTreeMaking.scala

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,32 @@ trait MatchTreeMaking extends MatchCodeGen with Debugging {
197197
}
198198
}
199199

200+
/**
201+
* Make a TreeMaker that performs null check.
202+
* This is called prior to extractor call.
203+
*/
204+
case class NonNullTestTreeMaker(
205+
prevBinder: Symbol,
206+
expectedTp: Type,
207+
override val pos: Position) extends FunTreeMaker {
208+
import CODE._
209+
override lazy val nextBinder = prevBinder.asTerm // just passing through
210+
val nextBinderTp = nextBinder.info.widen
211+
212+
val nullCheck = REF(prevBinder) OBJ_NE NULL
213+
lazy val localSubstitution = Substitution(Nil, Nil)
214+
215+
def isExpectedPrimitiveType = isPrimitiveValueType(expectedTp)
216+
217+
def chainBefore(next: Tree)(casegen: Casegen): Tree =
218+
atPos(pos) {
219+
if (isExpectedPrimitiveType) next
220+
else casegen.ifThenElseZero(nullCheck, next)
221+
}
222+
223+
override def toString = s"NN(${prevBinder.name})"
224+
}
225+
200226
/**
201227
* Make a TreeMaker that will result in an extractor call specified by `extractor`
202228
* the next TreeMaker (here, we don't know which it'll be) is chained after this one by flatMap'ing
@@ -272,7 +298,6 @@ trait MatchTreeMaking extends MatchCodeGen with Debugging {
272298
val subPatBinders: List[Symbol],
273299
val subPatRefs: List[Tree],
274300
val mutableBinders: List[Symbol],
275-
binderKnownNonNull: Boolean,
276301
val ignoredSubPatBinders: Set[Symbol]
277302
) extends FunTreeMaker with PreserveSubPatBinders {
278303

@@ -283,13 +308,7 @@ trait MatchTreeMaking extends MatchCodeGen with Debugging {
283308
def extraStoredBinders: Set[Symbol] = mutableBinders.toSet
284309

285310
def chainBefore(next: Tree)(casegen: Casegen): Tree = {
286-
val nullCheck = REF(prevBinder) OBJ_NE NULL
287-
val cond =
288-
if (binderKnownNonNull) extraCond
289-
else (extraCond map (nullCheck AND _)
290-
orElse Some(nullCheck))
291-
292-
cond match {
311+
extraCond match {
293312
case Some(cond) =>
294313
casegen.ifThenElseZero(cond, bindSubPats(substitution(next)))
295314
case _ =>

src/library/scala/Array.scala

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -498,11 +498,7 @@ object Array {
498498
* @return sequence wrapped in a [[scala.Some]], if `x` is an Array, otherwise `None`
499499
*/
500500
def unapplySeq[T](x: Array[T]): Option[IndexedSeq[T]] =
501-
if (x == null) None else Some(ArraySeq.unsafeWrapArray[T](x))
502-
// !!! the null check should to be necessary, but without it 2241 fails. Seems to be a bug
503-
// in pattern matcher. @PP: I noted in #4364 I think the behavior is correct.
504-
// Is ArraySeq safe here? In 2.12 we used to call .toIndexedSeq which copied the array
505-
// instead of wrapping it in a ArraySeq but it appears unnecessary.
501+
Some(ArraySeq.unsafeWrapArray[T](x))
506502
}
507503

508504
/** Arrays are mutable, indexed collections of values. `Array[T]` is Scala's representation

src/library/scala/util/matching/Regex.scala

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -279,10 +279,8 @@ class Regex private[matching](val pattern: Pattern, groupNames: String*) extends
279279
* @param s The string to match
280280
* @return The matches
281281
*/
282-
def unapplySeq(s: CharSequence): Option[List[String]] = s match {
283-
case null => None
284-
case _ =>
285-
val m = pattern matcher s
282+
def unapplySeq(s: CharSequence): Option[List[String]] = {
283+
val m = pattern matcher s
286284
if (runMatcher(m)) Some(List.tabulate(m.groupCount) { i => m.group(i + 1) })
287285
else None
288286
}
@@ -336,7 +334,7 @@ class Regex private[matching](val pattern: Pattern, groupNames: String*) extends
336334
* and the result of that match is used.
337335
*/
338336
def unapplySeq(m: Match): Option[List[String]] =
339-
if (m == null || m.matched == null) None
337+
if (m.matched == null) None
340338
else if (m.matcher.pattern == this.pattern) Regex.extractGroupsFromMatch(m)
341339
else unapplySeq(m.matched)
342340

test/files/run/name-based-patmat.check

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
name-based-patmat.scala:73: warning: unreachable code
2+
case Foo(5, 10) => 4 // should warn unreachable
3+
^
14
`catdog only` has 11 chars
25
`catdog only, no product` has 23 chars
36
catdog

test/files/run/patmatnew.check

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
patmatnew.scala:351: warning: a pure expression does nothing in statement position
1+
patmatnew.scala:352: warning: a pure expression does nothing in statement position
22
case 1 => "OK"
33
^
4-
patmatnew.scala:352: warning: a pure expression does nothing in statement position
4+
patmatnew.scala:353: warning: a pure expression does nothing in statement position
55
case 2 => assert(false); "KO"
66
^
7-
patmatnew.scala:352: warning: multiline expressions might require enclosing parentheses; a value can be silently discarded when Unit is expected
7+
patmatnew.scala:353: warning: multiline expressions might require enclosing parentheses; a value can be silently discarded when Unit is expected
88
case 2 => assert(false); "KO"
99
^
10-
patmatnew.scala:353: warning: a pure expression does nothing in statement position
10+
patmatnew.scala:354: warning: a pure expression does nothing in statement position
1111
case 3 => assert(false); "KO"
1212
^
13-
patmatnew.scala:353: warning: multiline expressions might require enclosing parentheses; a value can be silently discarded when Unit is expected
13+
patmatnew.scala:354: warning: multiline expressions might require enclosing parentheses; a value can be silently discarded when Unit is expected
1414
case 3 => assert(false); "KO"
1515
^
16-
patmatnew.scala:670: warning: This catches all Throwables. If this is really intended, use `case e : Throwable` to clear this warning.
16+
patmatnew.scala:671: warning: This catches all Throwables. If this is really intended, use `case e : Throwable` to clear this warning.
1717
case e => {
1818
^
19-
patmatnew.scala:489: warning: unreachable code
19+
patmatnew.scala:490: warning: unreachable code
2020
case _ if false =>
2121
^

test/files/run/patmatnew.flags

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
-deprecation

test/files/run/patmatnew.scala

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ object Test {
3838
Ticket346.run()
3939
Ticket37.run()
4040
Ticket44.run()
41+
NullMatch.run()
4142
}
4243

4344
def assertEquals(a: Any, b: Any): Unit = { assert(a == b) }
@@ -762,4 +763,76 @@ object Test {
762763

763764
} // end Ticket346
764765

766+
// scala/bug#4364
767+
object NullMatch {
768+
object XArray {
769+
def unapplySeq[A](x: Array[A]): Option[IndexedSeq[A]] =
770+
if (x eq null) sys.error("Unexpected null!")
771+
else Some(x.toIndexedSeq)
772+
}
773+
774+
object YArray {
775+
def unapply(xs: Array[Int]): Boolean =
776+
if (xs eq null) sys.error("Unexpected null!")
777+
else true
778+
}
779+
780+
object Animal {
781+
def unapply(x: AnyRef): Option[AnyRef] =
782+
if (x.toString == "Animal") Some(x)
783+
else None
784+
}
785+
786+
def nullMatch[A](xs: Array[A]): Boolean = xs match {
787+
case Array(xs @_*) => false
788+
case _ => true
789+
}
790+
791+
def nullMatch2[A](xs: Array[A]): Boolean = xs match {
792+
case XArray(xs @_*) => false
793+
case _ => true
794+
}
795+
796+
def nullMatch3[A](xs: Array[A]): Boolean = xs match {
797+
case XArray(xs @_*) if 1 == 1 => false
798+
case _ => true
799+
}
800+
801+
def nullMatch4(xs: Array[Int]): Boolean = xs match {
802+
case YArray() => false
803+
case _ => true
804+
}
805+
806+
def nullMatch5(x: AnyRef): Boolean = x match {
807+
case Animal(x) => false
808+
case _ => true
809+
}
810+
811+
def t8787nullMatch() = {
812+
val r = """\d+""".r
813+
val s: String = null
814+
val x = s match { case r() => 1 ; case _ => 2 }
815+
2 == x
816+
}
817+
818+
def t8787nullMatcher() = {
819+
val r = """(\d+):(\d+)""".r
820+
val s = "1:2 3:4 5:6"
821+
val z = ((r findAllMatchIn s).toList :+ null) flatMap {
822+
case r(x, y) => Some((x.toInt, y.toInt))
823+
case _ => None
824+
}
825+
List((1,2),(3,4),(5,6)) == z
826+
}
827+
828+
def run(): Unit = {
829+
assert(nullMatch(null))
830+
assert(nullMatch2(null))
831+
assert(nullMatch3(null))
832+
assert(nullMatch4(null))
833+
assert(nullMatch5(null))
834+
assert(t8787nullMatch())
835+
assert(t8787nullMatcher())
836+
}
837+
}
765838
}

0 commit comments

Comments
 (0)