Skip to content

Display warning on equals comparing non-references#8120

Merged
dwijnand merged 1 commit intoscala:2.13.xfrom
eed3si9n:wip/deprecate-anyval-equals
Oct 14, 2020
Merged

Display warning on equals comparing non-references#8120
dwijnand merged 1 commit intoscala:2.13.xfrom
eed3si9n:wip/deprecate-anyval-equals

Conversation

@eed3si9n
Copy link
Member

@eed3si9n eed3si9n commented Jun 3, 2019

Ref scala/bug#11551
Ref https://twitter.com/not_xuwei_k/status/1135374786593468416

Scala 2.13.1 or Dotty allows you to write the following expression:

1 equals 1L

or generically as:

scala> def check[A](a1: A, a2: A): Boolean = a1.equals(a2)
def check[A](a1: A, a2: A): Boolean

problem

This looks seemingly innocuous but it's actually not "safe" because of cooperative equality.

scala> 1 equals 1L
val res0: Boolean = false

scala> check(1L, 1)
val res1: Boolean = false

what this changes

This PR extends "sensible check" to equals, to warn against using it as opposed to ==:

[info] Running (fork) scala.tools.nsc.MainGenericRunner -usejavacp
Welcome to Scala 2.13.0-pre-db58db9 (OpenJDK 64-Bit Server VM, Java 1.8.0_232).
Type in expressions for evaluation. Or try :help.

scala> def check[A](a1: A, a2: A): Boolean = a1.equals(a2)
                                                      ^
       warning: comparing values of types A and A using `equals` is unsafe due to cooperative equality; use `==` instead
check: [A](a1: A, a2: A)Boolean

/cc @xuwei-k

@scala-jenkins scala-jenkins added this to the 2.13.1 milestone Jun 3, 2019
@eed3si9n eed3si9n force-pushed the wip/deprecate-anyval-equals branch from 998619a to b1cf03a Compare June 4, 2019 02:47
@xuwei-k
Copy link
Contributor

xuwei-k commented Jun 4, 2019

Is this within expectations? 🤔

--- a/test/files/neg/checksensible.check
+++ b/test/files/neg/checksensible.check
@@ -112,6 +112,9 @@ checksensible.scala:111: warning: comparing values of types AnyVal and Int using
 checksensible.scala:114: warning: comparing values of types A and Int using `equals` is unsafe due to cooperative equality; use `==` instead
   def foo[A](a: A) = a.equals(1)
                              ^
+checksensible.scala:118: warning: comparing values of types Int and Int using `equals` is unsafe due to cooperative equality; use `==` instead
+  1 equals 1
+    ^
 error: No warnings can be incurred under -Werror.
-38 warnings found
+39 warnings found
 one error found
diff --git a/test/files/neg/checksensible.scala b/test/files/neg/checksensible.scala
index 7542509e55..37dfc0f2fc 100644
--- a/test/files/neg/checksensible.scala
+++ b/test/files/neg/checksensible.scala
@@ -114,4 +114,6 @@ class AnyEqualsTest {
   def foo[A](a: A) = a.equals(1)
   // ok
   def bar[A <: AnyRef](a: A) = a.equals(1)
+
+  1 equals 1
 }

@sjrd
Copy link
Member

sjrd commented Jun 4, 2019

I'm not sure I like this. There are a lot of places where calling equals is the right thing to do, for performance and/or semantics.

@eed3si9n
Copy link
Member Author

eed3si9n commented Jun 4, 2019

Is this within expectations?

Nope. I need to filter out receiver == actual more.

@eed3si9n eed3si9n force-pushed the wip/deprecate-anyval-equals branch from b1cf03a to db58db9 Compare June 4, 2019 05:08
@eed3si9n
Copy link
Member Author

eed3si9n commented Jun 4, 2019

@sjrd

I'm not sure I like this. There are a lot of places where calling equals is the right thing to do, for performance and/or semantics.

Could you share some examples? Something like the following?

def foo[A](a: A) = a.equals(1)

In a normal application either written in Scala or Scala.JS, is the benefit worth the confusion of == not being the same as equals? One direction might be to remove cooperative equality (ref https://contributors.scala-lang.org/t/can-we-get-rid-of-cooperative-equality/1131), but given that won't happen for a while, the logical option would be to discourage the use of Any#equals.

@dwijnand
Copy link
Member

dwijnand commented Jun 4, 2019

If we're keeping cooperative equality maybe this warning should be opt-out-able, including globally opt-out-able for codebases where it's throughout.

@sjrd
Copy link
Member

sjrd commented Jun 4, 2019

It's mostly necessary to implement Java-like contracts. Here are the changes I need to apply to the Scala.js codebase to accommodate this PR:
https://gist.github.com/sjrd/bd11d587635c7ce85230a46f9657ef77
That does not include a bunch of warnings in my test suite as well; just the projects that have -Xfatal-warnings on.

@dwijnand
Copy link
Member

dwijnand commented Jun 4, 2019

This one is particularly representative:

       if (self.asInstanceOf[AnyRef] eq null) that.asInstanceOf[AnyRef] eq null
-      else self.equals(that)
+      else self.asInstanceOf[AnyRef].equals(that)

Perhaps we should have a method that looks less like a cast and more like "ok, convert this to a reference value, boxing if necessary". Say it's called toAnyRef (🖌), then maybe the warning and error messages could say: use "a == b" or "a.toAnyRef.equals(b)" (and for eq: use a.toAnyRef.eq(b)).

@lrytz
Copy link
Member

lrytz commented Jul 22, 2019

Can we move this one forward? It seems very rare that one would actually have to call Any.equals, maybe we can enumerate these situations?

@eed3si9n
Copy link
Member Author

Are there something I can do to improve this PR, or is warning a wrong-headed approach?

@szeiger szeiger modified the milestones: 2.13.1, 2.13.2 Aug 30, 2019
@lrytz
Copy link
Member

lrytz commented Jan 31, 2020

@eed3si9n should we close this, as there's no obvious path forward?

@eed3si9n
Copy link
Member Author

@lrytz I find that symbolic operator == supposedly documented as:

  final def ==(that: Any): Boolean = this equals that

and equals being different to be unintuitive aspect of Scala. It did cause scala/bug#11551 after all, so even for Scala maintainers, it's a pitfall they could sometimes fall into. If they are not exchangeable, then adding a warning would be improvement.

If we're saying it's a shorthand to force boxing, that's even more surprising, and self.asInstanceOf[AnyRef].eq(that) would make the intent more clear.

@lrytz lrytz self-assigned this Feb 3, 2020
@lrytz
Copy link
Member

lrytz commented Feb 3, 2020

I'm supportive of the warning, but it's a complicated topic, so I'd really like to get more input, for example from @sjrd (again), @Ichoran, @NthPortal, @som-snytt, @retronym.

Random idea: would it make sense to have Any.equals protected (at some point, @deprecated first), and only have Object/AnyRef.equals public?

@NthPortal
Copy link
Contributor

I find the referenced bug particularly compelling. I think having the warning helps make a sharp edge a little bit safer.

I think it is a reasonable warning, unless someone brings up a use case where the warned-about behaviour is correct and Scala-like. Sébastien is in a fairly unique position of having to conform to Java's behaviour, which is not cooperatively equal; however, if one were to design similar APIs (e.g. like that of Optional) without those constraints, using equals would be a mistake.

@eed3si9n
Copy link
Member Author

eed3si9n commented Feb 5, 2020

@lrytz

Random idea: would it make sense to have Any.equals protected (at some point, @deprecated first), and only have Object/AnyRef.equals public?

If we keep the collaborative equality, then I think that makes sense. I'd rather see == become a symbolic alias of equals method for Scala 3.x under strictEquality.

@SethTisue SethTisue modified the milestones: 2.13.2, 2.13.3 Feb 6, 2020
@xuwei-k
Copy link
Contributor

xuwei-k commented Feb 14, 2020

FYI scala/scala3#8314

@eed3si9n eed3si9n force-pushed the wip/deprecate-anyval-equals branch from db58db9 to 1d6121b Compare February 20, 2020 15:47
@eed3si9n
Copy link
Member Author

Rebased against 2.13.x.

@SethTisue SethTisue modified the milestones: 2.13.3, 2.13.4 May 12, 2020
@dwijnand
Copy link
Member

dwijnand commented Oct 5, 2020

@sjrd given we have configurable warnings now, are you more warm to this idea? Personally I like the look of it, and I agree that if a standard library maintainer can shoot their own foot, this warning should exist.

@som-snytt
Copy link
Contributor

I'm not sure -Wconf has a cat for this. I just saw a PR with a nowarn(msg="something") to suppress it. This is so fundamental, it should be easier.

@dwijnand
Copy link
Member

I'm inclined to merge this, if you could give it a rebase pls, Eugene.

Ref scala/bug#11551
Ref https://twitter.com/not_xuwei_k/status/1135374786593468416

This extends sensible check to `equals`, to warn against using it as opposed to `==`.
@eed3si9n eed3si9n force-pushed the wip/deprecate-anyval-equals branch from 1d6121b to ec80eb4 Compare October 14, 2020 16:11
@eed3si9n
Copy link
Member Author

Rebased against 2.13.x.

@sjrd
Copy link
Member

sjrd commented Oct 14, 2020

@sjrd given we have configurable warnings now, are you more warm to this idea?

Would I be able to blanket-ignore all such warnings in the entire codebase, without adding any @nowarn in the code (because my code still compiles with 2.11 and 2.12), and without ignoring any other warning?

@NthPortal
Copy link
Contributor

NthPortal commented Oct 14, 2020

@sjrd yes. at the very least by doing -Wconf:msg=comparing values of types.*is unsafe due to cooperative equality:s, or, if it eventually gets its own category, with -Wconf:cat=lint-equals-non-ref:s or something

Edit: cat=other-non-cooperative-equals

@dwijnand dwijnand merged commit 4e92035 into scala:2.13.x Oct 14, 2020
@eed3si9n eed3si9n deleted the wip/deprecate-anyval-equals branch October 15, 2020 01:44
val actual = underlyingClass(other.tpe)
def typesString = normalizeAll(qual.tpe.widen)+" and "+normalizeAll(other.tpe.widen)
def nonSensiblyEquals() = {
reporter.warning(pos, s"comparing values of types $typesString using `${name.decode}` is unsafe due to cooperative equality; use `==` instead")
Copy link
Member

Choose a reason for hiding this comment

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

This should call refchecksWarning, going directly do reporter.warning bypasses the Wconf infrastructure. Any takers?

➜  sandbox git:(2.13.x) qs '-Wconf:msg=statement:e,msg=equality:e'
Welcome to Scala 2.13.4-20201009-113655-8fbebaa (Java HotSpot(TM) 64-Bit GraalVM EE 19.2.1, Java 1.8.0_231).
Type in expressions for evaluation. Or try :help.

scala> def f = { 1; 2 }
                 ^
       error: a pure expression does nothing in statement position; multiline expressions might require enclosing parentheses

scala> def g = 1L equals 1
                  ^
       warning: comparing values of types Long and Int using `equals` is unsafe due to cooperative equality; use `==` instead
def g: Boolean

Copy link
Contributor

Choose a reason for hiding this comment

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

I just noticed this myself, trying to fix up the warning for #9235

Copy link
Contributor

Choose a reason for hiding this comment

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

@lrytz if all it is is changing that one line, I'll take a stab

Copy link
Contributor

Choose a reason for hiding this comment

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

@lrytz what's that qs command/alias?

Copy link
Member

Choose a reason for hiding this comment

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

Likely build/quick/bin/scala or perhaps $(git root 2>/dev/null)/build/quick/bin/scala

Copy link
Member

Choose a reason for hiding this comment

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

$(git root 2>/dev/null)/build/quick/bin/scala

If only I was that smart. Though git root doesn't seem to be a built-in. Adopted, thank you.

@dwijnand dwijnand added release-notes worth highlighting in next release notes and removed release-notes worth highlighting in next release notes labels Nov 10, 2020
@SethTisue SethTisue changed the title Display warning on equals comparing non-references Display warning on equals comparing non-references Nov 18, 2020
@SethTisue
Copy link
Member

PR adjusting the wording a bit: #9320

@nchammas
Copy link

sjrd yes. at the very least by doing -Wconf:msg=comparing values of types.*is unsafe due to cooperative equality:s, or, if it eventually gets its own category, with -Wconf:cat=lint-equals-non-ref:s or something

Edit: cat=other-non-cooperative-equals

Sorry to dig up an old PR, but where can I find documentation for the other-non-cooperative-equals category?

I'm trying to find the earliest version of Scala that supports it.

@SethTisue
Copy link
Member

SethTisue commented Feb 27, 2024

@nchammas re: version, I think you would need to go digging (either in the source code, or just by trying the different versions) to determine that. -Wconf itself landed in Scala 2.13.2, then was backported to Scala 2.12.13 as well. So you don't need to look at any versions older than that.

@SethTisue
Copy link
Member

SethTisue commented Feb 27, 2024

and we don't have fine-grained documentation covering individual warning categories. the usual best available source of information, besides the source code, is the PR description of the PR that added that warning in the first place. (usually such PRs are linked from the Scala release notes.)

dongjoon-hyun pushed a commit to apache/spark that referenced this pull request Feb 28, 2024
### What changes were proposed in this pull request?

This is a backport of fcc5dbc / #45036 with a tweak so that it works on Scala 2.12.

### Why are the changes needed?

This is a correctness bug fix.

The original fix against `master` suppresses a warning category that doesn't exist on certain versions of Scala 2.13 and 2.12, and the exact versions are [not documented anywhere][1].

To be safe, this backport simply suppresses all warnings instead of just `other-non-cooperative-equals`. It would be interesting to see if `-Wconf:nowarn` complains, since the warning about non-cooperative equals itself is also not thrown on all versions of Scala, but I don't think that's a priority.

[1]: scala/scala#8120 (comment)

### Does this PR introduce _any_ user-facing change?

Yes.

### How was this patch tested?

CI + added tests.

### Was this patch authored or co-authored using generative AI tooling?

No.

Closes #45296 from nchammas/SPARK-45599-OpenHashSet.

Authored-by: Nicholas Chammas <nicholas.chammas@gmail.com>
Signed-off-by: Dongjoon Hyun <dhyun@apple.com>
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.