Skip to content

Avoid NPE when calling hashCode on a value class wrapping null#11204

Merged
lrytz merged 1 commit intoscala:2.13.xfrom
lrytz:t7396
Jan 27, 2026
Merged

Avoid NPE when calling hashCode on a value class wrapping null#11204
lrytz merged 1 commit intoscala:2.13.xfrom
lrytz:t7396

Conversation

@lrytz
Copy link
Copy Markdown
Member

@lrytz lrytz commented Jan 22, 2026

Change the synthetic hashCode for value classes from

def hashCode(): Int = this.underlying.hashCode

to

def hashCode(): Int = java.util.Objects.hashCode(this.underlying)

(unless the underlying value is primitive)

Fixes scala/bug#7396

@scala-jenkins scala-jenkins added this to the 2.13.19 milestone Jan 22, 2026
@lrytz lrytz requested a review from sjrd January 22, 2026 15:56
@retronym
Copy link
Copy Markdown
Member

Noting that toString will also throw an NPE. That seems less safe to change now.

equals already safely compares two VC(null) as equals.

@lrytz
Copy link
Copy Markdown
Member Author

lrytz commented Jan 23, 2026

Noting that toString will also throw an NPE. That seems less safe to change now.

This PR also fixes toString, it is crashing before because of hashCode.

Why would it be less safe to change?

I'll add a test.

scala> new V(null).toString
java.lang.NullPointerException: Cannot invoke "String.hashCode()" because "$this" is null
  at V$.hashCode$extension(<console>:1)
  at V.hashCode(<console>:1)
  at java.base/java.lang.Object.toString(Object.java:269)

@lrytz lrytz force-pushed the t7396 branch 2 times, most recently from 6002346 to d2b5b1b Compare January 23, 2026 08:28
@som-snytt
Copy link
Copy Markdown
Contributor

Not sure if it needs to be added that "not fixed in dotty".

Welcome to Scala 3.8.0 (25, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.

scala> class M(val s: String) extends AnyVal
[[syntax trees at end of                     typer]] // rs$line$2
package <empty> {
  final lazy module val rs$line$2: rs$line$2 = new rs$line$2()
  final module class rs$line$2() extends Object() { this: rs$line$2.type =>
    final class M(s: String) extends AnyVal() {
      val s: String
    }
    final lazy module val M: M = new M()
    final module class M() extends AnyRef() { this: M.type =>}
  }
}

// defined class M

scala> M(null)
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by scala.runtime.LazyVals$
WARNING: Please consider reporting this to the maintainers of class scala.runtime.LazyVals$
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
[[syntax trees at end of                     typer]] // rs$line$3
package <empty> {
  final lazy module val rs$line$3: rs$line$3 = new rs$line$3()
  final module class rs$line$3() extends Object() { this: rs$line$3.type =>
    val res0: M = new M(null)
  }
}

java.lang.NullPointerException: Cannot invoke "String.hashCode()" because "$this" is null
  at rs$line$2$M$.hashCode$extension(rs$line$2:1)
  at rs$line$2$M.hashCode(rs$line$2:1)
  at java.base/java.lang.Object.toString(Object.java:269)
Details ``` at pprint.Walker.treeify$$anonfun$1$$anonfun$7(Walker.scala:118) at pprint.Renderer.str$lzyINIT1$1(Renderer.scala:145) at pprint.Renderer.str$1(Renderer.scala:146) at pprint.Renderer.rec(Renderer.scala:147) at pprint.PPrinter.tokenize(PPrinter.scala:165) at pprint.PPrinter.apply(PPrinter.scala:119) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) at java.base/java.lang.reflect.Method.invoke(Method.java:565) at dotty.tools.repl.Rendering.pprintRender(Rendering.scala:62) at dotty.tools.repl.Rendering.replStringOf(Rendering.scala:108) at dotty.tools.repl.Rendering.valueOf$$anonfun$2(Rendering.scala:124) at scala.Option.map(Option.scala:244) at dotty.tools.repl.Rendering.valueOf(Rendering.scala:124) at dotty.tools.repl.Rendering.renderVal(Rendering.scala:166) at dotty.tools.repl.ReplDriver.$anonfun$9(ReplDriver.scala:460) at scala.runtime.function.JProcedure1.apply(JProcedure1.java:15) at scala.runtime.function.JProcedure1.apply(JProcedure1.java:10) at scala.collection.immutable.List.foreach(List.scala:327) at dotty.tools.repl.ReplDriver.extractAndFormatMembers$1(ReplDriver.scala:460) at dotty.tools.repl.ReplDriver.renderDefinitions$$anonfun$2(ReplDriver.scala:498) at scala.Option.map(Option.scala:244) at dotty.tools.repl.ReplDriver.renderDefinitions(ReplDriver.scala:497) at dotty.tools.repl.ReplDriver.compile$$anonfun$2(ReplDriver.scala:402) at scala.util.Either.fold(Either.scala:199) at dotty.tools.repl.ReplDriver.compile(ReplDriver.scala:384) at dotty.tools.repl.ReplDriver.interpret(ReplDriver.scala:343) at dotty.tools.repl.ReplDriver.loop$1(ReplDriver.scala:256) at dotty.tools.repl.ReplDriver.runUntilQuit$$anonfun$1(ReplDriver.scala:264) at dotty.tools.repl.ReplDriver.withRedirectedOutput(ReplDriver.scala:298) at dotty.tools.repl.ReplDriver.runBody$$anonfun$1(ReplDriver.scala:272) at dotty.tools.runner.ScalaClassLoader$.asContext(ScalaClassLoader.scala:79) at dotty.tools.repl.ReplDriver.runBody(ReplDriver.scala:272) at dotty.tools.repl.ReplDriver.runUntilQuit(ReplDriver.scala:264) at dotty.tools.repl.ReplDriver.tryRunning(ReplDriver.scala:166) at dotty.tools.repl.Main$.main(Main.scala:8) at dotty.tools.repl.Main.main(Main.scala) ```

@lrytz
Copy link
Copy Markdown
Member Author

lrytz commented Jan 23, 2026

Not sure if it needs to be added that "not fixed in dotty".

Yeah, I'll port it to 3.

lrytz added a commit to lrytz/scala3 that referenced this pull request Jan 26, 2026
Change the synthetic hashCode for value classes from

  def hashCode(): Int = this.underlying.hashCode

to

  def hashCode(): Int = java.util.Objects.hashCode(this.underlying)

(unless the underlying value is primitive)

Forward-port of scala/scala#11204
lrytz added a commit to scala/scala3 that referenced this pull request Jan 27, 2026
Change the synthetic hashCode for value classes from

    def hashCode(): Int = this.underlying.hashCode

to

    def hashCode(): Int = java.util.Objects.hashCode(this.underlying)

(unless the underlying value is primitive)

Forward-port of scala/scala#11204
@lrytz lrytz merged commit aeda558 into scala:2.13.x Jan 27, 2026
3 checks passed
WojciechMazur pushed a commit to scala/scala3 that referenced this pull request Feb 4, 2026
Change the synthetic hashCode for value classes from

  def hashCode(): Int = this.underlying.hashCode

to

  def hashCode(): Int = java.util.Objects.hashCode(this.underlying)

(unless the underlying value is primitive)

Forward-port of scala/scala#11204

[Cherry-picked eb274c2]
tgodzik pushed a commit to scala/scala3-lts that referenced this pull request Feb 17, 2026
Change the synthetic hashCode for value classes from

  def hashCode(): Int = this.underlying.hashCode

to

  def hashCode(): Int = java.util.Objects.hashCode(this.underlying)

(unless the underlying value is primitive)

Forward-port of scala/scala#11204

[Cherry-picked eb274c2]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

regression in result of null.## (when wrapped by a Value Class)

5 participants