diff options
| author | 2026-10-14 08:09:40 +0000 | |
|---|---|---|
| committer | 2026-10-14 08:09:40 +0000 | |
| commit | a3e9e53e64a1aa5692bca2b60bcbff6a0fcfdba1 (patch) | |
| tree | 2598c50e36e488935211c2e304b720a58315bacb /tests/drivers/hint_fixes_test.js | |
| parent | embed: tests, editor support and docs; js vm file access; test fixes (diff) | |
lsp: quick fixes for hints and bare library names; comment cleanup
an undefined bare name that a module exports now says which one:
"put_c is used but never defined; did you mean to include rt and use
rt.put_c?", and the import quick fix is titled the same way and marked
preferred when it is the only one, so the editor's auto-fix applies it.
hints gain quick fixes that keep the program's meaning: an unused
parameter, or a local with no other use, is renamed to _; a constant
local with one declarator becomes const; a power-of-two multiply becomes
a parenthesized shift (not when a * / or % binds its left operand); and
let p = rt.new(n) with a small constant becomes let p[n]. the hints no
longer fire for _ discards or for locals already declared const.
whole-line /* */ comments in zc sources are // comments now, and //: is
gone from the maintained sources: the notes it marked were mostly the
documentation of the declaration below, which hovers now show. section
headers are separated from the declarations after them.
Diffstat (limited to 'tests/drivers/hint_fixes_test.js')
| -rw-r--r-- | tests/drivers/hint_fixes_test.js | 66 |
1 files changed, 66 insertions, 0 deletions
diff --git a/tests/drivers/hint_fixes_test.js b/tests/drivers/hint_fixes_test.js new file mode 100644 index 0000000..9513a85 --- /dev/null +++ b/tests/drivers/hint_fixes_test.js @@ -0,0 +1,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); |
