-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Usability: compiler suggests possible names in NotAMemberError #6711
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
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
lrytz
left a comment
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
| /* 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
| 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 pImplThese 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
lrytz
left a comment
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,_1etc) 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.