Skip to content

Commit 5968d41

Browse files
Merge pull request #7895 from dotty-staging/refactor-hints
Fix "did you mean" hinting
2 parents 4fd0520 + 63f87ee commit 5968d41

File tree

3 files changed

+132
-44
lines changed

3 files changed

+132
-44
lines changed

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

+34-44
Original file line numberDiff line numberDiff line change
@@ -314,56 +314,46 @@ object messages {
314314

315315
val msg: String = {
316316
import core.Flags._
317-
val maxDist = 3
318-
val decls = site.decls.toList
319-
.filter(_.isType == name.isTypeName)
320-
.flatMap { sym =>
321-
if (sym.flagsUNSAFE.isOneOf(Synthetic | PrivateLocal) || sym.isConstructor) Nil
322-
else List((sym.name.show, sym))
323-
}
317+
val maxDist = 3 // maximal number of differences to be considered for a hint
318+
val missing = name.show
319+
320+
// The names of all non-synthetic, non-private members of `site`
321+
// that are of the same type/term kind as the missing member.
322+
def candidates: Set[String] =
323+
for
324+
bc <- site.widen.baseClasses.toSet
325+
sym <- bc.info.decls.filter(sym =>
326+
sym.isType == name.isTypeName
327+
&& !sym.isConstructor
328+
&& !sym.flagsUNSAFE.isOneOf(Synthetic | Private))
329+
yield sym.name.show
324330

325331
// Calculate Levenshtein distance
326-
def distance(n1: Iterable[?], n2: Iterable[?]) =
327-
n1.foldLeft(List.range(0, n2.size)) { (prev, x) =>
328-
(prev zip prev.tail zip n2).scanLeft(prev.head + 1) {
329-
case (h, ((d, v), y)) => math.min(
330-
math.min(h + 1, v + 1),
331-
if (x == y) d else d + 1
332-
)
333-
}
334-
}.last
335-
336-
// Count number of wrong characters
337-
def incorrectChars(x: (String, Int, Symbol)): (String, Symbol, Int) = {
338-
val (currName, _, sym) = x
339-
val matching = name.show.zip(currName).foldLeft(0) {
340-
case (acc, (x,y)) => if (x != y) acc + 1 else acc
341-
}
342-
(currName, sym, matching)
343-
}
344-
345-
// Get closest match in `site`
346-
def closest: List[String] =
347-
decls
348-
.map { (n, sym) => (n, distance(n, name.show), sym) }
349-
.collect {
350-
case (n, dist, sym)
351-
if dist <= maxDist && dist < (name.toString.length min n.length) =>
352-
(n, dist, sym)
353-
}
354-
.groupBy(_._2).toList
355-
.sortBy(_._1)
356-
.headOption.map(_._2).getOrElse(Nil)
357-
.map(incorrectChars).toList
358-
.sortBy(_._3)
359-
.map(_._1)
360-
// [Martin] Note: I have no idea what this does. This shows the
361-
// pitfalls of not naming things, functional or not.
332+
def distance(s1: String, s2: String): Int =
333+
val dist = Array.ofDim[Int](s2.length + 1, s1.length + 1)
334+
for
335+
j <- 0 to s2.length
336+
i <- 0 to s1.length
337+
do
338+
dist(j)(i) =
339+
if j == 0 then i
340+
else if i == 0 then j
341+
else if s2(j - 1) == s1(i - 1) then dist(j - 1)(i - 1)
342+
else (dist(j - 1)(i) min dist(j)(i - 1) min dist(j - 1)(i - 1)) + 1
343+
dist(s2.length)(s1.length)
344+
345+
// A list of possible candidate strings with their Levenstein distances
346+
// to the name of the missing member
347+
def closest: List[(Int, String)] = candidates
348+
.toList
349+
.map(n => (distance(n.show, missing), n))
350+
.filter((d, n) => d <= maxDist && d < missing.length && d < n.length)
351+
.sorted // sort by distance first, alphabetically second
362352

363353
val finalAddendum =
364354
if addendum.nonEmpty then addendum
365355
else closest match {
366-
case n :: _ =>
356+
case (d, n) :: _ =>
367357
val siteName = site match
368358
case site: NamedType => site.name.show
369359
case site => i"$site"

tests/neg/name-hints.check

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
-- [E008] Member Not Found Error: tests/neg/name-hints.scala:6:15 ------------------------------------------------------
2+
6 | val x1 = Int.maxvalue // error
3+
| ^^^^^^^^^^^^
4+
| value maxvalue is not a member of object Int - did you mean Int.MaxValue?
5+
-- [E008] Member Not Found Error: tests/neg/name-hints.scala:7:15 ------------------------------------------------------
6+
7 | val x2 = Int.MxValue // error
7+
| ^^^^^^^^^^^
8+
| value MxValue is not a member of object Int - did you mean Int.MaxValue?
9+
-- [E008] Member Not Found Error: tests/neg/name-hints.scala:8:15 ------------------------------------------------------
10+
8 | val x3 = Int.MaxxValue // error
11+
| ^^^^^^^^^^^^^
12+
| value MaxxValue is not a member of object Int - did you mean Int.MaxValue?
13+
-- [E008] Member Not Found Error: tests/neg/name-hints.scala:10:13 -----------------------------------------------------
14+
10 | val d1 = O.abcd // error
15+
| ^^^^^^
16+
| value abcd is not a member of object O - did you mean O.abcde?
17+
-- [E008] Member Not Found Error: tests/neg/name-hints.scala:11:13 -----------------------------------------------------
18+
11 | val d2 = O.abc // error
19+
| ^^^^^
20+
| value abc is not a member of object O - did you mean O.abcde?
21+
-- [E008] Member Not Found Error: tests/neg/name-hints.scala:12:13 -----------------------------------------------------
22+
12 | val d3 = O.ab // error, no hint since distance = 3 > 2 = length
23+
| ^^^^
24+
| value ab is not a member of object O
25+
-- [E008] Member Not Found Error: tests/neg/name-hints.scala:13:13 -----------------------------------------------------
26+
13 | val s1 = O.Abcde // error
27+
| ^^^^^^^
28+
| value Abcde is not a member of object O - did you mean O.abcde?
29+
-- [E008] Member Not Found Error: tests/neg/name-hints.scala:14:13 -----------------------------------------------------
30+
14 | val s3 = O.AbCde // error
31+
| ^^^^^^^
32+
| value AbCde is not a member of object O - did you mean O.abcde?
33+
-- [E008] Member Not Found Error: tests/neg/name-hints.scala:15:13 -----------------------------------------------------
34+
15 | val s3 = O.AbCdE // error
35+
| ^^^^^^^
36+
| value AbCdE is not a member of object O - did you mean O.abcde?
37+
-- [E008] Member Not Found Error: tests/neg/name-hints.scala:16:13 -----------------------------------------------------
38+
16 | val s3 = O.AbCDE // error, no hint
39+
| ^^^^^^^
40+
| value AbCDE is not a member of object O
41+
-- [E008] Member Not Found Error: tests/neg/name-hints.scala:17:13 -----------------------------------------------------
42+
17 | val a1 = O.abcde0 // error
43+
| ^^^^^^^^
44+
| value abcde0 is not a member of object O - did you mean O.abcde?
45+
-- [E008] Member Not Found Error: tests/neg/name-hints.scala:18:13 -----------------------------------------------------
46+
18 | val a2 = O.abcde00 // error
47+
| ^^^^^^^^^
48+
| value abcde00 is not a member of object O - did you mean O.abcde?
49+
-- [E008] Member Not Found Error: tests/neg/name-hints.scala:19:13 -----------------------------------------------------
50+
19 | val a3 = O.abcde000 // error
51+
| ^^^^^^^^^^
52+
| value abcde000 is not a member of object O - did you mean O.abcde?
53+
-- [E008] Member Not Found Error: tests/neg/name-hints.scala:20:13 -----------------------------------------------------
54+
20 | val a4 = O.abcde0000 // error, no hint
55+
| ^^^^^^^^^^^
56+
| value abcde0000 is not a member of object O
57+
-- [E008] Member Not Found Error: tests/neg/name-hints.scala:22:13 -----------------------------------------------------
58+
22 | val y1 = O.x // error, no hint
59+
| ^^^
60+
| value x is not a member of object O
61+
-- [E008] Member Not Found Error: tests/neg/name-hints.scala:23:13 -----------------------------------------------------
62+
23 | val y2 = O.xY // error
63+
| ^^^^
64+
| value xY is not a member of object O - did you mean O.xy?
65+
-- [E008] Member Not Found Error: tests/neg/name-hints.scala:24:13 -----------------------------------------------------
66+
24 | val y3 = O.xyz // error
67+
| ^^^^^
68+
| value xyz is not a member of object O - did you mean O.xy?
69+
-- [E008] Member Not Found Error: tests/neg/name-hints.scala:25:13 -----------------------------------------------------
70+
25 | val y2 = O.XY // error, no hint
71+
| ^^^^
72+
| value XY is not a member of object O

tests/neg/name-hints.scala

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
object O with
2+
val abcde: Int = 0
3+
val xy: Int = 1
4+
5+
object Test with
6+
val x1 = Int.maxvalue // error
7+
val x2 = Int.MxValue // error
8+
val x3 = Int.MaxxValue // error
9+
10+
val d1 = O.abcd // error
11+
val d2 = O.abc // error
12+
val d3 = O.ab // error, no hint since distance = 3 > 2 = length
13+
val s1 = O.Abcde // error
14+
val s3 = O.AbCde // error
15+
val s3 = O.AbCdE // error
16+
val s3 = O.AbCDE // error, no hint
17+
val a1 = O.abcde0 // error
18+
val a2 = O.abcde00 // error
19+
val a3 = O.abcde000 // error
20+
val a4 = O.abcde0000 // error, no hint
21+
22+
val y1 = O.x // error, no hint
23+
val y2 = O.xY // error
24+
val y3 = O.xyz // error
25+
val y2 = O.XY // error, no hint
26+

0 commit comments

Comments
 (0)