From f2e2e3f8ed6a47d2ec84ea32b43905dc527858c9 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 23 Oct 2023 10:24:34 +0200 Subject: [PATCH 1/3] Give "did you mean ...?" hints also for simple identifiers Fixes #18682 --- .../tools/dotc/reporting/DidYouMean.scala | 150 ++++++++++++++++++ .../dotty/tools/dotc/reporting/messages.scala | 92 +++++------ .../src/dotty/tools/dotc/typer/Checking.scala | 2 +- .../dotty/tools/dotc/typer/TypeAssigner.scala | 6 +- .../src/dotty/tools/dotc/typer/Typer.scala | 4 +- tests/neg-macros/i15009a.check | 2 +- tests/neg/i13320.check | 2 +- tests/neg/i16653.check | 2 +- tests/neg/i18682.check | 32 ++++ tests/neg/i18682.scala | 16 ++ tests/neg/name-hints.check | 4 +- tests/neg/name-hints.scala | 2 +- tests/neg/yimports-stable.check | 4 +- 13 files changed, 249 insertions(+), 69 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/reporting/DidYouMean.scala create mode 100644 tests/neg/i18682.check create mode 100644 tests/neg/i18682.scala diff --git a/compiler/src/dotty/tools/dotc/reporting/DidYouMean.scala b/compiler/src/dotty/tools/dotc/reporting/DidYouMean.scala new file mode 100644 index 000000000000..c8c109709236 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/reporting/DidYouMean.scala @@ -0,0 +1,150 @@ +package dotty.tools +package dotc +package reporting + +import core._ +import Contexts._ +import Decorators.*, Symbols.*, Names.*, Types.*, Flags.* +import typer.ProtoTypes.{FunProto, SelectionProto} +import transform.SymUtils.isNoValue + +/** A utility object to support "did you mean" hinting */ +object DidYouMean: + + def kindOK(sym: Symbol, isType: Boolean, isApplied: Boolean)(using Context): Boolean = + if isType then sym.isType + else sym.isTerm || isApplied && sym.isClass && !sym.is(ModuleClass) + // also count classes if followed by `(` since they have constructor proxies, + // but these don't show up separately as members + // Note: One need to be careful here not to complete symbols. For instance, + // we run into trouble if we ask whether a symbol is a legal value. + + /** The names of all non-synthetic, non-private members of `site` + * that are of the same type/term kind as the missing member. + */ + def memberCandidates(site: Type, isType: Boolean, isApplied: Boolean)(using Context): collection.Set[Symbol] = + for + bc <- site.widen.baseClasses.toSet + sym <- bc.info.decls.filter(sym => + kindOK(sym, isType, isApplied) + && !sym.isConstructor + && !sym.flagsUNSAFE.isOneOf(Synthetic | Private)) + yield sym + + case class Binding(name: Name, sym: Symbol, site: Type) + + /** The name, symbol, and prefix type of all non-synthetic declarations that are + * defined or imported in some enclosing scope and that are of the same type/term + * kind as the missing member. + */ + def inScopeCandidates(isType: Boolean, isApplied: Boolean, rootImportOK: Boolean)(using Context): collection.Set[Binding] = + val acc = collection.mutable.HashSet[Binding]() + def nextInteresting(ctx: Context): Context = + if ctx.outer.isImportContext + || ctx.outer.scope != ctx.scope + || ctx.outer.owner.isClass && ctx.outer.owner != ctx.owner + || (ctx.outer eq NoContext) + then ctx.outer + else nextInteresting(ctx.outer) + + def recur()(using Context): Unit = + if ctx eq NoContext then + () // done + else if ctx.isImportContext then + val imp = ctx.importInfo.nn + if imp.isRootImport && !rootImportOK then + () // done + else imp.importSym.info match + case ImportType(expr) => + val candidates = memberCandidates(expr.tpe, isType, isApplied) + if imp.isWildcardImport then + for cand <- candidates if !imp.excluded.contains(cand.name.toTermName) do + acc += Binding(cand.name, cand, expr.tpe) + for sel <- imp.selectors do + val selStr = sel.name.show + if sel.name == sel.rename then + for cand <- candidates if cand.name.toTermName.show == selStr do + acc += Binding(cand.name, cand, expr.tpe) + else if !sel.isUnimport then + for cand <- candidates if cand.name.toTermName.show == selStr do + acc += Binding(sel.rename.likeSpaced(cand.name), cand, expr.tpe) + case _ => + recur()(using nextInteresting(ctx)) + else + if ctx.owner.isClass then + for sym <- memberCandidates(ctx.owner.typeRef, isType, isApplied) do + acc += Binding(sym.name, sym, ctx.owner.thisType) + else + ctx.scope.foreach: sym => + if kindOK(sym, isType, isApplied) + && !sym.isConstructor + && !sym.flagsUNSAFE.is(Synthetic) + then acc += Binding(sym.name, sym, NoPrefix) + recur()(using nextInteresting(ctx)) + end recur + + recur() + acc + end inScopeCandidates + + /** The Levenshtein distance between two strings */ + def distance(s1: String, s2: String): Int = + val dist = Array.ofDim[Int](s2.length + 1, s1.length + 1) + for + j <- 0 to s2.length + i <- 0 to s1.length + do + dist(j)(i) = + if j == 0 then i + else if i == 0 then j + else if s2(j - 1) == s1(i - 1) then dist(j - 1)(i - 1) + else (dist(j - 1)(i) min dist(j)(i - 1) min dist(j - 1)(i - 1)) + 1 + dist(s2.length)(s1.length) + + /** List of possible candidate names with their Levenstein distances + * to the name `from` of the missing member. + * @param maxDist Maximal number of differences to be considered for a hint + * A distance qualifies if it is at most `maxDist`, shorter than + * the lengths of both the candidate name and the missing member name + * and not greater than half the average of those lengths. + */ + extension [S <: Symbol | Binding](candidates: collection.Set[S]) + def closestTo(str: String, maxDist: Int = 3)(using Context): List[(Int, S)] = + def nameStr(cand: S): String = cand match + case sym: Symbol => sym.name.show + case bdg: Binding => bdg.name.show + candidates + .toList + .map(cand => (distance(nameStr(cand), str), cand)) + .filter((d, cand) => + d <= maxDist + && d * 4 <= str.length + nameStr(cand).length + && d < str.length + && d < nameStr(cand).length) + .sortBy((d, cand) => (d, nameStr(cand))) // sort by distance first, alphabetically second + + def didYouMean(candidates: List[(Int, Binding)], proto: Type, prefix: String)(using Context): String = + + def qualifies(b: Binding)(using Context): Boolean = + proto match + case _: SelectionProto => true + case _ => + try !b.sym.isNoValue + catch case ex: Exception => false + + def showName(name: Name, sym: Symbol)(using Context): String = + if sym.is(ModuleClass) then s"${name.show}.type" + else name.show + + def recur(candidates: List[(Int, Binding)]): String = candidates match + case (d, b) :: rest + if d != 0 || b.sym.is(ModuleClass) => // Avoid repeating the same name in "did you mean" + if qualifies(b) then + s" - did you mean $prefix${showName(b.name, b.sym)}?" + else + recur(rest) + case _ => "" + + recur(candidates) + end didYouMean +end DidYouMean \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 673e3dcc243d..331a4e6797eb 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -16,7 +16,7 @@ import ErrorMessageID._ import ast.Trees import config.{Feature, ScalaVersion} import typer.ErrorReporting.{err, matchReductionAddendum, substitutableTypeSymbolsInScope} -import typer.ProtoTypes.ViewProto +import typer.ProtoTypes.{ViewProto, SelectionProto, FunProto} import typer.Implicits.* import typer.Inferencing import scala.util.control.NonFatal @@ -34,6 +34,7 @@ import dotty.tools.dotc.util.Spans.Span import dotty.tools.dotc.util.SourcePosition import scala.jdk.CollectionConverters.* import dotty.tools.dotc.util.SourceFile +import DidYouMean.* /** Messages * ======== @@ -243,14 +244,29 @@ extends NamingMsg(DuplicateBindID) { } } -class MissingIdent(tree: untpd.Ident, treeKind: String, val name: Name)(using Context) +class MissingIdent(tree: untpd.Ident, treeKind: String, val name: Name, proto: Type)(using Context) extends NotFoundMsg(MissingIdentID) { - def msg(using Context) = i"Not found: $treeKind$name" + def msg(using Context) = + val missing = name.show + val addendum = + didYouMean( + inScopeCandidates(name.isTypeName, isApplied = proto.isInstanceOf[FunProto], rootImportOK = true) + .closestTo(missing), + proto, "") + + i"Not found: $treeKind$name$addendum" def explain(using Context) = { - i"""|The identifier for `$treeKind$name` is not bound, that is, - |no declaration for this identifier can be found. - |That can happen, for example, if `$name` or its declaration has either been - |misspelt or if an import is missing.""" + i"""|Each identifier in Scala needs a matching declaration. There are two kinds of + |identifiers: type identifiers and value identifiers. Value identifiers are introduced + |by `val`, `def`, or `object` declarations. Type identifiers are introduced by `type`, + |`class`, or `trait` declarations. + | + |Identifiers refer to matching declarations in their environment, or they can be + |imported from elsewhere. + | + |Possible reasons why no matching declaration was found: + | - The declaration or the use is mis-spelt. + | - An import is missing.""" } } @@ -309,48 +325,13 @@ class TypeMismatch(found: Type, expected: Type, inTree: Option[untpd.Tree], adde end TypeMismatch -class NotAMember(site: Type, val name: Name, selected: String, addendum: => String = "")(using Context) +class NotAMember(site: Type, val name: Name, selected: String, proto: Type, addendum: => String = "")(using Context) extends NotFoundMsg(NotAMemberID), ShowMatchTrace(site) { //println(i"site = $site, decls = ${site.decls}, source = ${site.typeSymbol.sourceFile}") //DEBUG def msg(using Context) = { - import core.Flags._ - val maxDist = 3 // maximal number of differences to be considered for a hint val missing = name.show - // The symbols of all non-synthetic, non-private members of `site` - // that are of the same type/term kind as the missing member. - def candidates: Set[Symbol] = - for - bc <- site.widen.baseClasses.toSet - sym <- bc.info.decls.filter(sym => - sym.isType == name.isTypeName - && !sym.isConstructor - && !sym.flagsUNSAFE.isOneOf(Synthetic | Private)) - yield sym - - // Calculate Levenshtein distance - def distance(s1: String, s2: String): Int = - val dist = Array.ofDim[Int](s2.length + 1, s1.length + 1) - for - j <- 0 to s2.length - i <- 0 to s1.length - do - dist(j)(i) = - if j == 0 then i - else if i == 0 then j - else if s2(j - 1) == s1(i - 1) then dist(j - 1)(i - 1) - else (dist(j - 1)(i) min dist(j)(i - 1) min dist(j - 1)(i - 1)) + 1 - dist(s2.length)(s1.length) - - // A list of possible candidate symbols with their Levenstein distances - // to the name of the missing member - def closest: List[(Int, Symbol)] = candidates - .toList - .map(sym => (distance(sym.name.show, missing), sym)) - .filter((d, sym) => d <= maxDist && d < missing.length && d < sym.name.show.length) - .sortBy((d, sym) => (d, sym.name.show)) // sort by distance first, alphabetically second - val enumClause = if ((name eq nme.values) || (name eq nme.valueOf)) && site.classSymbol.companionClass.isEnumClass then val kind = if name eq nme.values then i"${nme.values} array" else i"${nme.valueOf} lookup method" @@ -367,17 +348,18 @@ extends NotFoundMsg(NotAMemberID), ShowMatchTrace(site) { val finalAddendum = if addendum.nonEmpty then prefixEnumClause(addendum) - else closest match - case (d, sym) :: _ => - val siteName = site match - case site: NamedType => site.name.show - case site => i"$site" - val showName = - // Add .type to the name if it is a module - if sym.is(ModuleClass) then s"${sym.name.show}.type" - else sym.name.show - s" - did you mean $siteName.$showName?$enumClause" - case Nil => prefixEnumClause("") + else + val hint = didYouMean( + memberCandidates(site, name.isTypeName, isApplied = proto.isInstanceOf[FunProto]) + .closestTo(missing) + .map((d, sym) => (d, Binding(sym.name, sym, site))), + proto, + prefix = site match + case site: NamedType => i"${site.name}." + case site => i"$site." + ) + if hint.isEmpty then prefixEnumClause("") + else hint ++ enumClause i"$selected $name is not a member of ${site.widen}$finalAddendum" } @@ -876,7 +858,7 @@ extends Message(PatternMatchExhaustivityID) { val pathes = List( ActionPatch( - srcPos = endPos, + srcPos = endPos, replacement = uncoveredCases.map(c => indent(s"case $c => ???", startColumn)) .mkString("\n", "\n", "") ), diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 51cf019a2f85..ecace20883c2 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -1529,7 +1529,7 @@ trait Checking { && !qualType.member(sel.name).exists && !qualType.member(sel.name.toTypeName).exists then - report.error(NotAMember(qualType, sel.name, "value"), sel.imported.srcPos) + report.error(NotAMember(qualType, sel.name, "value", WildcardType), sel.imported.srcPos) if sel.isUnimport then if originals.contains(sel.name) then report.error(UnimportedAndImported(sel.name, targets.contains(sel.name)), sel.imported.srcPos) diff --git a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala index a08bf1fc96b4..8ded39030a1e 100644 --- a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala +++ b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala @@ -165,7 +165,7 @@ trait TypeAssigner { def importSuggestionAddendum(pt: Type)(using Context): String = "" - def notAMemberErrorType(tree: untpd.Select, qual: Tree)(using Context): ErrorType = + def notAMemberErrorType(tree: untpd.Select, qual: Tree, proto: Type)(using Context): ErrorType = val qualType = qual.tpe.widenIfUnstable def kind = if tree.isType then "type" else "value" val foundWithoutNull = qualType match @@ -177,7 +177,7 @@ trait TypeAssigner { def addendum = err.selectErrorAddendum(tree, qual, qualType, importSuggestionAddendum, foundWithoutNull) val msg: Message = if tree.name == nme.CONSTRUCTOR then em"$qualType does not have a constructor" - else NotAMember(qualType, tree.name, kind, addendum) + else NotAMember(qualType, tree.name, kind, proto, addendum) errorType(msg, tree.srcPos) def inaccessibleErrorType(tpe: NamedType, superAccess: Boolean, pos: SrcPos)(using Context): Type = @@ -206,7 +206,7 @@ trait TypeAssigner { def assignType(tree: untpd.Select, qual: Tree)(using Context): Select = val rawType = selectionType(tree, qual) val checkedType = ensureAccessible(rawType, qual.isInstanceOf[Super], tree.srcPos) - val ownType = checkedType.orElse(notAMemberErrorType(tree, qual)) + val ownType = checkedType.orElse(notAMemberErrorType(tree, qual, WildcardType)) assignType(tree, ownType) /** Normalize type T appearing in a new T by following eta expansions to diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 431e863f85d2..d4459de63630 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -665,7 +665,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer then // we are in the arguments of a this(...) constructor call errorTree(tree, em"$tree is not accessible from constructor arguments") else - errorTree(tree, MissingIdent(tree, kind, name)) + errorTree(tree, MissingIdent(tree, kind, name, pt)) end typedIdent /** (1) If this reference is neither applied nor selected, check that it does @@ -754,7 +754,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case rawType: NamedType => inaccessibleErrorType(rawType, superAccess, tree.srcPos) case _ => - notAMemberErrorType(tree, qual)) + notAMemberErrorType(tree, qual, pt)) end typedSelect def typedSelect(tree: untpd.Select, pt: Type)(using Context): Tree = { diff --git a/tests/neg-macros/i15009a.check b/tests/neg-macros/i15009a.check index 7f12378b2158..7e154c2be1c9 100644 --- a/tests/neg-macros/i15009a.check +++ b/tests/neg-macros/i15009a.check @@ -31,6 +31,6 @@ -- [E006] Not Found Error: tests/neg-macros/i15009a.scala:12:2 --------------------------------------------------------- 12 | $int // error: Not found: $int | ^^^^ - | Not found: $int + | Not found: $int - did you mean int? | | longer explanation available when compiling with `-explain` diff --git a/tests/neg/i13320.check b/tests/neg/i13320.check index 1e336d8fa7bf..557846cc7d7e 100644 --- a/tests/neg/i13320.check +++ b/tests/neg/i13320.check @@ -9,4 +9,4 @@ -- [E008] Not Found Error: tests/neg/i13320.scala:4:22 ----------------------------------------------------------------- 4 |var x: Foo.Booo = Foo.Booo // error // error | ^^^^^^^^ - | value Booo is not a member of object Foo - did you mean Foo.Boo? \ No newline at end of file + | value Booo is not a member of object Foo - did you mean Foo.Boo? diff --git a/tests/neg/i16653.check b/tests/neg/i16653.check index dd5c756f6f79..1ed7a1dbbc8e 100644 --- a/tests/neg/i16653.check +++ b/tests/neg/i16653.check @@ -1,6 +1,6 @@ -- [E006] Not Found Error: tests/neg/i16653.scala:1:7 ------------------------------------------------------------------ 1 |import demo.implicits._ // error | ^^^^ - | Not found: demo + | Not found: demo - did you mean Demo? | | longer explanation available when compiling with `-explain` diff --git a/tests/neg/i18682.check b/tests/neg/i18682.check new file mode 100644 index 000000000000..a1d80aa3cd56 --- /dev/null +++ b/tests/neg/i18682.check @@ -0,0 +1,32 @@ +-- [E006] Not Found Error: tests/neg/i18682.scala:3:8 ------------------------------------------------------------------ +3 |val _ = Fop(1) // error + | ^^^ + | Not found: Fop - did you mean Foo? + | + | longer explanation available when compiling with `-explain` +-- [E006] Not Found Error: tests/neg/i18682.scala:4:12 ----------------------------------------------------------------- +4 |val _ = new Fooo(2) // error + | ^^^^ + | Not found: type Fooo - did you mean Foo? + | + | longer explanation available when compiling with `-explain` +-- [E006] Not Found Error: tests/neg/i18682.scala:6:8 ------------------------------------------------------------------ +6 |val _ = hellx // error + | ^^^^^ + | Not found: hellx - did you mean hello? + | + | longer explanation available when compiling with `-explain` +-- [E008] Not Found Error: tests/neg/i18682.scala:13:12 ---------------------------------------------------------------- +13 |val _ = bar.Bap // error, App does shown as hint, too far away + | ^^^^^^^ + | value Bap is not a member of object Bar +-- [E008] Not Found Error: tests/neg/i18682.scala:14:12 ---------------------------------------------------------------- +14 |val _ = bar.Bap() // error + | ^^^^^^^ + | value Bap is not a member of object Bar - did you mean bar.Baz? +-- [E006] Not Found Error: tests/neg/i18682.scala:16:8 ----------------------------------------------------------------- +16 |val _ = error // error, java.lang.Error does not show as hint, since it is not a value + | ^^^^^ + | Not found: error + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/i18682.scala b/tests/neg/i18682.scala new file mode 100644 index 000000000000..72ecbeaf4342 --- /dev/null +++ b/tests/neg/i18682.scala @@ -0,0 +1,16 @@ +class Foo(x: Int) + +val _ = Fop(1) // error +val _ = new Fooo(2) // error +val hello = "hi" +val _ = hellx // error + +object Bar: + class Baz() + object App + +val bar = Bar +val _ = bar.Bap // error, App does shown as hint, too far away +val _ = bar.Bap() // error + +val _ = error // error, java.lang.Error does not show as hint, since it is not a value \ No newline at end of file diff --git a/tests/neg/name-hints.check b/tests/neg/name-hints.check index 324416d08c96..bac56c0c0b76 100644 --- a/tests/neg/name-hints.check +++ b/tests/neg/name-hints.check @@ -31,9 +31,9 @@ | ^^^^^^^ | value AbCde is not a member of object O - did you mean O.abcde? -- [E008] Not Found Error: tests/neg/name-hints.scala:15:13 ------------------------------------------------------------ -15 | val s3 = O.AbCdE // error +15 | val s3 = O.AbcdE // error | ^^^^^^^ - | value AbCdE is not a member of object O - did you mean O.abcde? + | value AbcdE is not a member of object O - did you mean O.abcde? -- [E008] Not Found Error: tests/neg/name-hints.scala:16:13 ------------------------------------------------------------ 16 | val s3 = O.AbCDE // error, no hint | ^^^^^^^ diff --git a/tests/neg/name-hints.scala b/tests/neg/name-hints.scala index cb4cb8884087..114053a0b673 100644 --- a/tests/neg/name-hints.scala +++ b/tests/neg/name-hints.scala @@ -12,7 +12,7 @@ object Test: val d3 = O.ab // error, no hint since distance = 3 > 2 = length val s1 = O.Abcde // error val s3 = O.AbCde // error - val s3 = O.AbCdE // error + val s3 = O.AbcdE // error val s3 = O.AbCDE // error, no hint val a1 = O.abcde0 // error val a2 = O.abcde00 // error diff --git a/tests/neg/yimports-stable.check b/tests/neg/yimports-stable.check index c5bfd914ae07..6a0b059de908 100644 --- a/tests/neg/yimports-stable.check +++ b/tests/neg/yimports-stable.check @@ -3,12 +3,12 @@ error: bad preamble import hello.world.potions -- [E006] Not Found Error: tests/neg/yimports-stable/C_2.scala:4:9 ----------------------------------------------------- 4 | val v: Numb = magic // error // error | ^^^^ - | Not found: type Numb + | Not found: type Numb - did you mean Null? | | longer explanation available when compiling with `-explain` -- [E006] Not Found Error: tests/neg/yimports-stable/C_2.scala:4:16 ---------------------------------------------------- 4 | val v: Numb = magic // error // error | ^^^^^ - | Not found: magic + | Not found: magic - did you mean main? | | longer explanation available when compiling with `-explain` From 5ce1ac9a72b21e7a09ad88055ef24a83a0bd3b9d Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 23 Oct 2023 11:31:55 +0200 Subject: [PATCH 2/3] Improvements to "did you mean ...?" scheme 1. Only show accessible members 2. Show several alternatives if they are at same distance. Unlike Scala 2, we do not show alternatives at larger distance. I fear that would produce more noise than signal. Fixes #17067 --- .../tools/dotc/reporting/DidYouMean.scala | 24 +++++++++++---- tests/neg/i18682.check | 30 +++++++++++++++---- tests/neg/i18682.scala | 19 ++++++++++-- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/reporting/DidYouMean.scala b/compiler/src/dotty/tools/dotc/reporting/DidYouMean.scala index c8c109709236..f78fd3bd190b 100644 --- a/compiler/src/dotty/tools/dotc/reporting/DidYouMean.scala +++ b/compiler/src/dotty/tools/dotc/reporting/DidYouMean.scala @@ -126,21 +126,33 @@ object DidYouMean: def didYouMean(candidates: List[(Int, Binding)], proto: Type, prefix: String)(using Context): String = def qualifies(b: Binding)(using Context): Boolean = - proto match - case _: SelectionProto => true - case _ => - try !b.sym.isNoValue - catch case ex: Exception => false + try + val valueOK = proto match + case _: SelectionProto => true + case _ => !b.sym.isNoValue + val accessOK = b.sym.isAccessibleFrom(b.site) + valueOK && accessOK + catch case ex: Exception => false + // exceptions might arise when completing (e.g. malformed class file, or cyclic reference) def showName(name: Name, sym: Symbol)(using Context): String = if sym.is(ModuleClass) then s"${name.show}.type" else name.show + def alternatives(distance: Int, candidates: List[(Int, Binding)]): List[Binding] = candidates match + case (d, b) :: rest if d == distance => + if qualifies(b) then b :: alternatives(distance, rest) else alternatives(distance, rest) + case _ => + Nil + def recur(candidates: List[(Int, Binding)]): String = candidates match case (d, b) :: rest if d != 0 || b.sym.is(ModuleClass) => // Avoid repeating the same name in "did you mean" if qualifies(b) then - s" - did you mean $prefix${showName(b.name, b.sym)}?" + def hint(b: Binding) = prefix ++ showName(b.name, b.sym) + val alts = alternatives(d, rest).map(hint).take(3) + val suffix = if alts.isEmpty then "" else alts.mkString(" or perhaps ", " or ", "?") + s" - did you mean ${hint(b)}?$suffix" else recur(rest) case _ => "" diff --git a/tests/neg/i18682.check b/tests/neg/i18682.check index a1d80aa3cd56..650204ebfbdb 100644 --- a/tests/neg/i18682.check +++ b/tests/neg/i18682.check @@ -16,17 +16,35 @@ | Not found: hellx - did you mean hello? | | longer explanation available when compiling with `-explain` --- [E008] Not Found Error: tests/neg/i18682.scala:13:12 ---------------------------------------------------------------- -13 |val _ = bar.Bap // error, App does shown as hint, too far away +-- [E008] Not Found Error: tests/neg/i18682.scala:16:12 ---------------------------------------------------------------- +16 |val _ = bar.Bap // error, App does not show as hint, too far away | ^^^^^^^ | value Bap is not a member of object Bar --- [E008] Not Found Error: tests/neg/i18682.scala:14:12 ---------------------------------------------------------------- -14 |val _ = bar.Bap() // error +-- [E008] Not Found Error: tests/neg/i18682.scala:17:12 ---------------------------------------------------------------- +17 |val _ = bar.Bap() // error | ^^^^^^^ | value Bap is not a member of object Bar - did you mean bar.Baz? --- [E006] Not Found Error: tests/neg/i18682.scala:16:8 ----------------------------------------------------------------- -16 |val _ = error // error, java.lang.Error does not show as hint, since it is not a value +-- [E006] Not Found Error: tests/neg/i18682.scala:19:8 ----------------------------------------------------------------- +19 |val _ = error // error, java.lang.Error does not show as hint, since it is not a value | ^^^^^ | Not found: error | | longer explanation available when compiling with `-explain` +-- [E008] Not Found Error: tests/neg/i18682.scala:22:50 ---------------------------------------------------------------- +22 |val _ = "123".view.reverse.padTo(5, '0').iterator.reverse // error, no hint since `reversed` is not accessible + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | value reverse is not a member of Iterator[Char] +-- [E006] Not Found Error: tests/neg/i18682.scala:27:8 ----------------------------------------------------------------- +27 |val _ = pool // error + | ^^^^ + | Not found: pool - did you mean cool? or perhaps wool? + | + | longer explanation available when compiling with `-explain` +-- [E008] Not Found Error: tests/neg/i18682.scala:29:12 ---------------------------------------------------------------- +29 |val _ = bar.poodle // error + | ^^^^^^^^^^ + | value poodle is not a member of object Bar - did you mean bar.pool? +-- [E008] Not Found Error: tests/neg/i18682.scala:31:12 ---------------------------------------------------------------- +31 |val _ = bar.ool // error + | ^^^^^^^ + | value ool is not a member of object Bar - did you mean bar.cool? or perhaps bar.pool or bar.wool? diff --git a/tests/neg/i18682.scala b/tests/neg/i18682.scala index 72ecbeaf4342..d1478ebf6e84 100644 --- a/tests/neg/i18682.scala +++ b/tests/neg/i18682.scala @@ -8,9 +8,24 @@ val _ = hellx // error object Bar: class Baz() object App + def cool = 1 + def wool = 2 + def pool = 3 val bar = Bar -val _ = bar.Bap // error, App does shown as hint, too far away +val _ = bar.Bap // error, App does not show as hint, too far away val _ = bar.Bap() // error -val _ = error // error, java.lang.Error does not show as hint, since it is not a value \ No newline at end of file +val _ = error // error, java.lang.Error does not show as hint, since it is not a value + +// #17067 +val _ = "123".view.reverse.padTo(5, '0').iterator.reverse // error, no hint since `reversed` is not accessible + +val cool = "cool" +val wool = "wool" + +val _ = pool // error + +val _ = bar.poodle // error + +val _ = bar.ool // error From 094c7aa4bb3bf04ea7ddc545f59fc99e30ad8d27 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 25 Oct 2023 10:36:45 +0200 Subject: [PATCH 3/3] Update compiler/src/dotty/tools/dotc/reporting/messages.scala Co-authored-by: Jamie Thompson --- compiler/src/dotty/tools/dotc/reporting/messages.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 331a4e6797eb..5f5d3fc9c4af 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -259,7 +259,7 @@ extends NotFoundMsg(MissingIdentID) { i"""|Each identifier in Scala needs a matching declaration. There are two kinds of |identifiers: type identifiers and value identifiers. Value identifiers are introduced |by `val`, `def`, or `object` declarations. Type identifiers are introduced by `type`, - |`class`, or `trait` declarations. + |`class`, `enum`, or `trait` declarations. | |Identifiers refer to matching declarations in their environment, or they can be |imported from elsewhere.