Skip to content

Commit d490f7d

Browse files
committed
Add Levenshtein distance for member values and types
1 parent b9e03b8 commit d490f7d

File tree

3 files changed

+76
-4
lines changed

3 files changed

+76
-4
lines changed

src/dotty/tools/dotc/reporting/diagnostic/messages.scala

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,13 +199,78 @@ object messages {
199199
case class TypeMismatch(found: Type, expected: Type, whyNoMatch: String = "", implicitFailure: String = "")(implicit ctx: Context)
200200
extends Message(7) {
201201
val kind = "Type Mismatch"
202-
private val (where, printCtx) = Formatting.disambiguateTypes(found, expected)
203-
private val (fnd, exp) = Formatting.typeDiff(found, expected)(printCtx)
204-
val msg =
202+
val msg = {
203+
val (where, printCtx) = Formatting.disambiguateTypes(found, expected)
204+
val (fnd, exp) = Formatting.typeDiff(found, expected)(printCtx)
205205
s"""|found: $fnd
206206
|required: $exp
207207
|
208208
|$where""".stripMargin + whyNoMatch + implicitFailure
209+
}
210+
211+
val explanation = ""
212+
}
213+
214+
case class NotAMember(site: Type, name: Name, selected: String)(implicit ctx: Context)
215+
extends Message(8) {
216+
val kind = "Member Not Found"
217+
218+
val msg = {
219+
import core.Flags._
220+
val maxDist = 3
221+
val decls = site.decls.flatMap { sym =>
222+
if (sym.is(Synthetic | PrivateOrLocal) || sym.isConstructor) Nil
223+
else List((sym.name.show, sym))
224+
}
225+
226+
// Calculate Levenshtein distance
227+
def distance(n1: Iterable[_], n2: Iterable[_]) =
228+
n1.foldLeft(List.range(0, n2.size)) { (prev, x) =>
229+
(prev zip prev.tail zip n2).scanLeft(prev.head + 1) {
230+
case (h, ((d, v), y)) => math.min(
231+
math.min(h + 1, v + 1),
232+
if (x == y) d else d + 1
233+
)
234+
}
235+
}.last
236+
237+
// Count number of wrong characters
238+
def incorrectChars(x: (String, Int, Symbol)): (String, Symbol, Int) = {
239+
val (currName, _, sym) = x
240+
val matching = name.show.zip(currName).foldLeft(0) {
241+
case (acc, (x,y)) => if (x != y) acc + 1 else acc
242+
}
243+
(currName, sym, matching)
244+
}
245+
246+
// Get closest match in `site`
247+
val closest =
248+
decls
249+
.map { case (n, sym) => (n, distance(n, name.show), sym) }
250+
.collect { case (n, dist, sym) if dist <= maxDist => (n, dist, sym) }
251+
.groupBy(_._2).toList
252+
.sortBy(_._1)
253+
.headOption.map(_._2).getOrElse(Nil)
254+
.map(incorrectChars).toList
255+
.sortBy(_._3)
256+
.take(1).map { case (n, sym, _) => (n, sym) }
257+
258+
val siteName = site match {
259+
case site: NamedType => site.name.show
260+
case site => i"$site"
261+
}
262+
263+
val closeMember = closest match {
264+
case (n, sym) :: Nil => hl""" - did you mean `${s"$siteName.$n"}`?"""
265+
case Nil => ""
266+
case _ => assert(
267+
false,
268+
"Could not single out one distinct member to match on input with"
269+
)
270+
}
271+
272+
ex"$selected `$name` is not a member of $site$closeMember"
273+
}
209274

210275
val explanation = ""
211276
}

src/dotty/tools/dotc/typer/TypeAssigner.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import config.Printers.typr
1212
import ast.Trees._
1313
import NameOps._
1414
import collection.mutable
15+
import reporting.diagnostic.Message
16+
import reporting.diagnostic.messages._
1517

1618
trait TypeAssigner {
1719
import tpd._
@@ -220,7 +222,7 @@ trait TypeAssigner {
220222
else ""
221223
ctx.error(
222224
if (name == nme.CONSTRUCTOR) ex"$site does not have a constructor"
223-
else ex"$kind $name is not a member of $site$addendum",
225+
else NotAMember(site, name, kind),
224226
pos)
225227
}
226228
ErrorType

tests/repl/errmsgs.check

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,9 @@ scala> abstract class C {
7373
|
7474
| where: T is a type in the initalizer of value s which is an alias of String
7575
| T' is a type in method f which is an alias of Int
76+
scala> class Foo() { def bar: Int = 1 }; val foo = new Foo(); foo.barr
77+
-- [E008] Member Not Found Error: <console> ----------------------------------------------------------------------------
78+
4 |class Foo() { def bar: Int = 1 }; val foo = new Foo(); foo.barr
79+
| ^^^^^^^^
80+
| value `barr` is not a member of Foo(foo) - did you mean `foo.bar`?
7681
scala> :quit

0 commit comments

Comments
 (0)