Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve warning for wildcard matching only null under the explicit nulls flag (scala#21577) #21623

Merged
merged 3 commits into from
Oct 28, 2024

Conversation

HarrisL2
Copy link
Contributor

@HarrisL2 HarrisL2 commented Sep 21, 2024

Improve warning for wildcard matching only null under the explicit nulls flag

Fixes scala#21577

I have signed the CLA

@HarrisL2 HarrisL2 force-pushed the i21577 branch 2 times, most recently from 97faede to d7a439e Compare October 4, 2024 03:05
@HarrisL2 HarrisL2 changed the title Improve warning for wildcard matching non-nullable types (scala#21577) Improve warning for wildcard matching only null under the explicit nulls flag (scala#21577) Oct 4, 2024
@HarrisL2 HarrisL2 requested a review from noti0na1 October 4, 2024 04:45
@noti0na1
Copy link
Member

noti0na1 commented Oct 4, 2024

The reason why there is a warning for the "unreachable wildcard except null" cases is: this kind of cases we normally don't write. If there is an extra wildcard case at the end, it much match with null, and it is better to explicitly write it down.

For example, if val a: A,

  • with explicit nulls, a is expected to not carry a null value, so a case _: A => should cover all possible values, and any additional cases should be considered "unreachable";
  • without explicit nulls, a is expected to carry a non-nullable value in most of cases, and a case _: A => is normally good enough to cover all possible values. If there is an additional case _ =>, it must match with null, so we give a "unreachable except null" warning.

For val a: FlexibleType(A) in explicit nulls, we want to treat it as A without explicit nulls in terms of space analyse. Hence, a match case _: A => should not produce any warning. If there is an additional case _ =>, we give a "unreachable except null" warning similarly.

tests/explicit-nulls/warn/i21577.scala Outdated Show resolved Hide resolved
tests/explicit-nulls/warn/i21577.scala Outdated Show resolved Hide resolved
tests/explicit-nulls/warn/i21577.scala Outdated Show resolved Hide resolved
tests/explicit-nulls/warn/i21577.scala Outdated Show resolved Hide resolved
@noti0na1
Copy link
Member

noti0na1 commented Oct 7, 2024

Another issue which can be improved:

class A
class B

def f(s: A | B) = s match
  case _: A => 0
  case null => 1
  case _: B => 2
  case _ => 3

If we compile without explicit nulls, it says the last wildcard case is "Unreachable case except for null". If we change to case null =>, it will say it is Unreachable.

It should be unreachable from the begining.

@noti0na1
Copy link
Member

noti0na1 commented Oct 8, 2024

I did some refactoring to checkReachability, can you test if this version works?

  def checkReachability(m: Match)(using Context): Unit = trace(i"checkReachability($m)"):
    val selTyp = toUnderlying(m.selector.tpe).dealias
    val isNullable = selTyp.isInstanceOf[FlexibleType] || selTyp.classSymbol.isNullableClass
    val targetSpace = trace(i"targetSpace($selTyp)"):
      if isNullable && !ctx.mode.is(Mode.SafeNulls)
      then project(OrType(selTyp, ConstantType(Constant(null)), soft = false))
      else project(selTyp)

    @tailrec def recur(cases: List[CaseDef], prevs: List[Space], deferred: List[Tree]): Unit =
      cases match
        case Nil =>
        case CaseDef(pat, guard, _) :: rest =>
          val curr = trace(i"project($pat)")(project(pat))
          val covered = trace("covered")(simplify(intersect(curr, targetSpace)))
          val prev = trace("prev")(simplify(Or(prevs)))

          if prev == Empty && covered == Empty then // defer until a case is reachable
            recur(rest, prevs, pat :: deferred)
          else
            for pat <- deferred.reverseIterator
            do report.warning(MatchCaseUnreachable(), pat.srcPos)

            if pat != EmptyTree // rethrow case of catch uses EmptyTree
                && !pat.symbol.isAllOf(SyntheticCase, butNot=Method) // ExpandSAMs default cases use SyntheticCase
                && isSubspace(covered, prev)
            then
              val nullOnly = isNullable && rest.isEmpty && isWildcardArg(pat)
              val msg = if nullOnly then MatchCaseOnlyNullWarning() else MatchCaseUnreachable()
              report.warning(msg, pat.srcPos)

            val newPrev = if guard.isEmpty then covered :: prevs else prevs
            recur(rest, newPrev, Nil)

    recur(m.cases, Nil, Nil)
  end checkReachability

I think you also need to add a case to decompose:

case tp: FlexibleType                            => List(tp.underlying, ConstantType(Constant(null)))

and a case to toUnderlying:

case tp: FlexibleType                           => tp.derivedFlexibleType(toUnderlying(tp.underlying))

A flexible type is considered "nullable" as it may contain null value, and it is key to keep the flexible type in the space.

I don't think FlexibleType has been handled properly before in this file. Could you also test some examples such that more complicated types are inside the FlexibleType?

…(scala#21577)

Adds a more detailed warning message when a wildcard case is only reachable
by null under explict nulls flag.

Fixes scala#21577
Copy link
Member

@dwijnand dwijnand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great

@noti0na1 noti0na1 enabled auto-merge October 28, 2024 15:47
@noti0na1 noti0na1 merged commit a3786a5 into scala:main Oct 28, 2024
26 checks passed
@WojciechMazur WojciechMazur added this to the 3.6.3 milestone Dec 9, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants