1
+ package dotty .tools
2
+ package dotc
3
+ package reporting
4
+
5
+ import core ._
6
+ import Contexts ._
7
+ import Decorators .* , Symbols .* , Names .* , Types .* , Flags .*
8
+ import typer .ProtoTypes .{FunProto , SelectionProto }
9
+ import transform .SymUtils .isNoValue
10
+
11
+ /** A utility object to support "did you mean" hinting */
12
+ object DidYouMean :
13
+
14
+ def kindOK (sym : Symbol , isType : Boolean , isApplied : Boolean )(using Context ): Boolean =
15
+ if isType then sym.isType
16
+ else sym.isTerm || isApplied && sym.isClass && ! sym.is(ModuleClass )
17
+ // also count classes if followed by `(` since they have constructor proxies,
18
+ // but these don't show up separately as members
19
+ // Note: One need to be careful here not to complete symbols. For instance,
20
+ // we run into trouble if we ask whether a symbol is a legal value.
21
+
22
+ /** The names of all non-synthetic, non-private members of `site`
23
+ * that are of the same type/term kind as the missing member.
24
+ */
25
+ def memberCandidates (site : Type , isType : Boolean , isApplied : Boolean )(using Context ): collection.Set [Symbol ] =
26
+ for
27
+ bc <- site.widen.baseClasses.toSet
28
+ sym <- bc.info.decls.filter(sym =>
29
+ kindOK(sym, isType, isApplied)
30
+ && ! sym.isConstructor
31
+ && ! sym.flagsUNSAFE.isOneOf(Synthetic | Private ))
32
+ yield sym
33
+
34
+ case class Binding (name : Name , sym : Symbol , site : Type )
35
+
36
+ /** The name, symbol, and prefix type of all non-synthetic declarations that are
37
+ * defined or imported in some enclosing scope and that are of the same type/term
38
+ * kind as the missing member.
39
+ */
40
+ def inScopeCandidates (isType : Boolean , isApplied : Boolean , rootImportOK : Boolean )(using Context ): collection.Set [Binding ] =
41
+ val acc = collection.mutable.HashSet [Binding ]()
42
+ def nextInteresting (ctx : Context ): Context =
43
+ if ctx.outer.isImportContext
44
+ || ctx.outer.scope != ctx.scope
45
+ || ctx.outer.owner.isClass && ctx.outer.owner != ctx.owner
46
+ || (ctx.outer eq NoContext )
47
+ then ctx.outer
48
+ else nextInteresting(ctx.outer)
49
+
50
+ def recur ()(using Context ): Unit =
51
+ if ctx eq NoContext then
52
+ () // done
53
+ else if ctx.isImportContext then
54
+ val imp = ctx.importInfo.nn
55
+ if imp.isRootImport && ! rootImportOK then
56
+ () // done
57
+ else imp.importSym.info match
58
+ case ImportType (expr) =>
59
+ val candidates = memberCandidates(expr.tpe, isType, isApplied)
60
+ if imp.isWildcardImport then
61
+ for cand <- candidates if ! imp.excluded.contains(cand.name.toTermName) do
62
+ acc += Binding (cand.name, cand, expr.tpe)
63
+ for sel <- imp.selectors do
64
+ val selStr = sel.name.show
65
+ if sel.name == sel.rename then
66
+ for cand <- candidates if cand.name.toTermName.show == selStr do
67
+ acc += Binding (cand.name, cand, expr.tpe)
68
+ else if ! sel.isUnimport then
69
+ for cand <- candidates if cand.name.toTermName.show == selStr do
70
+ acc += Binding (sel.rename.likeSpaced(cand.name), cand, expr.tpe)
71
+ case _ =>
72
+ recur()(using nextInteresting(ctx))
73
+ else
74
+ if ctx.owner.isClass then
75
+ for sym <- memberCandidates(ctx.owner.typeRef, isType, isApplied) do
76
+ acc += Binding (sym.name, sym, ctx.owner.thisType)
77
+ else
78
+ ctx.scope.foreach: sym =>
79
+ if kindOK(sym, isType, isApplied)
80
+ && ! sym.isConstructor
81
+ && ! sym.flagsUNSAFE.is(Synthetic )
82
+ then acc += Binding (sym.name, sym, NoPrefix )
83
+ recur()(using nextInteresting(ctx))
84
+ end recur
85
+
86
+ recur()
87
+ acc
88
+ end inScopeCandidates
89
+
90
+ /** The Levenshtein distance between two strings */
91
+ def distance (s1 : String , s2 : String ): Int =
92
+ val dist = Array .ofDim[Int ](s2.length + 1 , s1.length + 1 )
93
+ for
94
+ j <- 0 to s2.length
95
+ i <- 0 to s1.length
96
+ do
97
+ dist(j)(i) =
98
+ if j == 0 then i
99
+ else if i == 0 then j
100
+ else if s2(j - 1 ) == s1(i - 1 ) then dist(j - 1 )(i - 1 )
101
+ else (dist(j - 1 )(i) min dist(j)(i - 1 ) min dist(j - 1 )(i - 1 )) + 1
102
+ dist(s2.length)(s1.length)
103
+
104
+ /** List of possible candidate names with their Levenstein distances
105
+ * to the name `from` of the missing member.
106
+ * @param maxDist Maximal number of differences to be considered for a hint
107
+ * A distance qualifies if it is at most `maxDist`, shorter than
108
+ * the lengths of both the candidate name and the missing member name
109
+ * and not greater than half the average of those lengths.
110
+ */
111
+ extension [S <: Symbol | Binding ](candidates : collection.Set [S ])
112
+ def closestTo (str : String , maxDist : Int = 3 )(using Context ): List [(Int , S )] =
113
+ def nameStr (cand : S ): String = cand match
114
+ case sym : Symbol => sym.name.show
115
+ case bdg : Binding => bdg.name.show
116
+ candidates
117
+ .toList
118
+ .map(cand => (distance(nameStr(cand), str), cand))
119
+ .filter((d, cand) =>
120
+ d <= maxDist
121
+ && d * 4 <= str.length + nameStr(cand).length
122
+ && d < str.length
123
+ && d < nameStr(cand).length)
124
+ .sortBy((d, cand) => (d, nameStr(cand))) // sort by distance first, alphabetically second
125
+
126
+ def didYouMean (candidates : List [(Int , Binding )], proto : Type , prefix : String )(using Context ): String =
127
+
128
+ def qualifies (b : Binding )(using Context ): Boolean =
129
+ try
130
+ val valueOK = proto match
131
+ case _ : SelectionProto => true
132
+ case _ => ! b.sym.isNoValue
133
+ val accessOK = b.sym.isAccessibleFrom(b.site)
134
+ valueOK && accessOK
135
+ catch case ex : Exception => false
136
+ // exceptions might arise when completing (e.g. malformed class file, or cyclic reference)
137
+
138
+ def showName (name : Name , sym : Symbol )(using Context ): String =
139
+ if sym.is(ModuleClass ) then s " ${name.show}.type "
140
+ else name.show
141
+
142
+ def alternatives (distance : Int , candidates : List [(Int , Binding )]): List [Binding ] = candidates match
143
+ case (d, b) :: rest if d == distance =>
144
+ if qualifies(b) then b :: alternatives(distance, rest) else alternatives(distance, rest)
145
+ case _ =>
146
+ Nil
147
+
148
+ def recur (candidates : List [(Int , Binding )]): String = candidates match
149
+ case (d, b) :: rest
150
+ if d != 0 || b.sym.is(ModuleClass ) => // Avoid repeating the same name in "did you mean"
151
+ if qualifies(b) then
152
+ def hint (b : Binding ) = prefix ++ showName(b.name, b.sym)
153
+ val alts = alternatives(d, rest).map(hint).take(3 )
154
+ val suffix = if alts.isEmpty then " " else alts.mkString(" or perhaps " , " or " , " ?" )
155
+ s " - did you mean ${hint(b)}? $suffix"
156
+ else
157
+ recur(rest)
158
+ case _ => " "
159
+
160
+ recur(candidates)
161
+ end didYouMean
162
+ end DidYouMean
0 commit comments