From b7e599adb9f016bdaed11ca04ab4c8d5f6607aa4 Mon Sep 17 00:00:00 2001 From: Wojciech Mazur Date: Sun, 23 Jun 2024 00:40:57 +0200 Subject: [PATCH 1/4] Give "did you mean ...?" hints also for simple identifiers Fixes #18682 [Cherry-picked f2e2e3f8ed6a47d2ec84ea32b43905dc527858c9][modified] --- .../tools/dotc/reporting/DidYouMean.scala | 150 ++++++++++++++++++ .../dotty/tools/dotc/reporting/messages.scala | 90 +++++------ .../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, 248 insertions(+), 68 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 0656c563522c..faf5d82e477b 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" } diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index b95bdaf54165..5e9f9c0a3e00 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -1540,7 +1540,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 seen.contains(sel.name) then report.error(ImportRenamedTwice(sel.imported), sel.imported.srcPos) seen += sel.name diff --git a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala index b57866d43a37..052d633e0cea 100644 --- a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala +++ b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala @@ -161,7 +161,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 @@ -173,7 +173,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 = @@ -202,7 +202,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 bf6e0631ca10..b073d8d413e0 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -656,7 +656,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 @@ -745,7 +745,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 d9a9024353f3895bf373af51ee7754138d315c0d Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 23 Oct 2023 11:31:55 +0200 Subject: [PATCH 2/4] 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 [Cherry-picked 5ce1ac9a72b21e7a09ad88055ef24a83a0bd3b9d] --- .../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 b2e31fe00e84129c08c26b1941569d291537573a Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 25 Oct 2023 10:36:45 +0200 Subject: [PATCH 3/4] Update compiler/src/dotty/tools/dotc/reporting/messages.scala Co-authored-by: Jamie Thompson [Cherry-picked 094c7aa4bb3bf04ea7ddc545f59fc99e30ad8d27] --- 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 faf5d82e477b..8ea18f7bdfd0 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. From c6130760180c159fcd60ba5dede74d7311acaac2 Mon Sep 17 00:00:00 2001 From: Wojciech Mazur Date: Sun, 23 Jun 2024 00:45:45 +0200 Subject: [PATCH 4/4] Add untpd.ImportSelector#isUnimport --- compiler/src/dotty/tools/dotc/ast/untpd.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 3c51aa7f6a21..d0deb4d21d58 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -137,6 +137,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { val rename: TermName = renamed match case Ident(rename: TermName) => rename case _ => name + + def isUnimport = rename == nme.WILDCARD } case class Number(digits: String, kind: NumberKind)(implicit @constructorOnly src: SourceFile) extends TermTree