diff --git a/analysis/src/Cli.ml b/analysis/src/Cli.ml index a49326d92..7e0f1bb77 100644 --- a/analysis/src/Cli.ml +++ b/analysis/src/Cli.ml @@ -10,6 +10,7 @@ API examples: ./rescript-editor-analysis.exe hover src/MyFile.res 10 2 ./rescript-editor-analysis.exe references src/MyFile.res 10 2 ./rescript-editor-analysis.exe rename src/MyFile.res 10 2 foo + ./rescript-editor-analysis.exe diagnosticSyntax src/MyFile.res Dev-time examples: ./rescript-editor-analysis.exe dump src/MyFile.res src/MyFile2.res @@ -60,6 +61,10 @@ Options: ./rescript-editor-analysis.exe format src/MyFile.res + diagnosticSyntax: print to stdout diagnostic for syntax + + ./rescript-editor-analysis.exe diagnosticSyntax src/MyFile.res + test: run tests specified by special comments in file src/MyFile.res ./rescript-editor-analysis.exe test src/src/MyFile.res @@ -88,6 +93,8 @@ let main () = Commands.codeAction ~path ~pos:(int_of_string line, int_of_string col) ~currentFile ~debug:false + | [_; "diagnosticSyntax"; path;] -> + Commands.diagnosticSyntax ~path | _ :: "reanalyze" :: _ -> let len = Array.length Sys.argv in for i = 1 to len - 2 do diff --git a/analysis/src/Commands.ml b/analysis/src/Commands.ml index 65066f8a8..48c4d6338 100644 --- a/analysis/src/Commands.ml +++ b/analysis/src/Commands.ml @@ -257,6 +257,10 @@ let format ~path = signature else "" +let diagnosticSyntax ~path = + print_endline + (Diagnostics.document_syntax ~path |> Protocol.array) + let test ~path = Uri.stripPath := true; match Files.readFile path with @@ -378,6 +382,7 @@ let test ~path = Printf.printf "%s\nnewText:\n%s<--here\n%s%s\n" (Protocol.stringifyRange range) indent indent newText))) + | "dia" -> diagnosticSyntax ~path | _ -> ()); print_newline ()) in diff --git a/analysis/src/Diagnostics.ml b/analysis/src/Diagnostics.ml new file mode 100644 index 000000000..5ef3673dd --- /dev/null +++ b/analysis/src/Diagnostics.ml @@ -0,0 +1,33 @@ +let document_syntax ~path = + let get_diagnostics diagnostics = + diagnostics + |> List.map (fun diagnostic -> + let _, startline, startcol = + Location.get_pos_info (Res_diagnostics.getStartPos diagnostic) + in + let _, endline, endcol = + Location.get_pos_info (Res_diagnostics.getEndPos diagnostic) + in + Protocol.stringifyDiagnostic + { + range = + { + start = {line = startline - 1; character = startcol}; + end_ = {line = endline - 1; character = endcol}; + }; + message = Res_diagnostics.explain diagnostic; + severity = 1; + }) + in + if FindFiles.isImplementation path then + let parseImplementation = + Res_driver.parsingEngine.parseImplementation ~forPrinter:false + ~filename:path + in + get_diagnostics parseImplementation.diagnostics + else if FindFiles.isInterface path then + let parseInterface = + Res_driver.parsingEngine.parseInterface ~forPrinter:false ~filename:path + in + get_diagnostics parseInterface.diagnostics + else [] \ No newline at end of file diff --git a/analysis/src/Protocol.ml b/analysis/src/Protocol.ml index 076137842..4a03b1508 100644 --- a/analysis/src/Protocol.ml +++ b/analysis/src/Protocol.ml @@ -10,11 +10,16 @@ type completionItem = { documentation: markupContent option; } -type hover = string -type location = {uri: string; range: range} -type documentSymbolItem = {name: string; kind: int; location: location} -type renameFile = {oldUri: string; newUri: string} -type textEdit = {range: range; newText: string} +type location = {uri : string; range : range} +type documentSymbolItem = {name : string; kind : int; location : location} +type renameFile = {oldUri : string; newUri : string} +type textEdit = {range : range; newText : string} + +type diagnostic = { + range : range; + message : string; + severity : int; +} type optionalVersionedTextDocumentIdentifier = { version: int option; @@ -89,7 +94,7 @@ let stringifyRenameFile {oldUri; newUri} = }|} (Json.escape oldUri) (Json.escape newUri) -let stringifyTextEdit te = +let stringifyTextEdit (te : textEdit) = Printf.sprintf {|{ "range": %s, "newText": "%s" @@ -126,3 +131,14 @@ let stringifyCodeAction ca = Printf.sprintf {|{"title": "%s", "kind": "%s", "edit": %s}|} ca.title (codeActionKindToString ca.codeActionKind) (ca.edit |> stringifyCodeActionEdit) + +(* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnostic *) +let stringifyDiagnostic d = + Printf.sprintf {|{ + "range": %s, + "message": "%s", + "severity": %d, + "source": "ReScript" +}|} + (stringifyRange d.range) (Json.escape d.message) + d.severity \ No newline at end of file diff --git a/analysis/tests/not_compiled/Diagnostics.res b/analysis/tests/not_compiled/Diagnostics.res new file mode 100644 index 000000000..c81f1e63e --- /dev/null +++ b/analysis/tests/not_compiled/Diagnostics.res @@ -0,0 +1,5 @@ +let = 1 + 1.0 +let add = =2 +lett a = 2 + +//^dia \ No newline at end of file diff --git a/analysis/tests/not_compiled/expected/Diagnostics.res.txt b/analysis/tests/not_compiled/expected/Diagnostics.res.txt new file mode 100644 index 000000000..a5df33b71 --- /dev/null +++ b/analysis/tests/not_compiled/expected/Diagnostics.res.txt @@ -0,0 +1,17 @@ +[{ + "range": {"start": {"line": 2, "character": 4}, "end": {"line": 2, "character": 6}}, + "message": "consecutive statements on a line must be separated by ';' or a newline", + "severity": 1, + "source": "ReScript" +}, { + "range": {"start": {"line": 1, "character": 9}, "end": {"line": 1, "character": 11}}, + "message": "This let-binding misses an expression", + "severity": 1, + "source": "ReScript" +}, { + "range": {"start": {"line": 0, "character": 4}, "end": {"line": 0, "character": 5}}, + "message": "I was expecting a name for this let-binding. Example: `let message = \"hello\"`", + "severity": 1, + "source": "ReScript" +}] + diff --git a/analysis/tests/test.sh b/analysis/tests/test.sh index 27a98ffb2..c931641e9 100755 --- a/analysis/tests/test.sh +++ b/analysis/tests/test.sh @@ -7,6 +7,15 @@ for file in src/*.{res,resi}; do fi done +for file in not_compiled/*.res; do + output="$(dirname $file)/expected/$(basename $file).txt" + ../rescript-editor-analysis.exe test $file &> $output + # CI. We use LF, and the CI OCaml fork prints CRLF. Convert. + if [ "$RUNNER_OS" == "Windows" ]; then + perl -pi -e 's/\r\n/\n/g' -- $output + fi +done + warningYellow='\033[0;33m' successGreen='\033[0;32m' reset='\033[0m' diff --git a/server/src/server.ts b/server/src/server.ts index c5683387b..f99ac82d3 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -57,7 +57,7 @@ let projectsFiles: Map< let codeActionsFromDiagnostics: codeActions.filesCodeActions = {}; // will be properly defined later depending on the mode (stdio/node-rpc) -let send: (msg: p.Message) => void = (_) => {}; +let send: (msg: p.Message) => void = (_) => { }; interface CreateInterfaceRequestParams { uri: string; @@ -598,6 +598,34 @@ function format(msg: p.RequestMessage): Array { } } +const updateDiagnosticSyntax = (fileUri: string, fileContent: string) => { + let filePath = fileURLToPath(fileUri); + let extension = path.extname(filePath); + let tmpname = utils.createFileInTempDir(extension); + fs.writeFileSync(tmpname, fileContent, { encoding: "utf-8" }); + + const items: p.Diagnostic[] | [] = utils.runAnalysisAfterSanityCheck( + filePath, + [ + "diagnosticSyntax", + tmpname + ], + ); + + const notification: p.NotificationMessage = { + jsonrpc: c.jsonrpcVersion, + method: "textDocument/publishDiagnostics", + params: { + uri: fileUri, + diagnostics: items + } + } + + fs.unlink(tmpname, () => null); + + send(notification) +} + function createInterface(msg: p.RequestMessage): p.Message { let params = msg.params as CreateInterfaceRequestParams; let extension = path.extname(params.uri); @@ -784,6 +812,7 @@ function onMessage(msg: p.Message) { } else if (msg.method === DidOpenTextDocumentNotification.method) { let params = msg.params as p.DidOpenTextDocumentParams; openedFile(params.textDocument.uri, params.textDocument.text); + updateDiagnosticSyntax(params.textDocument.uri, params.textDocument.text); } else if (msg.method === DidChangeTextDocumentNotification.method) { let params = msg.params as p.DidChangeTextDocumentParams; let extName = path.extname(params.textDocument.uri); @@ -797,6 +826,7 @@ function onMessage(msg: p.Message) { params.textDocument.uri, changes[changes.length - 1].text ); + updateDiagnosticSyntax(params.textDocument.uri, changes[changes.length - 1].text); } } } else if (msg.method === DidCloseTextDocumentNotification.method) { diff --git a/server/src/utils.ts b/server/src/utils.ts index c3176697c..c67fb5fd7 100644 --- a/server/src/utils.ts +++ b/server/src/utils.ts @@ -603,9 +603,9 @@ export let parseCompilerLogOutput = ( // 10 ┆ } else if (line.startsWith(" ")) { // part of the actual diagnostics message - parsedDiagnostics[parsedDiagnostics.length - 1].content.push( - line.slice(2) - ); + parsedDiagnostics[parsedDiagnostics.length - 1].content.push( + line.slice(2) + ); } else if (line.trim() != "") { // We'll assume that everything else is also part of the diagnostics too. // Most of these should have been indented 2 spaces; sadly, some of them @@ -635,7 +635,7 @@ export let parseCompilerLogOutput = ( range, source: "ReScript", // remove start and end whitespaces/newlines - message: diagnosticMessage.join("\n").trim() + "\n", + message: diagnosticMessage.join("\n").trim(), }; // Check for potential code actions