-
Notifications
You must be signed in to change notification settings - Fork 3.1k
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
Usability: compiler suggests possible names in NotAMemberError #6711
Conversation
case _ => | ||
val threshold = 2 | ||
val x = name.toString | ||
val names = target.nonPrivateMembers.toList.map(_.name.toString).distinct |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.toVector
instead ? I do not know the size of nonPrivateMembers
but it sounds significant and a Vector would perform better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
target.nonPrivateMembers
returns Scope containing method names and type names available under some package or object so the size would be around 20 most of the times? Scope implements only .toList
, so if we were to use Vector
I'd need to call .toList.toVector
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Different codebase, still scopes.. 😀
This is great. I feel like I'm in one of those dystopian novels where the heroes go back in time to fix things, and now there is peace, harmony, and income equality. I looked at t3118 just for fun. Suggesting Ideally, we'd advise only solutions that type check, but maybe there are some heuristics to apply, such as does it have an apply method if the wrong expr is an apply. Similarly for a new thing. Even so, better selection is a win. "We have a fine selection of selections that may fulfill your needs. May I suggest |
Thank you :)
In general, all 1 letter suggestions are shots in the dark, since they can be reached from any other 1 letter input with 2 edit distance. I probably should ignore them for now.
That would be amazing, but I don't have enough charges left on my portal gun. |
I was going to joke that 1-letter alternates should be within a certain distance on the keyboard, but then you'd have to know which keyboard. |
val x = name.toString | ||
if (x.size < 2) Vector() | ||
else { | ||
val names = target.nonPrivateMembers.toList.toVector.map(_.name.toString).distinct |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nonPrivateMembers.iterator.filterNot(m => m.isConstructor).map(_.name.decode).filter(_.length > 2).distinct.toList
This does it in a single traversal. Using the decoded name to avoid the issue below:
scala> object C { def <= = 0 }
defined object C
warning: previously defined class C is not a companion to object C.
Companions must be defined together; you may wish to use :paste mode for this.
scala> C.lesseq
^
error: value lesseq is not a member of object C
did you mean $less$eq?
I think filtering short names also here makes sense. Doing it after decoding removes operators like <=
.
I think it still includes too much, for example
scala> case class C(x: Int)
scala> typeOf[C].nonPrivateMembers.iterator.filterNot(m => m.isConstructor).map(_.name.decode).filter(_.length > 2).distinct.toVector
res32: scala.collection.immutable.Vector[String] = Vector(equals, toString, hashCode, canEqual, productIterator, productElement, productArity, productPrefix, copy$default$1, copy, $asInstanceOf, $isInstanceOf, synchronized, finalize, wait, notifyAll, notify, clone, getClass, asInstanceOf, isInstanceOf)
We should exclude copy$default$1
, $asInstanceOf
, $isInstanceOf
. But filtering all of m.isSynthetic
is too much
scala> typeOf[C].nonPrivateMembers.iterator.filterNot(m => m.isSynthetic || m.isConstructor).map(_.name.decode).filter(_.length > 2).distinct.toVector
res33: scala.collection.immutable.Vector[String] = Vector(synchronized, finalize, wait, notifyAll, notify, clone, getClass, asInstanceOf, isInstanceOf)
We lose copy
, hashCode
, ...
Maybe we can do it by character (.contains("$")
)? Since we're using decoded names, $
should only show up in names that are not supposed to be used by the user.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could also delay the toList
until after the filters below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a few more comments
val owner = qual.tpe.typeSymbol | ||
val target = qual.tpe.widen | ||
def targetKindString = if (owner.isTypeParameterOrSkolem) "type parameter " else "" | ||
def nameString = decodeWithKind(name, owner) | ||
/* Illuminating some common situations and errors a bit further. */ | ||
def addendum = { | ||
val companion = { | ||
val companionSymbol: Option[Symbol] = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We usually use NoSymbol
instead of None: Option[Symbol]
. Then we can also remove the pattern match below.
// find out all the names available under target within 2 edit distances | ||
lazy val alternatives: Vector[String] = { | ||
val editThreshold = 2 | ||
val x = name.toString |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be name.decode
val x = name.toString | ||
if (x.size < 2) Vector() | ||
else { | ||
val names = target.nonPrivateMembers.toList.toVector.map(_.name.toString).distinct |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could also delay the toList
until after the filters below
else { | ||
val names = target.nonPrivateMembers.toList.toVector.map(_.name.toString).distinct | ||
names filter { n => | ||
EditDistance.levenshtein(n, x) <= editThreshold |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should do some pre-filtering, only compute the distance if math.abs(n.length - x.length) <= 2
.
names filter { n => | ||
EditDistance.levenshtein(n, x) <= editThreshold | ||
} filterNot { s => | ||
(s == x) || EditDistance.excludeMethods(s) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is cheaper than computing the distance, so should be done first
val owner = qual.tpe.typeSymbol | ||
val target = qual.tpe.widen | ||
def targetKindString = if (owner.isTypeParameterOrSkolem) "type parameter " else "" | ||
def nameString = decodeWithKind(name, owner) | ||
/* Illuminating some common situations and errors a bit further. */ | ||
def addendum = { | ||
val companionSymbol: Symbol = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this code ever/often called in type errors generated during failed branches of implicit search? If so, we should disable the (expensive) addendum creation. We recently did something similar
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure if it's called in the implicit search.
Inside typedSelect (
def typedSelect(tree: Tree, qual: Tree, name: Name): Tree = { |
val qual1 = adaptToMemberWithArgs(tree, qual, name, mode)
happens at line 4988, which I am guessing is where implicit search is done.At line 5036, typedSelect calls
lookupInQualifier
, which can call NotAMemberError
. Could that mean that we're ok?
An experiment I did was to add sys.error(..)
in alternatives
below, and then added
object Suggest {
implicit class RichInt(val i: Int) {
def flippity: Unit = ()
}
1.flippity
}
to pos/implicits-new.scala
test, and it ran without blowing up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could run all of partest --pos
and -partest --run
with that patch? If that goes well, I think we don't need to add any guards.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lrytz Good call. I am seeing a bunch of errors, but they do not look like implicit searches. Most of the failures look like spec-arrays-pos.log
:
error: Int $plus$eq List()
likely caused by something like i += 1
.
Only other kind that I noticed that doesn't involve $eq
is t8013-pos.log
etc:
error: t8013.Perverse.Impervolator pImpl List()
where pImpl
shows up as a macro implementation:
def p(args: Any*): String = macro pImpl
These occur seldom enough, so I think they are ok, but I'm going to put a guard for x.endsWith("=")
.
// find out all the names available under target within 2 edit distances | ||
lazy val alternatives: List[String] = { | ||
val editThreshold = 2 | ||
val x = name.toString |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
name.decode
else | ||
alternatives match { | ||
case Nil => "" | ||
case xs => "\ndid you mean " + StringUtil.oxford(xs, "or") + "?" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should abbreviate this list if xs.length
is unreasonably large.
(n != x) && | ||
!n.contains("$") && | ||
EditDistance.levenshtein(n, x) <= editThreshold) | ||
.distinct.toList |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably only suggest term/type names depending on whether name
is a term/type name.
This reminds me of a solution I submitted to Ruby Quiz way back in 2006
👴 |
val x = name.toString | ||
if (x.size < 2) Nil | ||
else { | ||
target.nonPrivateMembers.iterator |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to look at all members and filter by those that are accessible at the callsite's Context
. We should them be careful to exclude fields:
.filterNot(m => nme.isLocalName(m.name)))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added 1e8ec90 to address the accessibility.
Some test |
Yes. I've been only doing
|
48b64f6
to
ddf17e5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good to me otherwise!
5ab5944
to
08b27c3
Compare
Is it deemed not useful to provide at least as many suggestions as completions?
Folks working in Java are likely to do this:
Yes! That's exactly what I meant!
List to sea means your ship is in trouble. This is the one I wanted to test:
|
Wouldn't that require presentation compiler etc? Maybe we could work on that in another round of PR if that's feasible. I'd like to see this PR land on 2.13 if possible. |
lazy val alternatives: List[String] = { | ||
val editThreshold = 2 | ||
val x = name.decode | ||
if ((x.size < 2) || x.endsWith("=")) Nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@retronym, I assume you meant adding context.openImplicits.nonEmpty
to the if condition, so that we don't suggest alternatives when an implicit search is underway? With that, does this LGTY?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added 96e590d.
96e590d
to
0f303bb
Compare
Rebased on 2.13.x to make sure there are no semantic merge conflicts. I'll merge this once it goes green. |
Fixes scala/bug#10181 With this change, NotAMemberError checks for possible members under target with edit distance of 2 or 1. ```scala scala> import scala.io.StdIn.{readline, readInt} ^ error: value readline is not a member of object scala.io.StdIn did you mean readLine? scala> import scala.io.stdin.{readLine => line} ^ error: object stdin is not a member of package io did you mean StdIn? scala> import scala.sth ^ error: object sth is not a member of package scala did you mean sys or math? ```
0f303bb
to
1db387a
Compare
Added file headers to the new files, and squashed. |
🎉 thank you, @eed3si9n! |
this is very, very cool |
I am happy that this landed in time for 2.13. |
FWIW, dotty has had an implementation of something like this for a couple of years: https://github.com/lampepfl/dotty/blob/614265d779cd601de10edd23eea93598cd824c21//compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala#L310-L364 It'd be good to synchronize the two implementations. |
Fixes scala/bug#10181
With this change, NotAMemberError checks for possible members under target with edit distance of 2 or 1. To avoid false positives, candidates with size <2 (
eq
,ne
,_1
etc) are filtered out.note
See also
-Ysuggest-idents
(3e9e4ec), which got axed apparently for possible "performance impact". Unlike that, this is a bit more constrained because I am just looking up the members oftarget
, I think.