-
Notifications
You must be signed in to change notification settings - Fork 56
/
utils.ts
359 lines (330 loc) · 9.9 KB
/
utils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
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);
}
}
};
// TODO: races here?
// TODO: this doesn't handle file:/// scheme
// We need to recursively search for bs-platform/{platform}/bsc.exe upward from
// the project's root, because in some setups, such as yarn workspace/monorepo,
// the node_modules/bs-platform package might be hoisted up instead of alongside
// the project root.
// Also, if someone's ever formatting a regular project setup's dependency
// (which is weird but whatever), they'll at least find an upward bs-platform
// from the dependent.
export let findBscExeDirOfFile = (
source: p.DocumentUri
): null | p.DocumentUri => {
let dir = path.dirname(source);
let bscPath = path.join(dir, c.bscExePartialPath);
if (fs.existsSync(bscPath)) {
return dir;
} else {
if (dir === source) {
// reached the top
return null;
} else {
return findBscExeDirOfFile(dir);
}
}
};
type execResult =
| {
kind: "success";
result: string;
}
| {
kind: "error";
error: string;
};
export let formatUsingValidBscExePath = (
code: string,
bscExePath: 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(
bscExePath,
["-color", "never", "-format", formatTempFileFullPath],
);
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 runBsbWatcherUsingValidBsbNodePath = (
bsbNodePath: p.DocumentUri,
projectRootPath: p.DocumentUri
) => {
if (process.platform === "win32") {
/*
- a node.js script in node_modules/.bin on windows is wrapped in a
batch script wrapper (there's also a regular binary of the same name on
windows, but that one's a shell script wrapper for cygwin). More info:
https://github.com/npm/cmd-shim/blob/c5118da34126e6639361fe9706a5ff07e726ed45/index.js#L1
- a batch script adds the suffix .cmd to the script
- you can't call batch scripts through the regular `execFile`:
https://nodejs.org/api/child_process.html#child_process_spawning_bat_and_cmd_files_on_windows
- So you have to use `exec` instead, and make sure you quote the path
(since the path might have spaces), which `execFile` would have done
for you under the hood
*/
return childProcess.exec(`"${bsbNodePath}".cmd -w`, {
cwd: projectRootPath,
});
} else {
return childProcess.execFile(bsbNodePath, ["-w"], {
cwd: projectRootPath,
});
}
};
// Logic for parsing .compiler.log
/* example .compiler.log content:
#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)
*/
// parser helpers
let normalizeFileForWindows = (file: string) => {
return process.platform === "win32" ? `file:\\\\\\${file}` : file;
};
let parseFileAndRange = (fileAndRange: string) => {
// https://github.com/rescript-lang/rescript-compiler/blob/0a3f4bb32ca81e89cefd5a912b8795878836f883/jscomp/super_errors/super_location.ml#L15-L25
/* The file + location format can be:
a/b.res <- fallback, no location available (usually due to bad ppx...)
a/b.res:10:20
a/b.res:10:20-21 <- last number here is the end char of line 10
a/b.res:10:20-30:11
*/
let regex = /(.+)\:(\d+)\:(\d+)(-(\d+)(\:(\d+))?)?$/;
/* ^^ file
^^^ start line
^^^ start character
^ optional range
^^^ end line or chararacter
^^^ end character
*/
// for the trimming, see https://github.com/rescript-lang/rescript-vscode/pull/71#issuecomment-769160576
let trimmedFileAndRange = fileAndRange.trim();
let match = trimmedFileAndRange.match(regex);
if (match === null) {
// no location! Though LSP insist that we provide at least a dummy location
return {
file: normalizeFileForWindows(trimmedFileAndRange),
range: {
start: { line: 0, character: 0 },
end: { line: 0, character: 0 },
},
};
}
let [
_source,
file,
startLine,
startChar,
optionalEndGroup,
endLineOrChar,
_colonPlusEndCharOrNothing,
endCharOrNothing,
] = match;
// language-server position is 0-based. Ours is 1-based. Convert
// also, our end character is inclusive. Language-server's is exclusive
let range;
if (optionalEndGroup == null) {
let start = {
line: parseInt(startLine) - 1,
character: parseInt(startChar),
};
range = {
start: start,
end: start,
};
} else {
let isSingleLine = endCharOrNothing == null;
let [endLine, endChar] = isSingleLine
? [startLine, endLineOrChar]
: [endLineOrChar, endCharOrNothing];
range = {
start: {
line: parseInt(startLine) - 1,
character: parseInt(startChar) - 1,
},
end: { line: parseInt(endLine) - 1, character: parseInt(endChar) },
};
}
return {
file: normalizeFileForWindows(file),
range,
};
};
// main parsing logic
type filesDiagnostics = {
[key: string]: p.Diagnostic[];
};
type parsedCompilerLogResult = {
done: boolean;
result: filesDiagnostics;
};
export let parseCompilerLogOutput = (
content: string
): parsedCompilerLogResult => {
type parsedDiagnostic = {
code: number | undefined;
severity: t.DiagnosticSeverity;
tag: t.DiagnosticTag | undefined;
content: string[];
};
let parsedDiagnostics: parsedDiagnostic[] = [];
let lines = content.split(os.EOL);
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)) {
// ^^ indent
// ^^^^^^^^^^^^^^^ gutter
// ^^^^^ separator
// swallow code display. Examples:
// 10 │
// . │
// │
// 10 ┆
} else if (line.startsWith(" ")) {
parsedDiagnostics[parsedDiagnostics.length - 1].content.push(line);
}
}
let result: filesDiagnostics = {};
parsedDiagnostics.forEach((parsedDiagnostic) => {
let [fileAndRangeLine, ...diagnosticMessage] = parsedDiagnostic.content;
let { file, range } = parseFileAndRange(fileAndRangeLine);
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,
source: "ReScript",
message: cleanedUpDiagnostic,
});
});
return { done, result };
};