Skip to content

Commit 481a5c1

Browse files
authored
Integrates nim check as opt-out replacement for nimsuggest chk (#271)
* wip nimcheck * Integrates nim check into the ls * Fixes an issue where configuration wasnt properly updated. Opt-out `useNimCheck` * Fixes a crash. Uses `nim` project path for `nim check`
1 parent 5f3657a commit 481a5c1

File tree

4 files changed

+215
-22
lines changed

4 files changed

+215
-22
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ Note when in a nimble project, `nimble` will drive the entry points for `nimsugg
109109
- `nim.notificationVerbosity` - configure the verbosity of notifications. Can be set to `"none"`, `"error"`, `"warning"`, or `"info"`.
110110
- `nim.formatOnSave` - format the file on save. Requires `nph` to be available in the PATH.
111111
- `nim.nimsuggestIdleTimeout` - the timeout in ms after which an idle `nimsuggest` will be stopped. If not specified the default is 120 seconds.
112-
112+
- `nim.useNimCheck` - use `nim check` instead of `nimsuggest` for linting. Defaults to `true`.
113113
## Features
114114

115115
`nimlangserver` supports the following LSP features:

ls.nim

Lines changed: 112 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import
2020
uri,
2121
json_serialization,
2222
json_rpc/[servers/socketserver],
23-
regex
23+
regex,
24+
nimcheck
2425

2526
proc getVersionFromNimble(): string =
2627
#We should static run nimble dump instead
@@ -38,6 +39,7 @@ const
3839
LSPVersion* = getVersionFromNimble()
3940
CRLF* = "\r\n"
4041
CONTENT_LENGTH* = "Content-Length: "
42+
USE_NIM_CHECK_BY_DEFAULT* = false
4143
type
4244
NlsNimsuggestConfig* = ref object of RootObj
4345
projectFile*: string
@@ -83,6 +85,7 @@ type
8385
notificationVerbosity*: Option[NlsNotificationVerbosity]
8486
formatOnSave*: Option[bool]
8587
nimsuggestIdleTimeout*: Option[int] #idle timeout in ms
88+
useNimCheck*: Option[bool]
8689

8790
NlsFileInfo* = ref object of RootObj
8891
projectFile*: Future[string]
@@ -287,9 +290,19 @@ proc getWorkspaceConfiguration*(
287290
#TODO review and handle project specific confs when received instead of reliying in this func
288291
if ls.workspaceConfiguration.finished:
289292
return parseWorkspaceConfiguration(ls.workspaceConfiguration.read)
290-
else:
291-
return NlsConfig()
293+
return NlsConfig()
292294
except CatchableError as ex:
295+
error "Failed to get workspace configuration", error = ex.msg
296+
writeStackTrace(ex)
297+
298+
proc getAndWaitForWorkspaceConfiguration*(
299+
ls: LanguageServer
300+
): Future[NlsConfig] {.async.} =
301+
try:
302+
let conf = await ls.workspaceConfiguration
303+
return parseWorkspaceConfiguration(conf)
304+
except CatchableError as ex:
305+
error "Failed to get workspace configuration", error = ex.msg
293306
writeStackTrace(ex)
294307

295308
proc showMessage*(
@@ -462,6 +475,17 @@ proc getNimSuggestPathAndVersion(
462475
ls.showMessage(fmt "Using {nimVersion}", MessageType.Info)
463476
(nimsuggestPath, nimVersion)
464477

478+
proc getNimPath*(conf: NlsConfig): Option[string] =
479+
if conf.nimSuggestPath.isSome:
480+
some(conf.nimSuggestPath.get.parentDir / "nim")
481+
else:
482+
let path = findExe "nim"
483+
if path != "":
484+
some(path)
485+
else:
486+
warn "Failed to find nim path"
487+
none(string)
488+
465489
proc getProjectFileAutoGuess*(ls: LanguageServer, fileUri: string): string =
466490
let file = fileUri.decodeUrl
467491
debug "Auto-guessing project file for", file = file
@@ -580,6 +604,18 @@ proc toUtf16Pos*(
580604
if pos.isSome:
581605
result.column = pos.get()
582606

607+
proc toUtf16Pos*(checkResult: CheckResult, ls: LanguageServer): CheckResult =
608+
result = checkResult
609+
let uri = pathToUri(checkResult.file)
610+
let pos = toUtf16Pos(ls, uri, checkResult.line - 1, checkResult.column)
611+
if pos.isSome:
612+
result.column = pos.get()
613+
614+
for i in 0..<result.stacktrace.len:
615+
let stPos = toUtf16Pos(ls, uri, result.stacktrace[i].line - 1, result.stacktrace[i].column)
616+
if stPos.isSome:
617+
result.stacktrace[i].column = stPos.get()
618+
583619
proc range*(startLine, startCharacter, endLine, endCharacter: int): Range =
584620
return
585621
Range %* {
@@ -619,13 +655,39 @@ proc toDiagnostic(suggest: Suggest): Diagnostic =
619655
}
620656
return node.to(Diagnostic)
621657

622-
proc sendDiagnostics*(ls: LanguageServer, diagnostics: seq[Suggest], path: string) =
658+
proc toDiagnostic(checkResult: CheckResult): Diagnostic =
659+
let
660+
textStart = checkResult.msg.find('\'')
661+
textEnd = checkResult.msg.rfind('\'')
662+
endColumn =
663+
if textStart >= 0 and textEnd >= 0 and textEnd > textStart:
664+
checkResult.column + utf16Len(checkResult.msg[textStart + 1 ..< textEnd])
665+
else:
666+
checkResult.column + 1
667+
668+
let node =
669+
%*{
670+
"uri": pathToUri(checkResult.file),
671+
"range": range(checkResult.line - 1, max(0, checkResult.column), checkResult.line - 1, max(0, endColumn)),
672+
"severity":
673+
case checkResult.severity
674+
of "Error": DiagnosticSeverity.Error.int
675+
of "Hint": DiagnosticSeverity.Hint.int
676+
of "Warning": DiagnosticSeverity.Warning.int
677+
else: DiagnosticSeverity.Error.int
678+
,
679+
"message": checkResult.msg,
680+
"source": "nim",
681+
"code": "nim check",
682+
}
683+
return node.to(Diagnostic)
684+
685+
proc sendDiagnostics*(ls: LanguageServer, diagnostics: seq[Suggest] | seq[CheckResult], path: string) =
623686
debug "Sending diagnostics", count = diagnostics.len, path = path
624687
let params =
625688
PublishDiagnosticsParams %*
626689
{"uri": pathToUri(path), "diagnostics": diagnostics.map(x => x.toUtf16Pos(ls).toDiagnostic)}
627690
ls.notify("textDocument/publishDiagnostics", %params)
628-
629691
if diagnostics.len != 0:
630692
ls.filesWithDiags.incl path
631693
else:
@@ -638,6 +700,40 @@ proc tryGetNimsuggest*(
638700
proc checkProject*(ls: LanguageServer, uri: string): Future[void] {.async, gcsafe.} =
639701
if not ls.getWorkspaceConfiguration.await().autoCheckProject.get(true):
640702
return
703+
let conf = await ls.getAndWaitForWorkspaceConfiguration()
704+
let useNimCheck = conf.useNimCheck.get(USE_NIM_CHECK_BY_DEFAULT)
705+
706+
let nimPath = getNimPath(conf)
707+
708+
if useNimCheck and nimPath.isSome:
709+
proc getFilePath(c: CheckResult): string = c.file
710+
711+
let token = fmt "Checking {uri}"
712+
ls.workDoneProgressCreate(token)
713+
ls.progress(token, "begin", fmt "Checking project {uri}")
714+
if uri == "":
715+
warn "Checking project with empty uri", uri = uri
716+
ls.progress(token, "end")
717+
return
718+
let diagnostics = await nimCheck(uriToPath(uri), nimPath.get)
719+
let filesWithDiags = diagnostics.map(r => r.file).toHashSet
720+
721+
ls.progress(token, "end")
722+
723+
debug "Found diagnostics", file = filesWithDiags
724+
for (path, diags) in groupBy(diagnostics, getFilePath):
725+
ls.sendDiagnostics(diags, path)
726+
727+
# clean files with no diags
728+
for path in ls.filesWithDiags:
729+
if not filesWithDiags.contains path:
730+
debug "Sending zero diags", path = path
731+
let params =
732+
PublishDiagnosticsParams %* {"uri": pathToUri(path), "diagnostics": @[]}
733+
ls.notify("textDocument/publishDiagnostics", %params)
734+
ls.filesWithDiags = filesWithDiags
735+
return
736+
641737
debug "Running diagnostics", uri = uri
642738
let ns = ls.tryGetNimsuggest(uri).await
643739
if ns.isNone:
@@ -923,15 +1019,22 @@ proc warnIfUnknown*(
9231019
)
9241020

9251021
proc checkFile*(ls: LanguageServer, uri: string): Future[void] {.async.} =
926-
debug "Checking", uri = uri
1022+
let conf = await ls.getAndWaitForWorkspaceConfiguration()
1023+
let useNimCheck = conf.useNimCheck.get(USE_NIM_CHECK_BY_DEFAULT)
1024+
let nimPath = conf.getNimPath()
9271025
let token = fmt "Checking file {uri}"
9281026
ls.workDoneProgressCreate(token)
9291027
ls.progress(token, "begin", fmt "Checking {uri.uriToPath}")
9301028

931-
let
932-
path = uriToPath(uri)
933-
ns = await ls.tryGetNimsuggest(uri)
1029+
let path = uriToPath(uri)
1030+
1031+
if useNimCheck and nimPath.isSome:
1032+
let checkResults = await nimCheck(uriToPath(uri), nimPath.get)
1033+
ls.progress(token, "end")
1034+
ls.sendDiagnostics(checkResults, path)
1035+
return
9341036

1037+
let ns = await ls.tryGetNimsuggest(uri)
9351038
if ns.isSome:
9361039
let diagnostics = ns.get().chkFile(path, ls.uriToStash(uri)).await()
9371040
ls.progress(token, "end")

nimcheck.nim

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import std/[strutils]
2+
import regex
3+
import chronos, chronos/asyncproc
4+
import stew/[byteutils]
5+
import chronicles
6+
import protocol/types
7+
8+
type
9+
CheckStacktrace* = object
10+
file*: string
11+
line*: int
12+
column*: int
13+
msg*: string
14+
15+
CheckResult* = object
16+
file*: string
17+
line*: int
18+
column*: int
19+
msg*: string
20+
severity*: string
21+
stacktrace*: seq[CheckStacktrace]
22+
23+
proc parseCheckResults(lines: seq[string]): seq[CheckResult] =
24+
result = @[]
25+
var
26+
messageText = ""
27+
stacktrace: seq[CheckStacktrace]
28+
lastFile, lastLineStr, lastCharStr: string
29+
m: RegexMatch2
30+
31+
let dotsPattern = re2"^\.+$"
32+
let errorPattern = re2"^([^(]+)\((\d+),\s*(\d+)\)\s*(\w+):\s*(.*)$"
33+
34+
for line in lines:
35+
let line = line.strip()
36+
37+
if line.startsWith("Hint: used config file") or line == "" or line.match(dotsPattern):
38+
continue
39+
40+
if not find(line, errorPattern, m):
41+
if messageText.len < 1024:
42+
messageText &= "\n" & line
43+
else:
44+
try:
45+
let
46+
file = line[m.captures[0]]
47+
lineStr = line[m.captures[1]]
48+
charStr = line[m.captures[2]]
49+
severity = line[m.captures[3]]
50+
msg = line[m.captures[4]]
51+
52+
let
53+
lineNum = parseInt(lineStr)
54+
colNum = parseInt(charStr)
55+
56+
result.add(CheckResult(
57+
file: file,
58+
line: lineNum,
59+
column: colNum,
60+
msg: msg,
61+
severity: severity,
62+
stacktrace: @[]
63+
))
64+
65+
except Exception as e:
66+
error "Error processing line", line = line, msg = e.msg
67+
continue
68+
69+
if messageText.len > 0 and result.len > 0:
70+
result[^1].msg &= "\n" & messageText
71+
72+
73+
proc nimCheck*(filePath: string, nimPath: string): Future[seq[CheckResult]] {.async.} =
74+
debug "nimCheck", filePath = filePath, nimPath = nimPath
75+
let process = await startProcess(
76+
nimPath,
77+
arguments = @["check", "--listFullPaths", filePath],
78+
options = {UsePath},
79+
stderrHandle = AsyncProcess.Pipe,
80+
stdoutHandle = AsyncProcess.Pipe,
81+
)
82+
let res = await process.waitForExit(InfiniteDuration)
83+
debug "nimCheck exit", res = res
84+
var output = ""
85+
if res == 0: #Nim check return 0 if there are no errors but we still need to check for hints and warnings
86+
output = string.fromBytes(process.stdoutStream.read().await)
87+
else:
88+
output = string.fromBytes(process.stderrStream.read().await)
89+
90+
let lines = output.splitLines()
91+
parseCheckResults(lines)

utils.nim

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,23 @@ import std/[unicode, uri, strformat, os, strutils, options, json, jsonutils, sug
22
import chronos, chronicles
33
import "$nim/compiler/pathutils"
44
import json_rpc/private/jrpc_sys
5-
65
type
76
FingerTable = seq[tuple[u16pos, offset: int]]
87

98
UriParseError* = object of Defect
109
uri: string
1110

11+
proc writeStackTrace*(ex = getCurrentException()) =
12+
try:
13+
if ex != nil:
14+
stderr.write "An exception occured \n"
15+
stderr.write ex.msg & "\n"
16+
stderr.write ex.getStackTrace()
17+
else:
18+
stderr.write getStackTrace()
19+
except IOError:
20+
discard
21+
1222
proc createUTFMapping*(line: string): FingerTable =
1323
var pos = 0
1424
for rune in line.runes:
@@ -156,17 +166,6 @@ proc catchOrQuit*(error: Exception) =
156166
fatal "Fatal exception reached", err = error.msg, stackTrace = getStackTrace()
157167
quit 1
158168

159-
proc writeStackTrace*(ex = getCurrentException()) =
160-
try:
161-
if ex != nil:
162-
stderr.write "An exception occured \n"
163-
stderr.write ex.msg & "\n"
164-
stderr.write ex.getStackTrace()
165-
else:
166-
stderr.write getStackTrace()
167-
except IOError:
168-
discard
169-
170169
proc traceAsyncErrors*(fut: Future) =
171170
fut.addCallback do(data: pointer):
172171
if not fut.error.isNil:

0 commit comments

Comments
 (0)