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