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
|
// quick fixes for hints and for library names used without their module: each fix is the
// edit the hint describes, offered only where the change keeps the program's meaning.
// usage: node tests/drivers/hint_fixes_test.js path/to/zclsp
// interpreted hosts (no jit) are far slower; ZC_TEST_TIMEOUT_SCALE stretches every deadline
const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1;
const cp = require('child_process');
const frame = m => { const b = Buffer.from(JSON.stringify(m)); return Buffer.concat([Buffer.from(`Content-Length: ${b.length}\r\n\r\n`), b]); };
let ok = 0, bad = 0;
function check(cond, label, extra) {
if (cond) { ok++; console.log('ok ' + label); }
else { bad++; console.log('FAIL ' + label + (extra === undefined ? '' : ': ' + JSON.stringify(extra).slice(0, 400))); }
}
// the actions offered at the first occurrence of needle (plus offset) in text
function actions(text, needle, offset = 0) {
const at = text.indexOf(needle) + offset, before = text.slice(0, at);
const position = { line: before.split('\n').length - 1, character: before.length - before.lastIndexOf('\n') - 1 };
const uri = 'file:///hint-fixes.zc';
const messages = [{ id: 1, method: 'initialize', params: {} },
{ method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, text } } },
{ id: 2, method: 'textDocument/codeAction', params: { textDocument: { uri }, range: { start: position, end: position }, context: { diagnostics: [] } } },
{ method: 'exit' }];
const r = cp.spawnSync(process.argv[2], [], { input: Buffer.concat(messages.map(m => frame({ jsonrpc: '2.0', ...m }))), timeout: 30000 * TIMEOUT_SCALE });
const out = r.stdout.toString(), i = out.indexOf('{"jsonrpc":"2.0","id":2');
return i < 0 ? null : JSON.parse(out.slice(i, out.indexOf('Content-Length', i) < 0 ? undefined : out.indexOf('Content-Length', i))).result;
}
// apply the first action's edits to text
function apply(text, action) {
const lines = text.split('\n');
const offset = p => lines.slice(0, p.line).reduce((n, l) => n + l.length + 1, 0) + p.character;
const edits = action.edit.documentChanges[0].edits.map(e => [offset(e.range.start), offset(e.range.end), e.newText]).sort((a, b) => b[0] - a[0]);
for (const [s, e, t] of edits) text = text.slice(0, s) + t + text.slice(e);
return text;
}
function fixes(label, text, needle, offset, expected, title) {
const result = actions(text, needle, offset);
const first = result && result[0];
check(first && first.title === title && first.isPreferred === true && apply(text, first) === expected, label, result);
}
function none(label, text, needle, offset) {
const result = actions(text, needle, offset);
check(Array.isArray(result) && result.length === 0, label, result);
}
const head = 'include rt;\nfn main() {\n';
fixes('an unused parameter is renamed to _', 'include rt;\nint g(int a, int b) { return a; }\nfn main() { return g(1, 2); }\n', 'int b', 4,
'include rt;\nint g(int a, int _) { return a; }\nfn main() { return g(1, 2); }\n', 'rename `b` to _');
fixes('a local that is never read is renamed to _', head + '\tlet unused = rt.get_c();\n\treturn 0;\n}\n', 'unused', 0,
head + '\tlet _ = rt.get_c();\n\treturn 0;\n}\n', 'rename `unused` to _');
none('a local written later is not renamed', head + '\tlet later = 1;\n\tlater = 2;\n\treturn 0;\n}\n', 'later', 0);
fixes('a typed constant local becomes const', head + '\tint x = 5;\n\trt.put_n(x);\n\treturn 0;\n}\n', 'x = 5', 0,
head + '\tconst int x = 5;\n\trt.put_n(x);\n\treturn 0;\n}\n', 'make `x` a const');
fixes('an untyped constant local becomes const int', head + '\tlet y = 7;\n\trt.put_n(y);\n\treturn 0;\n}\n', 'y = 7', 0,
head + '\tconst int y = 7;\n\trt.put_n(y);\n\treturn 0;\n}\n', 'make `y` a const');
none('one of several declarators is left alone', head + '\tint x = 5, z = rt.get_c();\n\trt.put_n(x + z);\n\treturn 0;\n}\n', 'x = 5', 0);
fixes('a power-of-two multiply becomes a parenthesized shift', head + '\tlet y = rt.get_c();\n\tlet q[4];\n\tq[0] = y;\n\trt.put_n(q[0] * 16 + y);\n\treturn 0;\n}\n', '* 16', 0,
head + '\tlet y = rt.get_c();\n\tlet q[4];\n\tq[0] = y;\n\trt.put_n((q[0] << 4) + y);\n\treturn 0;\n}\n', 'write << 4');
none('a multiply bound to a division on its left is left alone', head + '\tlet y = rt.get_c();\n\trt.put_n(y / 2 * 16);\n\treturn 0;\n}\n', '* 16', 0);
fixes('a small heap buffer becomes a stack buffer', head + '\tlet p = rt.new(8);\n\tp[0] = rt.get_c();\n\trt.put_n(p[0]);\n\treturn 0;\n}\n', 'p = rt', 0,
head + '\tlet p[8];\n\tp[0] = rt.get_c();\n\trt.put_n(p[0]);\n\treturn 0;\n}\n', 'use a stack buffer');
fixes('a runtime name without its module is imported and qualified', 'fn main() {\n\tput_c(65);\n\treturn 0;\n}\n', 'put_c', 0,
'include rt;\nfn main() {\n\trt.put_c(65);\n\treturn 0;\n}\n', 'include rt and use rt.put_c');
fixes('a compiler name is imported from run', 'include rt;\nfn main() {\n\trt.put_n(evals("return 1;"));\n\treturn 0;\n}\n', 'evals', 0,
'include rt;\ninclude run;\nfn main() {\n\trt.put_n(run.evals("return 1;"));\n\treturn 0;\n}\n', 'include run and use run.evals');
fixes('an imported module only needs the qualifier', 'include rt;\nfn main() {\n\tput_c(65);\n\treturn 0;\n}\n', 'put_c', 0,
'include rt;\nfn main() {\n\trt.put_c(65);\n\treturn 0;\n}\n', 'use rt.put_c');
console.log(ok + ' hint fix checks passed, ' + bad + ' failed');
process.exit(bad ? 1 : 0);
|