// 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 an untyped const', head + '\tlet y = 7;\n\trt.put_n(y);\n\treturn 0;\n}\n', 'y = 7', 0, head + '\tconst 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'); fixes('a library function is found in its static group', 'include rt;\nfn main() {\n\trt.put_s(itoa(5));\n\treturn 0;\n}\n', 'itoa', 0, 'include rt;\ninclude std;\nfn main() {\n\trt.put_s(std.Text.itoa(5));\n\treturn 0;\n}\n', 'include std and use std.Text.itoa'); fixes('a group-qualified name gets its module', 'include rt;\nfn main() {\n\trt.put_s(Text.itoa(5));\n\treturn 0;\n}\n', 'Text', 0, 'include rt;\ninclude std;\nfn main() {\n\trt.put_s(std.Text.itoa(5));\n\treturn 0;\n}\n', 'include std and use std.Text'); fixes('a top-level library function is imported', 'include rt;\nfn main() {\n\treturn validate(0);\n}\n', 'validate', 0, 'include rt;\ninclude icon;\nfn main() {\n\treturn icon.validate(0);\n}\n', 'include icon and use icon.validate'); fixes('a library read from src/lib is imported', 'include rt;\nfn main() {\n\treturn vector();\n}\n', 'vector', 0, 'include rt;\ninclude actor;\nfn main() {\n\treturn actor.vector();\n}\n', 'include actor and use actor.vector'); fixes('a misspelled export is corrected and imported', 'fn main() {\n\tputc(65);\n\treturn 0;\n}\n', 'putc', 0, 'include rt;\nfn main() {\n\trt.put_c(65);\n\treturn 0;\n}\n', 'include rt and use rt.put_c'); fixes('a misspelled local function is corrected', 'int count_items() { return 1; }\nfn main() {\n\treturn count_item();\n}\n', 'count_item(', 0, 'int count_items() { return 1; }\nfn main() {\n\treturn count_items();\n}\n', 'change to `count_items`'); fixes('a qualified name from a file library adds its include', 'include rt;\nfn main() {\n\treturn actor.vector();\n}\n', 'actor', 0, 'include rt;\ninclude actor;\nfn main() {\n\treturn actor.vector();\n}\n', 'include actor'); fixes('a misspelled field is corrected', 'include rt;\nstruct point { int length, width; }\nfn main() {\n\tpoint* p = rt.new(point);\n\treturn p.lenght;\n}\n', 'lenght', 0, 'include rt;\nstruct point { int length, width; }\nfn main() {\n\tpoint* p = rt.new(point);\n\treturn p.length;\n}\n', 'change to `length`'); fixes('a misspelled static function of a library group is corrected', 'include rt;\ninclude std;\nfn main() {\n\treturn std.Text.itao(5, 0);\n}\n', 'itao', 0, 'include rt;\ninclude std;\nfn main() {\n\treturn std.Text.itoa(5, 0);\n}\n', 'change to `itoa`'); console.log(ok + ' hint fix checks passed, ' + bad + ' failed'); process.exit(bad ? 1 : 0);