import { Range } from "vscode-languageserver-textdocument"; import * as c from "./constants"; import * as childProcess from "child_process"; import * as p from "vscode-languageserver-protocol"; import * as path from "path"; import * as t from "vscode-languageserver-types"; import fs from "fs"; import * as os from "os"; let tempFilePrefix = "rescript_format_file_" + process.pid + "_"; let tempFileId = 0; export let createFileInTempDir = (extension = "") => { let tempFileName = tempFilePrefix + tempFileId + extension; tempFileId = tempFileId + 1; return path.join(os.tmpdir(), tempFileName); }; // TODO: races here? // TODO: this doesn't handle file:/// scheme export let findProjectRootOfFile = ( source: p.DocumentUri ): null | p.DocumentUri => { let dir = path.dirname(source); if (fs.existsSync(path.join(dir, c.bsconfigPartialPath))) { return dir; } else { if (dir === source) { // reached top return null; } else { return findProjectRootOfFile(dir); } } }; type execResult = | { kind: "success"; result: string; } | { kind: "error"; error: string; }; export let formatUsingValidBscPath = ( code: string, bscPath: p.DocumentUri, isInterface: boolean ): execResult => { let extension = isInterface ? c.resiExt : c.resExt; let formatTempFileFullPath = createFileInTempDir(extension); fs.writeFileSync(formatTempFileFullPath, code, { encoding: "utf-8", }); try { let result = childProcess.execFileSync( bscPath, ["-color", "never", "-format", formatTempFileFullPath], { stdio: "pipe" } ); return { kind: "success", result: result.toString(), }; } catch (e) { return { kind: "error", error: e.message, }; } finally { // async close is fine. We don't use this file name again fs.unlink(formatTempFileFullPath, () => null); } }; export let runBsbWatcherUsingValidBsbPath = ( bsbPath: p.DocumentUri, projectRootPath: p.DocumentUri ) => { let process = childProcess.execFile(bsbPath, ["-w"], { cwd: projectRootPath, }); return process; // try { // let result = childProcess.execFileSync(bsbPath, [], { stdio: 'pipe', cwd: projectRootPath }) // return { // kind: 'success', // result: result.toString(), // } // } catch (e) { // return { // kind: 'error', // error: e.message, // } // } }; export let parseDiagnosticLocation = (location: string): Range => { // example output location: // 3:9 // 3:5-8 // 3:9-6:1 // language-server position is 0-based. Ours is 1-based. Don't forget to convert // also, our end character is inclusive. Language-server's is exclusive let isRange = location.indexOf("-") >= 0; if (isRange) { let [from, to] = location.split("-"); let [fromLine, fromChar] = from.split(":"); let isSingleLine = to.indexOf(":") >= 0; let [toLine, toChar] = isSingleLine ? to.split(":") : [fromLine, to]; return { start: { line: parseInt(fromLine) - 1, character: parseInt(fromChar) - 1, }, end: { line: parseInt(toLine) - 1, character: parseInt(toChar) }, }; } else { let [line, char] = location.split(":"); let start = { line: parseInt(line) - 1, character: parseInt(char) }; return { start: start, end: start, }; } }; type filesDiagnostics = { [key: string]: p.Diagnostic[]; }; type parsedCompilerLogResult = { done: boolean; result: filesDiagnostics; }; export let parseCompilerLogOutput = ( content: string ): parsedCompilerLogResult => { /* example .compiler.log file content that we're gonna parse: #Start(1600519680823) Syntax error! /Users/chenglou/github/reason-react/src/test.res:1:8-2:3 1 │ let a = 2 │ let b = 3 │ This let-binding misses an expression Warning number 8 /Users/chenglou/github/reason-react/src/test.res:3:5-8 1 │ let a = j`😀` 2 │ let b = `😀` 3 │ let None = None 4 │ let bla: int = " 5 │ hi You forgot to handle a possible case here, for example: Some _ We've found a bug for you! /Users/chenglou/github/reason-react/src/test.res:3:9 1 │ let a = 1 2 │ let b = "hi" 3 │ let a = b + 1 This has type: string Somewhere wanted: int #Done(1600519680836) */ type parsedDiagnostic = { code: number | undefined; severity: t.DiagnosticSeverity; tag: t.DiagnosticTag | undefined; content: string[]; }; let parsedDiagnostics: parsedDiagnostic[] = []; let lines = content.split("\n"); let done = false; for (let i = 0; i < lines.length; i++) { let line = lines[i]; if (line.startsWith(" We've found a bug for you!")) { parsedDiagnostics.push({ code: undefined, severity: t.DiagnosticSeverity.Error, tag: undefined, content: [], }); } else if (line.startsWith(" Warning number ")) { let warningNumber = parseInt(line.slice(" Warning number ".length)); let tag: t.DiagnosticTag | undefined = undefined; switch (warningNumber) { case 11: case 20: case 26: case 27: case 32: case 33: case 34: case 35: case 36: case 37: case 38: case 39: case 60: case 66: case 67: case 101: tag = t.DiagnosticTag.Unnecessary; break; case 3: tag = t.DiagnosticTag.Deprecated; break; } parsedDiagnostics.push({ code: Number.isNaN(warningNumber) ? undefined : warningNumber, severity: t.DiagnosticSeverity.Warning, tag: tag, content: [], }); } else if (line.startsWith(" Syntax error!")) { parsedDiagnostics.push({ code: undefined, severity: t.DiagnosticSeverity.Error, tag: undefined, content: [], }); } else if (line.startsWith("#Done(")) { done = true; } else if (/^ +[0-9]+ /.test(line)) { // code display. Swallow } else if (line.startsWith(" ")) { parsedDiagnostics[parsedDiagnostics.length - 1].content.push(line); } } let result: filesDiagnostics = {}; parsedDiagnostics.forEach((parsedDiagnostic) => { let [fileAndLocation, ...diagnosticMessage] = parsedDiagnostic.content; let locationSeparator = fileAndLocation.indexOf(":"); let file = fileAndLocation.substring(2, locationSeparator); let location = fileAndLocation.substring(locationSeparator + 1); if (result[file] == null) { result[file] = []; } let cleanedUpDiagnostic = diagnosticMessage .map((line) => { // remove the spaces in front return line.slice(2); }) .join("\n") // remove start and end whitespaces/newlines .trim() + "\n"; result[file].push({ severity: parsedDiagnostic.severity, tags: parsedDiagnostic.tag === undefined ? [] : [parsedDiagnostic.tag], code: parsedDiagnostic.code, range: parseDiagnosticLocation(location), source: "ReScript", message: cleanedUpDiagnostic, }); }); return { done, result }; };