From a3e9e53e64a1aa5692bca2b60bcbff6a0fcfdba1 Mon Sep 17 00:00:00 2001 From: zinc Date: Wed, 14 Oct 2026 08:09:40 +0000 Subject: 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. --- tests/drivers/codeactions_client_test.js | 2 +- tests/drivers/hint_fixes_test.js | 66 ++++++++++++++++++++++++++++ tests/drivers/toolchain_lsp_contract_test.js | 4 +- 3 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 tests/drivers/hint_fixes_test.js (limited to 'tests/drivers') diff --git a/tests/drivers/codeactions_client_test.js b/tests/drivers/codeactions_client_test.js index f1936e7..6f956b9 100644 --- a/tests/drivers/codeactions_client_test.js +++ b/tests/drivers/codeactions_client_test.js @@ -9,7 +9,7 @@ class CodeAction { constructor(title, kind) { this.title = title; this.kind = ki const vscode = { Range, WorkspaceEdit, CodeAction, CodeActionKind: { QuickFix: 'quickfix' }, Uri: { parse: s => s } }; const uri = 'file:///test.zc', doc = { uri: { toString: () => uri }, version: 3 }; const edit = { range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }, newText: 'include rt;\n' }; -const source = { title: 'include rt;', kind: 'quickfix', isPreferred: true, edit: { documentChanges: [{ textDocument: { uri, version: 3 }, edits: [edit] }] } }; +const source = { title: 'include rt', kind: 'quickfix', isPreferred: true, edit: { documentChanges: [{ textDocument: { uri, version: 3 }, edits: [edit] }] } }; const actions = convertCodeActions(vscode, [source], doc); assert.strictEqual(actions.length, 1); assert.strictEqual(actions[0].kind, 'quickfix'); 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); diff --git a/tests/drivers/toolchain_lsp_contract_test.js b/tests/drivers/toolchain_lsp_contract_test.js index 2872282..59d5a4c 100644 --- a/tests/drivers/toolchain_lsp_contract_test.js +++ b/tests/drivers/toolchain_lsp_contract_test.js @@ -284,12 +284,12 @@ test('inference', 'typed fields do not constrain the containing parameter to a s test('quickfix', 'an entire-line request finds the compiler error inside the range', () => { const source = 'fn main() { rt.put_n(7); }'; const r = run(source, [{ needle: 'fn', method: 'textDocument/codeAction', params: { range: { start: { line: 0, character: 0 }, end: { line: 0, character: source.length } }, context: { diagnostics: [{ range: { start: { line: 99, character: 99 }, end: { line: 99, character: 99 } }, message: 'stale' }] } } }]); - assert.ok(r.result(2)?.some(a => a.title === 'include rt;'), JSON.stringify(r.result(2))); + assert.ok(r.result(2)?.some(a => a.title === 'include rt'), JSON.stringify(r.result(2))); }); test('quickfix', 'an owner-name cursor can import a qualified module', () => { const source = 'fn main() { rt.put_n(7); }', start = { line: 0, character: source.indexOf('rt.') }; const r = run(source, [{ needle: 'rt.', method: 'textDocument/codeAction', params: { range: { start, end: start }, context: { diagnostics: [] } } }]); - assert.ok(r.result(2)?.some(a => a.title === 'include rt;')); + assert.ok(r.result(2)?.some(a => a.title === 'include rt')); }); test('modulehover', 'source library overview is available without a built-in module entry', () => { const folder = fs.mkdtempSync(path.join(os.tmpdir(), 'zc-module-hover-')); -- cgit v1.2.3