+ added nimsuggest support for exception inlay hints (#23202)

This adds nimsuggest support for displaying inlay hints for exceptions.
An inlay hint is displayed around function calls, that can raise an
exception, which isn't handled in the current subroutine (in other
words, exceptions that can propagate back to the caller). On mouse hover
on top of the hint, a list of exceptions that could propagate is shown.

The changes, required to support this are already commited to
nimlangserver and the VS code extension. The extension and the server
allow configuration for whether these new exception hints are enabled
(they can be enabled or disabled independently from the type hints), as
well as the inlay strings that are inserted before and after the name of
the function, around the function call. Potentially, one of these
strings can be empty, for example, the user can choose to add an inlay
hint only before the name of the function, or only after the name of the
function.
This commit is contained in:
Nikolay Nikolov
2024-03-15 19:20:10 +02:00
committed by GitHub
parent c2c00776e3
commit 899ba01ccf
7 changed files with 480 additions and 50 deletions

View File

@@ -11,9 +11,9 @@
## represents a complete Nim project. Single modules can either be kept in RAM
## or stored in a rod-file.
import std/[intsets, tables, hashes, strtabs]
import std/[intsets, tables, hashes, strtabs, algorithm]
import ../dist/checksums/src/checksums/md5
import ast, astalgo, options, lineinfos,idents, btrees, ropes, msgs, pathutils, packages
import ast, astalgo, options, lineinfos,idents, btrees, ropes, msgs, pathutils, packages, suggestsymdb
import ic / [packed_ast, ic]
@@ -55,11 +55,6 @@ type
concreteTypes*: seq[FullId]
inst*: PInstantiation
SymInfoPair* = object
sym*: PSym
info*: TLineInfo
isDecl*: bool
PipelinePass* = enum
NonePass
SemPass
@@ -108,7 +103,7 @@ type
doStopCompile*: proc(): bool {.closure.}
usageSym*: PSym # for nimsuggest
owners*: seq[PSym]
suggestSymbols*: Table[FileIndex, seq[SymInfoPair]]
suggestSymbols*: SuggestSymbolDatabase
suggestErrors*: Table[FileIndex, seq[Suggest]]
methods*: seq[tuple[methods: seq[PSym], dispatcher: PSym]] # needs serialization!
bucketTable*: CountTable[ItemId]
@@ -506,7 +501,7 @@ proc initModuleGraphFields(result: ModuleGraph) =
result.importStack = @[]
result.inclToMod = initTable[FileIndex, FileIndex]()
result.owners = @[]
result.suggestSymbols = initTable[FileIndex, seq[SymInfoPair]]()
result.suggestSymbols = initTable[FileIndex, SuggestFileSymbolDatabase]()
result.suggestErrors = initTable[FileIndex, seq[Suggest]]()
result.methods = @[]
result.compilerprocs = initStrTable()
@@ -712,16 +707,14 @@ func belongsToStdlib*(graph: ModuleGraph, sym: PSym): bool =
## Check if symbol belongs to the 'stdlib' package.
sym.getPackageSymbol.getPackageId == graph.systemModule.getPackageId
proc `==`*(a, b: SymInfoPair): bool =
result = a.sym == b.sym and a.info.exactEquals(b.info)
proc fileSymbols*(graph: ModuleGraph, fileIdx: FileIndex): seq[SymInfoPair] =
result = graph.suggestSymbols.getOrDefault(fileIdx, @[])
proc fileSymbols*(graph: ModuleGraph, fileIdx: FileIndex): SuggestFileSymbolDatabase =
result = graph.suggestSymbols.getOrDefault(fileIdx, newSuggestFileSymbolDatabase(fileIdx, optIdeExceptionInlayHints in graph.config.globalOptions))
doAssert(result.fileIndex == fileIdx)
iterator suggestSymbolsIter*(g: ModuleGraph): SymInfoPair =
for xs in g.suggestSymbols.values:
for x in xs:
yield x
for i in xs.lineInfo.low..xs.lineInfo.high:
yield xs.getSymInfoPair(i)
iterator suggestErrorsIter*(g: ModuleGraph): Suggest =
for xs in g.suggestErrors.values:

View File

@@ -86,6 +86,7 @@ type # please make sure we have under 32 options
# also: generate header file
optIdeDebug # idetools: debug mode
optIdeTerse # idetools: use terse descriptions
optIdeExceptionInlayHints
optExcessiveStackTrace # fully qualified module filenames
optShowAllMismatches # show all overloading resolution candidates
optWholeProject # for 'doc': output any dependency
@@ -298,6 +299,7 @@ type
SuggestInlayHintKind* = enum
sihkType = "Type",
sihkParameter = "Parameter"
sihkException = "Exception"
SuggestInlayHint* = ref object
kind*: SuggestInlayHintKind

View File

@@ -11,9 +11,9 @@ import
ast, astalgo, msgs, renderer, magicsys, types, idents, trees,
wordrecg, options, guards, lineinfos, semfold, semdata,
modulegraphs, varpartitions, typeallowed, nilcheck, errorhandling,
semstrictfuncs
semstrictfuncs, suggestsymdb
import std/[tables, intsets, strutils]
import std/[tables, intsets, strutils, sequtils]
when defined(nimPreviewSlimSystem):
import std/assertions
@@ -66,8 +66,12 @@ discard """
"""
type
CaughtExceptionsStack = object
nodes: seq[seq[PType]]
TEffects = object
exc: PNode # stack of exceptions
when defined(nimsuggest):
caughtExceptions: CaughtExceptionsStack
tags: PNode # list of tags
forbids: PNode # list of tags
bottom, inTryStmt, inExceptOrFinallyStmt, leftPartOfAsgn, inIfStmt, currentBlock: int
@@ -411,7 +415,7 @@ proc throws(tracked, n, orig: PNode) =
else:
tracked.add n
proc getEbase(g: ModuleGraph; info: TLineInfo): PType =
proc getEbase*(g: ModuleGraph; info: TLineInfo): PType =
result = g.sysTypeFromName(info, "Exception")
proc excType(g: ModuleGraph; n: PNode): PType =
@@ -492,6 +496,18 @@ proc catchesAll(tracked: PEffects) =
if tracked.exc.len > 0:
setLen(tracked.exc.sons, tracked.bottom)
proc push(s: var CaughtExceptionsStack) =
s.nodes.add(@[])
proc pop(s: var CaughtExceptionsStack) =
s.nodes.del(high(s.nodes))
proc addCatch(s: var CaughtExceptionsStack, e: PType) =
s.nodes[high(s.nodes)].add(e)
proc addCatchAll(s: var CaughtExceptionsStack) =
s.nodes[high(s.nodes)].add(nil)
proc track(tracked: PEffects, n: PNode)
proc trackTryStmt(tracked: PEffects, n: PNode) =
let oldBottom = tracked.bottom
@@ -500,12 +516,33 @@ proc trackTryStmt(tracked: PEffects, n: PNode) =
let oldState = tracked.init.len
var inter: TIntersection = @[]
when defined(nimsuggest):
tracked.caughtExceptions.push
for i in 1..<n.len:
let b = n[i]
if b.kind == nkExceptBranch:
if b.len == 1:
tracked.caughtExceptions.addCatchAll
else:
for j in 0..<b.len - 1:
if b[j].isInfixAs():
assert(b[j][1].kind == nkType)
tracked.caughtExceptions.addCatch(b[j][1].typ)
else:
assert(b[j].kind == nkType)
tracked.caughtExceptions.addCatch(b[j].typ)
else:
assert b.kind == nkFinally
inc tracked.inTryStmt
track(tracked, n[0])
dec tracked.inTryStmt
for i in oldState..<tracked.init.len:
addToIntersection(inter, tracked.init[i], bsNone)
when defined(nimsuggest):
tracked.caughtExceptions.pop
var branches = 1
var hasFinally = false
inc tracked.inExceptOrFinallyStmt
@@ -917,6 +954,19 @@ proc checkForSink(tracked: PEffects; n: PNode) =
if tracked.inIfStmt == 0 and optSinkInference in tracked.config.options:
checkForSink(tracked.config, tracked.c.idgen, tracked.owner, n)
proc markCaughtExceptions(tracked: PEffects; g: ModuleGraph; info: TLineInfo; s: PSym; usageSym: var PSym) =
when defined(nimsuggest):
proc internalMarkCaughtExceptions(tracked: PEffects; q: var SuggestFileSymbolDatabase; info: TLineInfo) =
var si = q.findSymInfoIndex(info)
if si != -1:
q.caughtExceptionsSet[si] = true
for w1 in tracked.caughtExceptions.nodes:
for w2 in w1:
q.caughtExceptions[si].add(w2)
if optIdeExceptionInlayHints in tracked.config.globalOptions:
internalMarkCaughtExceptions(tracked, g.suggestSymbols.mgetOrPut(info.fileIndex, newSuggestFileSymbolDatabase(info.fileIndex, true)), info)
proc trackCall(tracked: PEffects; n: PNode) =
template gcsafeAndSideeffectCheck() =
if notGcSafe(op) and not importedFromC(a):
@@ -937,6 +987,13 @@ proc trackCall(tracked: PEffects; n: PNode) =
if tracked.owner.kind != skMacro and n.typ.skipTypes(abstractVar).kind != tyOpenArray:
createTypeBoundOps(tracked, n.typ, n.info)
when defined(nimsuggest):
var actualLoc = a.info
if n.kind == nkHiddenCallConv:
actualLoc = n.info
if a.kind == nkSym:
markCaughtExceptions(tracked, tracked.graph, actualLoc, a.sym, tracked.graph.usageSym)
let notConstExpr = getConstExpr(tracked.ownerModule, n, tracked.c.idgen, tracked.graph) == nil
if notConstExpr:
if a.kind == nkCast and a[1].typ.kind == tyProc:

View File

@@ -32,7 +32,7 @@
# included from sigmatch.nim
import prefixmatches
import prefixmatches, suggestsymdb
from wordrecg import wDeprecated, wError, wAddr, wYield
import std/[algorithm, sets, parseutils, tables]
@@ -114,6 +114,10 @@ proc getTokenLenFromSource(conf: ConfigRef; ident: string; info: TLineInfo; skip
result = skipUntil(line, '`', column)
if cmpIgnoreStyle(line[column..column + result - 1], ident) != 0:
result = 0
elif column >= 0 and line[column] == '`' and isOpeningBacktick(column):
result = skipUntil(line, '`', column + 1) + 2
if cmpIgnoreStyle(line[column + 1..column + result - 2], ident) != 0:
result = 0
elif ident[0] in linter.Letters and ident[^1] != '=':
result = identLen(line, column)
if cmpIgnoreStyle(line[column..column + result - 1], ident[0..min(result-1,len(ident)-1)]) != 0:
@@ -265,7 +269,7 @@ proc `$`*(suggest: Suggest): string =
result.add(sep)
result.add($suggest.endCol)
proc suggestToSuggestInlayHint*(sug: Suggest): SuggestInlayHint =
proc suggestToSuggestInlayTypeHint*(sug: Suggest): SuggestInlayHint =
SuggestInlayHint(
kind: sihkType,
line: sug.line,
@@ -277,6 +281,30 @@ proc suggestToSuggestInlayHint*(sug: Suggest): SuggestInlayHint =
tooltip: ""
)
proc suggestToSuggestInlayExceptionHintLeft*(sug: Suggest, propagatedExceptions: seq[PType]): SuggestInlayHint =
SuggestInlayHint(
kind: sihkException,
line: sug.line,
column: sug.column,
label: "try ",
paddingLeft: false,
paddingRight: false,
allowInsert: false,
tooltip: "propagated exceptions: " & $propagatedExceptions
)
proc suggestToSuggestInlayExceptionHintRight*(sug: Suggest, propagatedExceptions: seq[PType]): SuggestInlayHint =
SuggestInlayHint(
kind: sihkException,
line: sug.line,
column: sug.column + sug.tokenLen,
label: "!",
paddingLeft: false,
paddingRight: false,
allowInsert: false,
tooltip: "propagated exceptions: " & $propagatedExceptions
)
proc suggestResult*(conf: ConfigRef; s: Suggest) =
if not isNil(conf.suggestionResultHook):
conf.suggestionResultHook(s)
@@ -534,6 +562,16 @@ proc isTracked*(current, trackPos: TLineInfo, tokenLen: int): bool =
else:
result = false
proc isTracked*(current, trackPos: TinyLineInfo, tokenLen: int): bool =
if current.line==trackPos.line:
let col = trackPos.col
if col >= current.col and col <= current.col+tokenLen-1:
result = true
else:
result = false
else:
result = false
when defined(nimsuggest):
# Since TLineInfo defined a == operator that doesn't include the column,
# we map TLineInfo to a unique int here for this lookup table:
@@ -584,7 +622,7 @@ proc suggestSym*(g: ModuleGraph; info: TLineInfo; s: PSym; usageSym: var PSym; i
## misnamed: should be 'symDeclared'
let conf = g.config
when defined(nimsuggest):
g.suggestSymbols.mgetOrPut(info.fileIndex, @[]).add SymInfoPair(sym: s, info: info, isDecl: isDecl)
g.suggestSymbols.add SymInfoPair(sym: s, info: info, isDecl: isDecl), optIdeExceptionInlayHints in g.config.globalOptions
if conf.suggestVersion == 0:
if s.allUsages.len == 0:

212
compiler/suggestsymdb.nim Normal file
View File

@@ -0,0 +1,212 @@
import std/[intsets, tables, algorithm, assertions]
import ast, lineinfos, msgs
type
PackedBoolArray* = object
s: IntSet
len: int
TinyLineInfo* = object
line*: uint16
col*: int16
SymInfoPair* = object
sym*: PSym
info*: TLineInfo
caughtExceptions*: seq[PType]
caughtExceptionsSet*: bool
isDecl*: bool
SuggestFileSymbolDatabase* = object
lineInfo*: seq[TinyLineInfo]
sym*: seq[PSym]
caughtExceptions*: seq[seq[PType]]
caughtExceptionsSet*: PackedBoolArray
isDecl*: PackedBoolArray
fileIndex*: FileIndex
trackCaughtExceptions*: bool
isSorted*: bool
SuggestSymbolDatabase* = Table[FileIndex, SuggestFileSymbolDatabase]
func newPackedBoolArray*(): PackedBoolArray =
PackedBoolArray(
s: initIntSet(),
len: 0
)
func low*(s: PackedBoolArray): int =
0
func high*(s: PackedBoolArray): int =
s.len - 1
func `[]`*(s: PackedBoolArray; idx: int): bool =
s.s.contains(idx)
proc `[]=`*(s: var PackedBoolArray; idx: int; v: bool) =
if v:
s.s.incl(idx)
else:
s.s.excl(idx)
proc add*(s: var PackedBoolArray; v: bool) =
inc(s.len)
if v:
s.s.incl(s.len - 1)
proc reverse*(s: var PackedBoolArray) =
var
reversedSet = initIntSet()
for i in 0..s.high:
if s.s.contains(i):
reversedSet.incl(s.high - i)
s.s = reversedSet
proc getSymInfoPair*(s: SuggestFileSymbolDatabase; idx: int): SymInfoPair =
SymInfoPair(
sym: s.sym[idx],
info: TLineInfo(
line: s.lineInfo[idx].line,
col: s.lineInfo[idx].col,
fileIndex: s.fileIndex
),
caughtExceptions:
if s.trackCaughtExceptions:
s.caughtExceptions[idx]
else:
@[],
caughtExceptionsSet:
if s.trackCaughtExceptions:
s.caughtExceptionsSet[idx]
else:
false,
isDecl: s.isDecl[idx]
)
proc reverse*(s: var SuggestFileSymbolDatabase) =
s.lineInfo.reverse()
s.sym.reverse()
s.caughtExceptions.reverse()
s.caughtExceptionsSet.reverse()
s.isDecl.reverse()
proc newSuggestFileSymbolDatabase*(aFileIndex: FileIndex; aTrackCaughtExceptions: bool): SuggestFileSymbolDatabase =
SuggestFileSymbolDatabase(
lineInfo: @[],
sym: @[],
caughtExceptions: @[],
caughtExceptionsSet: newPackedBoolArray(),
isDecl: newPackedBoolArray(),
fileIndex: aFileIndex,
trackCaughtExceptions: aTrackCaughtExceptions,
isSorted: true
)
proc exactEquals*(a, b: TinyLineInfo): bool =
result = a.line == b.line and a.col == b.col
proc `==`*(a, b: SymInfoPair): bool =
result = a.sym == b.sym and a.info.exactEquals(b.info)
func cmp*(a: TinyLineInfo; b: TinyLineInfo): int =
result = cmp(a.line, b.line)
if result == 0:
result = cmp(a.col, b.col)
func compare*(s: var SuggestFileSymbolDatabase; i, j: int): int =
result = cmp(s.lineInfo[i], s.lineInfo[j])
if result == 0:
result = cmp(s.isDecl[i], s.isDecl[j])
proc exchange(s: var SuggestFileSymbolDatabase; i, j: int) =
if i == j:
return
var tmp1 = s.lineInfo[i]
s.lineInfo[i] = s.lineInfo[j]
s.lineInfo[j] = tmp1
if s.trackCaughtExceptions:
var tmp2 = s.caughtExceptions[i]
s.caughtExceptions[i] = s.caughtExceptions[j]
s.caughtExceptions[j] = tmp2
var tmp3 = s.caughtExceptionsSet[i]
s.caughtExceptionsSet[i] = s.caughtExceptionsSet[j]
s.caughtExceptionsSet[j] = tmp3
var tmp4 = s.isDecl[i]
s.isDecl[i] = s.isDecl[j]
s.isDecl[j] = tmp4
var tmp5 = s.sym[i]
s.sym[i] = s.sym[j]
s.sym[j] = tmp5
proc quickSort(s: var SuggestFileSymbolDatabase; ll, rr: int) =
var
i, j, pivotIdx: int
l = ll
r = rr
while true:
i = l
j = r
pivotIdx = l + ((r - l) shr 1)
while true:
while (i < pivotIdx) and (s.compare(pivotIdx, i) > 0):
inc i
while (j > pivotIdx) and (s.compare(pivotIdx, j) < 0):
dec j
if i < j:
s.exchange(i, j)
if pivotIdx == i:
pivotIdx = j
inc i
elif pivotIdx == j:
pivotIdx = i
dec j
else:
inc i
dec j
else:
break
if (pivotIdx - l) < (r - pivotIdx):
if (l + 1) < pivotIdx:
s.quickSort(l, pivotIdx - 1)
l = pivotIdx + 1
else:
if (pivotIdx + 1) < r:
s.quickSort(pivotIdx + 1, r)
if (l + 1) < pivotIdx:
r = pivotIdx - 1
else:
break
if l >= r:
break
proc sort*(s: var SuggestFileSymbolDatabase) =
s.quickSort(s.lineInfo.low, s.lineInfo.high)
s.isSorted = true
proc add*(s: var SuggestFileSymbolDatabase; v: SymInfoPair) =
doAssert(v.info.fileIndex == s.fileIndex)
s.lineInfo.add(TinyLineInfo(
line: v.info.line,
col: v.info.col
))
s.sym.add(v.sym)
s.isDecl.add(v.isDecl)
if s.trackCaughtExceptions:
s.caughtExceptions.add(v.caughtExceptions)
s.caughtExceptionsSet.add(v.caughtExceptionsSet)
s.isSorted = false
proc add*(s: var SuggestSymbolDatabase; v: SymInfoPair; trackCaughtExceptions: bool) =
s.mgetOrPut(v.info.fileIndex, newSuggestFileSymbolDatabase(v.info.fileIndex, trackCaughtExceptions)).add(v)
proc findSymInfoIndex*(s: var SuggestFileSymbolDatabase; li: TLineInfo): int =
doAssert(li.fileIndex == s.fileIndex)
if not s.isSorted:
s.sort()
var q = TinyLineInfo(
line: li.line,
col: li.col
)
result = binarySearch(s.lineInfo, q, cmp)