Skip to content

Unable to select member of nullable user-defined class #11968

Closed
@noti0na1

Description

@noti0na1
scalac Stest.scala
class C {
    def get(): Int = 0
}

def g = {
    val s: String | Null = ???
    val l = s.length // ok
    val c: C | Null = ???
    c.get() // error: value get is not a member of C | Null
}

Output

-- [E008] Not Found Error: Stest.scala:13:6 ------------------------------------
13 |    c.get()
   |    ^^^^^
   |    value get is not a member of C | Null

The nullable C is expected to have similar behaviour as nullable String.

I think the issue is at TypeOps.orDominator. String | Null after join becomes String. But C | Null after join becomes Object.

This code can be compiled using explicit-nulls and unsafeNulls (scalac -Yexplicit-nulls -language:unsafeNulls), since unsafeNulls will ignore Null type when searching members, then there is no need to join the OrType (see Types.goOr).

(String | Null).join can return a correct result, because the type of String after widenExpr is different, approximateOr uses subtyping and lub to decide the result.

// approximateOr 
if ((tp1w ne tp1) || (tp2w ne tp2)) {
  val isSingle1 = tp1.isInstanceOf[SingletonType]
  val isSingle2 = tp2.isInstanceOf[SingletonType]
  return {
    if (tp2w eq tp2) orDominator(tp1w | tp2)                  // 2.1
    else if (tp1w eq tp1) orDominator(tp1 | tp2w)             // 2.1
    else if (tp1w frozen_<:< tp2w) orDominator(tp1w | tp2)    // 2.2
    else if (tp2w frozen_<:< tp1w) orDominator(tp1 | tp2w)    // 2.2
    else if (isSingle1 && !isSingle2) orDominator(tp1w | tp2) // 2.3
    else if (isSingle2 && !isSingle1) orDominator(tp1 | tp2w) // 2.3
    else if (tp1 frozen_<:< tp2w) tp2w                        // 2.4
    else if (tp2 frozen_<:< tp1w) tp1w                        // 2.5
    else orDominator(tp1w | tp2)                              // 2.6
  }
}

However, the type of C after widenExpr is the same, approximateOr uses baseClasses of C and Null to decide. I think we need to handle bottom classes for commonBaseClasses more carefully to fix the issue.

// Step 3: Intersect base classes of both sides
val commonBaseClasses = tp.mapReduceOr(_.baseClasses)(intersect)
val doms = dominators(commonBaseClasses, Nil)
def baseTp(cls: ClassSymbol): Type =
  tp.baseType(cls).mapReduceOr(identity)(mergeRefinedOrApplied)
doms.map(baseTp).reduceLeft(AndType.apply)

I don't understand the reason to use baseClasses here. Why don't we using subtyping compare in step 3 as well?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions