Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix #2844 #3911; add --spellsuggest to suggest symbols in scope with similar spellings on undefined symbol error #16067

Merged
merged 16 commits into from
Mar 16, 2021
Merged
2 changes: 2 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,8 @@

- Deprecated `--nilseqs` which is now a noop.

- Added `--spellSuggest` to show spelling suggestions on typos.

- Source+Edit links now appear on top of every docgen'd page when
`nim doc --git.url:url ...` is given.

Expand Down
4 changes: 4 additions & 0 deletions compiler/commands.nim
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,10 @@ proc processSwitch*(switch, arg: string, pass: TCmdLinePass, info: TLineInfo;
processOnOffSwitchG(conf, {optStdout}, arg, pass, info)
of "listfullpaths":
processOnOffSwitchG(conf, {optListFullPaths}, arg, pass, info)
of "spellsuggest":
if arg.len == 0: conf.spellSuggestMax = spellSuggestSecretSauce
elif arg == "auto": conf.spellSuggestMax = spellSuggestSecretSauce
else: conf.spellSuggestMax = parseInt(arg)
of "declaredlocs":
processOnOffSwitchG(conf, {optDeclaredLocs}, arg, pass, info)
of "dynliboverride":
Expand Down
101 changes: 69 additions & 32 deletions compiler/lookups.nim
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,17 @@ proc searchInScopes*(c: PContext, s: PIdent; ambiguous: var bool): PSym =
if result != nil: return result
result = someSymFromImportTable(c, s, ambiguous)

proc debugScopes*(c: PContext; limit=0) {.deprecated.} =
proc debugScopes*(c: PContext; limit=0, max = int.high) {.deprecated.} =
var i = 0
timotheecour marked this conversation as resolved.
Show resolved Hide resolved
var count = 0
for scope in allScopes(c.currentScope):
echo "scope ", i
for h in 0..high(scope.symbols.data):
if scope.symbols.data[h] != nil:
echo scope.symbols.data[h].name.s
if i == limit: break
if count >= max: return
echo count, ": ", scope.symbols.data[h].name.s
count.inc
if i == limit: return
inc i

proc searchInScopesFilterBy*(c: PContext, s: PIdent, filter: TSymKinds): seq[PSym] =
Expand Down Expand Up @@ -354,22 +357,60 @@ proc mergeShadowScope*(c: PContext) =
else:
c.addInterfaceDecl(sym)

when defined(nimfix):
# when we cannot find the identifier, retry with a changed identifier:
proc altSpelling(x: PIdent): PIdent =
when false:
# `nimfix` used to call `altSpelling` and prettybase.replaceDeprecated(n.info, ident, alt)
proc altSpelling(c: PContext, x: PIdent): PIdent =
case x.s[0]
of 'A'..'Z': result = getIdent(toLowerAscii(x.s[0]) & x.s.substr(1))
of 'a'..'z': result = getIdent(toLowerAscii(x.s[0]) & x.s.substr(1))
of 'A'..'Z': result = getIdent(c.cache, toLowerAscii(x.s[0]) & x.s.substr(1))
of 'a'..'z': result = getIdent(c.cache, toLowerAscii(x.s[0]) & x.s.substr(1))
else: result = x

template fixSpelling(n: PNode; ident: PIdent; op: untyped) =
let alt = ident.altSpelling
result = op(c, alt).skipAlias(n)
if result != nil:
prettybase.replaceDeprecated(n.info, ident, alt)
return result
else:
template fixSpelling(n: PNode; ident: PIdent; op: untyped) = discard
import std/[editdistance, heapqueue]

type SpellCandidate = object
dist: int
depth: int
msg: string
sym: PSym

template toOrderTup(a: SpellCandidate): auto =
# `dist` is first, to favor nearby matches
# `depth` is next, to favor nearby enclosing scopes among ties
# `sym.name.s` is last, to make the list ordered and deterministic among ties
(a.dist, a.depth, a.msg)

proc `<`(a, b: SpellCandidate): bool =
a.toOrderTup < b.toOrderTup

proc fixSpelling(c: PContext, n: PNode, ident: PIdent, result: var string) =
## when we cannot find the identifier, suggest nearby spellings
if c.config.spellSuggestMax == 0: return
if c.compilesContextId > 0: return # don't slowdown inside compiles()
timotheecour marked this conversation as resolved.
Show resolved Hide resolved
var list = initHeapQueue[SpellCandidate]()
let name0 = ident.s.nimIdentNormalize

for (sym, depth, isLocal) in allSyms(c):
let depth = -depth - 1
let dist = editDistance(name0, sym.name.s.nimIdentNormalize)
var msg: string
msg.add "\n ($1, $2): '$3'" % [$dist, $depth, sym.name.s]
addDeclaredLoc(msg, c.config, sym) # `msg` needed for deterministic ordering.
list.push SpellCandidate(dist: dist, depth: depth, msg: msg, sym: sym)

if list.len == 0: return
let e0 = list[0]
var count = 0
while true:
# pending https://github.com/timotheecour/Nim/issues/373 use more efficient `itemsSorted`.
if list.len == 0: break
let e = list.pop()
if c.config.spellSuggestMax == spellSuggestSecretSauce:
if e.dist > e0.dist: break
elif count >= c.config.spellSuggestMax: break
if count == 0:
result.add "\ncandidate misspellings (edit distance, lexical scope distance): "
result.add e.msg
count.inc

proc errorUseQualifier(c: PContext; info: TLineInfo; s: PSym; amb: var bool): PSym =
var err = "ambiguous identifier: '" & s.name.s & "'"
Expand Down Expand Up @@ -406,34 +447,34 @@ proc errorUseQualifier(c: PContext; info: TLineInfo; candidates: seq[PSym]) =
inc i
localError(c.config, info, errGenerated, err)

proc errorUndeclaredIdentifier*(c: PContext; info: TLineInfo; name: string) =
var err = "undeclared identifier: '" & name & "'"
proc errorUndeclaredIdentifier*(c: PContext; info: TLineInfo; name: string, extra = "") =
var err = "undeclared identifier: '" & name & "'" & extra
if c.recursiveDep.len > 0:
err.add "\nThis might be caused by a recursive module dependency:\n"
err.add c.recursiveDep
# prevent excessive errors for 'nim check'
c.recursiveDep = ""
localError(c.config, info, errGenerated, err)

proc errorUndeclaredIdentifierHint*(c: PContext; n: PNode, ident: PIdent): PSym =
var extra = ""
fixSpelling(c, n, ident, extra)
errorUndeclaredIdentifier(c, n.info, ident.s, extra)
result = errorSym(c, n)

proc lookUp*(c: PContext, n: PNode): PSym =
# Looks up a symbol. Generates an error in case of nil.
var amb = false
case n.kind
of nkIdent:
result = searchInScopes(c, n.ident, amb).skipAlias(n, c.config)
if result == nil:
fixSpelling(n, n.ident, searchInScopes)
errorUndeclaredIdentifier(c, n.info, n.ident.s)
result = errorSym(c, n)
if result == nil: result = errorUndeclaredIdentifierHint(c, n, n.ident)
of nkSym:
result = n.sym
of nkAccQuoted:
var ident = considerQuotedIdent(c, n)
result = searchInScopes(c, ident, amb).skipAlias(n, c.config)
if result == nil:
fixSpelling(n, ident, searchInScopes)
errorUndeclaredIdentifier(c, n.info, ident.s)
result = errorSym(c, n)
if result == nil: result = errorUndeclaredIdentifierHint(c, n, ident)
else:
internalError(c.config, n.info, "lookUp")
return
Expand Down Expand Up @@ -471,9 +512,7 @@ proc qualifiedLookUp*(c: PContext, n: PNode, flags: set[TLookupFlag]): PSym =
errorUseQualifier(c, n.info, candidates)

if result == nil and checkUndeclared in flags:
fixSpelling(n, ident, searchInScopes)
errorUndeclaredIdentifier(c, n.info, ident.s)
result = errorSym(c, n)
result = errorUndeclaredIdentifierHint(c, n, ident)
elif checkAmbiguity in flags and result != nil and amb:
result = errorUseQualifier(c, n.info, result, amb)
c.isAmbiguous = amb
Expand All @@ -494,9 +533,7 @@ proc qualifiedLookUp*(c: PContext, n: PNode, flags: set[TLookupFlag]): PSym =
else:
result = someSym(c.graph, m, ident).skipAlias(n, c.config)
if result == nil and checkUndeclared in flags:
fixSpelling(n[1], ident, searchInScopes)
errorUndeclaredIdentifier(c, n[1].info, ident.s)
result = errorSym(c, n[1])
result = errorUndeclaredIdentifierHint(c, n[1], ident)
elif n[1].kind == nkSym:
result = n[1].sym
elif checkUndeclared in flags and
Expand Down
2 changes: 2 additions & 0 deletions compiler/options.nim
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ type
numberOfProcessors*: int # number of processors
lastCmdTime*: float # when caas is enabled, we measure each command
symbolFiles*: SymbolFilesOption
spellSuggestMax*: int # max number of spelling suggestions for typos

cppDefines*: HashSet[string] # (*)
headerFile*: string
Expand Down Expand Up @@ -575,6 +576,7 @@ const
htmldocsDir* = htmldocsDirname.RelativeDir
docRootDefault* = "@default" # using `@` instead of `$` to avoid shell quoting complications
oKeepVariableNames* = true
spellSuggestSecretSauce* = -1

proc mainCommandArg*(conf: ConfigRef): string =
## This is intended for commands like check or parse
Expand Down
1 change: 1 addition & 0 deletions compiler/semcall.nim
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ proc resolveOverloads(c: PContext, n, orig: PNode,

if overloadsState == csEmpty and result.state == csEmpty:
if efNoUndeclared notin flags: # for tests/pragmas/tcustom_pragma.nim
# xxx adapt/use errorUndeclaredIdentifierHint(c, n, f.ident)
localError(c.config, n.info, getMsgDiagnostic(c, flags, n, f))
return
elif result.state != csMatch:
Expand Down
3 changes: 3 additions & 0 deletions doc/advopt.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ Advanced options:
--colors:on|off turn compiler messages coloring on|off
--listFullPaths:on|off list full paths in messages
--declaredLocs:on|off show declaration locations in messages
--spellSuggest|:num show at most `num >= 0` spelling suggestions on typos.
if `num` is not specified (or `auto`), return
an implementation defined set of suggestions.
-w:on|off|list, --warnings:on|off|list
turn all warnings on|off or list all available
--warning[X]:on|off turn specific warning X on|off
Expand Down
7 changes: 7 additions & 0 deletions tests/misc/mspellsuggest.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
proc fooBar4*(a: int) = discard
var fooBar9* = 0

var fooCar* = 0
type FooBar* = int
type FooCar* = int
type GooBa* = int
45 changes: 45 additions & 0 deletions tests/misc/tspellsuggest.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
discard """
# pending bug #16521 (bug 12) use `matrix`
cmd: "nim c --spellsuggest:15 --hints:off $file"
action: "reject"
nimout: '''
tspellsuggest.nim(45, 13) Error: undeclared identifier: 'fooBar'
candidate misspellings (edit distance, lexical scope distance):
(1, 0): 'fooBar8' [var declared in tspellsuggest.nim(43, 9)]
(1, 1): 'fooBar7' [var declared in tspellsuggest.nim(41, 7)]
(1, 3): 'fooBar1' [var declared in tspellsuggest.nim(33, 5)]
(1, 3): 'fooBar2' [let declared in tspellsuggest.nim(34, 5)]
(1, 3): 'fooBar3' [const declared in tspellsuggest.nim(35, 7)]
(1, 3): 'fooBar4' [proc declared in tspellsuggest.nim(36, 6)]
(1, 3): 'fooBar5' [template declared in tspellsuggest.nim(37, 10)]
(1, 3): 'fooBar6' [macro declared in tspellsuggest.nim(38, 7)]
(1, 5): 'FooBar' [type declared in mspellsuggest.nim(5, 6)]
(1, 5): 'fooBar4' [proc declared in mspellsuggest.nim(1, 6)]
(1, 5): 'fooBar9' [var declared in mspellsuggest.nim(2, 5)]
(1, 5): 'fooCar' [var declared in mspellsuggest.nim(4, 5)]
(2, 5): 'FooCar' [type declared in mspellsuggest.nim(6, 6)]
(2, 5): 'GooBa' [type declared in mspellsuggest.nim(7, 6)]
(3, 0): 'fooBarBaz' [const declared in tspellsuggest.nim(44, 11)]
'''
"""

# tests `--spellsuggest:num`



# line 30
import ./mspellsuggest

var fooBar1 = 0
let fooBar2 = 0
const fooBar3 = 0
proc fooBar4() = discard
template fooBar5() = discard
macro fooBar6() = discard

proc main =
var fooBar7 = 0
block:
var fooBar8 = 0
const fooBarBaz = 0
let x = fooBar
45 changes: 45 additions & 0 deletions tests/misc/tspellsuggest2.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
discard """
# pending bug #16521 (bug 12) use `matrix`
cmd: "nim c --spellsuggest --hints:off $file"
action: "reject"
nimout: '''
tspellsuggest2.nim(45, 13) Error: undeclared identifier: 'fooBar'
candidate misspellings (edit distance, lexical scope distance):
(1, 0): 'fooBar8' [var declared in tspellsuggest2.nim(43, 9)]
(1, 1): 'fooBar7' [var declared in tspellsuggest2.nim(41, 7)]
(1, 3): 'fooBar1' [var declared in tspellsuggest2.nim(33, 5)]
(1, 3): 'fooBar2' [let declared in tspellsuggest2.nim(34, 5)]
(1, 3): 'fooBar3' [const declared in tspellsuggest2.nim(35, 7)]
(1, 3): 'fooBar4' [proc declared in tspellsuggest2.nim(36, 6)]
(1, 3): 'fooBar5' [template declared in tspellsuggest2.nim(37, 10)]
(1, 3): 'fooBar6' [macro declared in tspellsuggest2.nim(38, 7)]
(1, 5): 'FooBar' [type declared in mspellsuggest.nim(5, 6)]
(1, 5): 'fooBar4' [proc declared in mspellsuggest.nim(1, 6)]
(1, 5): 'fooBar9' [var declared in mspellsuggest.nim(2, 5)]
(1, 5): 'fooCar' [var declared in mspellsuggest.nim(4, 5)]
'''
"""

# tests `--spellsuggest`






# line 30
import ./mspellsuggest

var fooBar1 = 0
let fooBar2 = 0
const fooBar3 = 0
proc fooBar4() = discard
template fooBar5() = discard
macro fooBar6() = discard

proc main =
var fooBar7 = 0
block:
var fooBar8 = 0
const fooBarBaz = 0
let x = fooBar