aboutsummaryrefslogtreecommitdiff
path: root/tests/drivers/toolchain_lsp_contract_test.js
blob: 0d1b5155989040a9deb813662da2bf4e0a0458d0 (plain) (blame)
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
// pending requirements have explicit protocol oracles, selectable in small groups.
const assert = require('assert');
const cp = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');
const host = process.argv[2], group = process.env.CONTRACT_GROUP;
const frame = m => { const b = Buffer.from(JSON.stringify({ jsonrpc: '2.0', ...m })); return Buffer.concat([Buffer.from(`Content-Length: ${b.length}\r\n\r\n`), b]); };
function run(text, requests = [], uri = 'file:///contracts.zc', files = []) {
    const messages = [{ id: 1, method: 'initialize', params: {} }, ...files.map(([file, content]) => ({ method: 'zc/file', params: { path: file, text: content } })), { method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, text } } }, ...requests.map((r, i) => {
        const at = text.indexOf(r.needle) + (r.offset || 0); assert.ok(at >= 0, r.needle);
        const before = text.slice(0, at);
        return { id: i + 2, method: r.method || 'textDocument/hover', params: { textDocument: { uri }, ...(r.method === 'textDocument/codeAction' ? {} : { position: { line: before.split('\n').length - 1, character: before.length - before.lastIndexOf('\n') - 1 } }), ...(r.params || {}) } };
    }), { method: 'exit' }];
    let output;
    if (host === '--wasm' || host === '--vm') {
        const wasm = host === '--wasm';
        const bridge = require(path.resolve('editors/vscode/client', wasm ? 'wasmproc.js' : 'vmproc.js'));
        const proc = (wasm ? bridge.wasmProcess : bridge.vmProcess)(path.resolve(process.argv[3]), { log: () => {} });
        const chunks = []; proc.stdout.on('data', b => chunks.push(b));
        for (const message of messages) proc.stdin.write(frame(message));
        output = Buffer.concat(chunks);
    } else {
        const r = cp.spawnSync(host, process.argv.slice(3), { input: Buffer.concat(messages.map(frame)), timeout: 60000, maxBuffer: 32 << 20 });
        assert.strictEqual(r.status, 0, String(r.stderr)); output = r.stdout;
    }
    const replies = []; let data = output;
    while (data.length) {
        const end = data.indexOf('\r\n\r\n'); assert.ok(end >= 0);
        const n = Number(/Content-Length:\s*(\d+)/i.exec(data.subarray(0, end).toString())[1]);
        assert.ok(data.length >= end + 4 + n);
        replies.push(JSON.parse(data.subarray(end + 4, end + 4 + n))); data = data.subarray(end + 4 + n);
    }
    const ds = replies.filter(r => r.method === 'textDocument/publishDiagnostics').at(-1)?.params.diagnostics || [];
    return { ds, result: id => replies.find(r => r.id === id)?.result };
}
let failures = 0, checks = 0;
function test(g, name, f) {
    if (group && group !== g) return;
    checks++;
    try { f(); console.log('ok   ' + g + ': ' + name); }
    catch (e) { failures++; console.error('FAIL ' + g + ': ' + name + '\n' + e.message); }
}
for (const [uri, prefix] of [['file:///contracts.zc', ''], ['tinyvm:/fixture.zc', '']]) {
    test('hovers', 'word-string global at declaration and use ' + uri, () => {
        const text = prefix + 'let hello = "hi"; fn main() { return hello[0]; }';
        const r = run(text, [{ needle: 'hello =' }, { needle: 'hello[0]' }], uri);
        for (const id of [2, 3]) { const md = r.result(id)?.contents.value || ''; assert.match(md, /\(char\*\) let hello \/\/ global;/); assert.ok(!md.includes('; type ')); }
    });
    test('hovers', 'fixed global array at declaration and use ' + uri, () => {
        const r = run('let prog[20000]; fn main() { return prog[0]; }', [{ needle: 'prog[20000]' }, { needle: 'prog[0]' }], uri);
        for (const id of [2, 3]) assert.match(r.result(id)?.contents.value || '', /\(word\[20000\]\) let prog \/\/ global;/);
    });
}
test('strings', 'packed declaration, parameter, return and uses keep bstr', () => {
    const r = run('fn echo(s) { return s; } fn main() { let hello = b"hi"; let other = echo(hello); return other[0]; }', [{ needle: 'hello =' }, { needle: 'hello);' }, { needle: 'other[0]' }, { needle: 'echo(hello)' }, { needle: 'return s;', offset: 7 }]);
    assert.ok(!r.ds.some(d => d.severity === 1), JSON.stringify(r.ds));
    for (const id of [2, 3, 4, 6]) { const md = r.result(id)?.contents.value || ''; assert.match(md, /\(bstr\) let/); assert.ok(!/\(word\)/.test(md)); }
    assert.match(r.result(5)?.contents.value || '', /bstr echo\(bstr s\)/);
});
test('strings', 'grouped word-string local stays char*', () => {
    const r = run('fn main() { let n = 1, text = "hi"; return text[0] + n; }', [{ needle: 'text[0]' }]);
    assert.match(r.result(2)?.contents.value || '', /\(char\*\) let text/);
});
test('strings', 'inferred packed return is shown in hover', () => {
    const text = 'fn text() { return b"hi"; } fn main() { let result = text(); return result[0]; }';
    const r = run(text, [{ needle: 'text()' }, { needle: 'result = ' }, { needle: 'result[0]' }]);
    assert.match(r.result(2)?.contents.value || '', /bstr text\(\)/);
    for (const id of [3, 4]) assert.match(r.result(id)?.contents.value || '', /\(bstr\) let result/);
});
test('strings', 'inferred word-string return is shown in hover', () => {
    const text = 'fn text() { return "hi"; } fn main() { let result = text(); return result[0]; }';
    const r = run(text, [{ needle: 'text()' }, { needle: 'result = ' }, { needle: 'result[0]' }]);
    assert.match(r.result(2)?.contents.value || '', /char\* text\(\)/);
    for (const id of [3, 4]) assert.match(r.result(id)?.contents.value || '', /\(char\*\) let result/);
});
for (const [name, src, expected, severity = 3] of [
    ['unused parameter', 'fn ignore(int unused) { return 0; }', true],
    ['used parameter', 'fn use(int used) { return used; }', false],
    ['explicit discard parameter', 'fn ignore(int _) { return 0; }', false],
    ['duplicate discard parameters', 'fn ignore(int _, int _) { return 0; }', false],
    ['write-only parameter', 'fn write(int unused) { unused = 2; return 0; }', true],
    ['shadowed parameter', 'fn shadow(int unused) { { int unused = 2; return unused; } }', true],
    ['unused method argument', 'struct point { fn f(int unused) { return 0; } }', true],
    ['unused closure argument', 'fn main() { let f = fn:int(int unused) { return 0; }; return f(2); }', true],
    ['unused generic argument', 'T first<T>(T used, T unused) { return used; } fn main() { return first<int>(1, 2); }', true],
    ['on directive', 'hints on; fn ignore(int unused) { return 0; }', true, 3],
    ['small directive', 'hints small; fn ignore(int unused) { return 0; }', true, 4],
    ['off directive', 'hints off; fn ignore(int unused) { return 0; }', false]
]) test('arguments', name, () => {
    const r = run(src + '\nfn marker() { return 0; }');
    const hints = r.ds.filter(d => /unused.*(argument|parameter)|(?:argument|parameter).*unused/.test(d.message));
    assert.strictEqual(hints.length > 0, expected, JSON.stringify(r.ds));
    if (expected) { assert.strictEqual(hints[0].severity, severity); assert.strictEqual(src.split('\n')[hints[0].range.start.line].slice(hints[0].range.start.character, hints[0].range.end.character), 'unused'); }
});
for (const [name, src, warning] of [
    ['discard read warns', 'fn f(int _) { return _; }', true],
    ['discard assignment is not a read', 'fn f(int _) { _ = 2; return 0; }', false],
    ['duplicate payload discards', 'include rt; enum pair { item { int x; int y; }, other }; fn main() { let p = pair.item(1, 2); switch(p) { case item(_, _): return 0; case _: return 1; } }', false],
    ['bound payload discard read', 'include rt; enum pair { item { int x; int y; }, other }; fn main() { let p = pair.item(1, 2); switch(p) { case item(_, _): return _; case _: return 1; } }', true],
    ['numeric catch-all', 'fn f(int n) { switch(n) { case 1: return 1; case _: return 2; } }', false]
]) test('discards', name, () => {
    const r = run(src);
    assert.ok(!r.ds.some(d => d.severity === 1), JSON.stringify(r.ds));
    const ds = r.ds.filter(d => /discard.*read|read.*discard/.test(d.message));
    assert.strictEqual(ds.length > 0, warning, JSON.stringify(r.ds));
    for (const d of ds) { assert.strictEqual(d.severity, 2); assert.strictEqual(src.slice(d.range.start.character, d.range.end.character), '_'); }
});
for (const [name, src, count, severity] of [
    ['uncommented literal', 'fn f(int n) { return n + 0x40ff; }', 1, 3],
    ['commented literal', 'fn f(int n) { return n + /* offset in memory */ 0x40ff; }', 0, 3],
    ['empty comment escape', 'fn f(int n) { return n + /**/ 0x40ff; }', 0, 3],
    ['nearby comment does not hide it', 'fn f(int n) { return /* explanation of n */ n + 0x40ff; }', 1, 3],
    ['compact dots', 'hints small; fn f(int n) { return n + 0x40ff; }', 1, 4],
    ['off directive', 'hints off; fn f(int n) { return n + 0x40ff; }', 0, 3]
]) test('magic', name, () => { const ds = run(src).ds.filter(d => /magic|numeric literal|number .*has no name/i.test(d.message)); assert.strictEqual(ds.length, count, JSON.stringify(ds)); if (count) { assert.strictEqual(ds[0].severity, severity); assert.match(ds[0].message, /inline block comment/); assert.ok(!ds[0].message.includes('a const says what it means')); } });
test('navigation', 's_to_n points to its runtime declaration', () => {
    const r = run('include rt; fn main() { return rt.s_to_n(b"42"); }', [{ method: 'textDocument/definition', needle: 's_to_n' }]);
    const loc = r.result(2)?.[0]; assert.ok(loc, 'missing definition'); assert.strictEqual(loc.uri, 'tinyvm:/rt.zc');
    const line = fs.readFileSync('src/runtime/rt.zc', 'utf8').split('\n')[loc.range.start.line];
    assert.strictEqual(line.slice(loc.range.start.character, loc.range.end.character), 's_to_n');
});
for (const [module, file] of [['core', 'core.zc'], ['rt', 'rt.zc']]) test('navigation', 'qualified duplicate names open the correct module: ' + module, () => {
    const source = 'include rt, core; fn main() { return ' + module + '.syscall(9,0,0,0); }';
    const r = run(source, [{ needle: 'syscall(', method: 'textDocument/definition' }, { needle: 'syscall(' }]);
    const location = r.result(2)?.[0];
    assert.strictEqual(location?.uri, 'tinyvm:/' + file);
    const sourcePath = module === 'core' ? 'src/core/core.zc' : 'src/runtime/rt.zc';
    const line = fs.readFileSync(sourcePath, 'utf8').split('\n')[location.range.start.line];
    assert.strictEqual(line.slice(location.range.start.character, location.range.end.character), 'syscall');
    if (module === 'core') assert.match(r.result(3)?.contents.value || '', /int syscall\(int operation/);
});
test('navigation', 'qualified references retain their resolved module identity', () => {
    const source = 'include rt, core; fn main() { core.syscall(9,0,0,0); rt.syscall(9,0,0,0); return core.syscall(9,0,0,0); }';
    const r = run(source, [{ needle: 'core.syscall', offset: 5, method: 'textDocument/references', params: { context: { includeDeclaration: false } } }]);
    const references = r.result(2) || [];
    assert.strictEqual(references.length, 2);
    for (const location of references) assert.ok(source.slice(location.range.start.character, location.range.end.character).includes('syscall'));
});
test('modules', 'qualified completion exports only imported module', () => {
    const r = run('include rt; fn main() { rt.put_', [{ method: 'textDocument/completion', needle: 'rt.put_', offset: 7 }]);
    const items = r.result(2)?.items || r.result(2) || [];
    assert.ok(items.some(i => i.label === 'put_s')); assert.ok(!items.some(i => i.label === 'evals'));
});
test('modules', 'lsp exports require an explicit include and qualified name', () => {
    const accepted = run('include lsp; fn main() { return lsp.hid(97); }');
    assert.ok(!accepted.ds.some(d => d.severity === 1), JSON.stringify(accepted.ds));
    const missing = run('fn main() { return lsp.hid(97); }');
    assert.ok(missing.ds.some(d => d.severity === 1 && /include lsp/.test(d.message)), JSON.stringify(missing.ds));
    const bare = run('include lsp; fn main() { return hid(97); }');
    assert.ok(bare.ds.some(d => d.severity === 1), JSON.stringify(bare.ds));
});
test('modules', 'missing include reports error at qualified member', () => {
    const r = run('fn main() { rt.put_s("hi"); return 0; }');
    assert.ok(r.ds.some(d => d.severity === 1 && /include rt/.test(d.message)), JSON.stringify(r.ds));
});
for (const [name, description] of [['rt', /runtime/], ['run', /resident compiler/], ['core', /vm host/], ['lsp', /language server/], ['std', /standard library/], ['icon', /syntax transformations/]]) {
    test('modulehover', 'module documentation in comma-separated include: ' + name, () => {
        const text = 'include rt, run, core, lsp, std, icon; fn main() { return 0; }';
        const r = run(text, [{ needle: name + (name === 'icon' ? ';' : ',') }]);
        const hover = r.result(2)?.contents;
        assert.strictEqual(hover?.kind, 'markdown');
        assert.match(hover.value, new RegExp('```zc\\ninclude ' + name + ';\\n```'));
        assert.match(hover.value, description);
        assert.match(hover.value, new RegExp('`' + name + '\\.[^`]+`'));
    });
}
test('modulehover', 'a local with the same name retains its variable hover', () => {
    const r = run('fn main() { int rt = 3; return rt; }', [{ needle: 'rt;' }]);
    assert.match(r.result(2)?.contents.value || '', /\(int\) let rt/);
    assert.ok(!r.result(2).contents.value.includes('include rt;'));
});
test('modulehover', 'comments and newlines preserve the include list', () => {
    const r = run('include /* runtime */ rt,\n /* compiler */ run; fn main() { return 0; }', [{ needle: 'run;' }]);
    assert.match(r.result(2)?.contents.value || '', /include run;[\s\S]*resident compiler/);
});
for (const [name, member] of [['rt', 'put_s'], ['run', 'evals'], ['core', 'syscall'], ['lsp', 'hid'], ['std', 'array_map'], ['icon', 'find_replace']]) {
    test('namespacecompletion', 'qualified members, signatures and documentation: ' + name, () => {
        const r = run('include rt, run, core, lsp, std, icon; fn main() { ' + name + '.', [{ needle: name + '.', offset: name.length + 1, method: 'textDocument/completion' }]);
        const items = r.result(2)?.items || [];
        const item = items.find(i => i.label === member);
        assert.ok(item, JSON.stringify(items.map(i => i.label)));
        assert.ok(item.detail && item.documentation?.value, JSON.stringify(item));
        assert.ok(!items.some(i => i.label === 'main'));
        assert.strictEqual(new Set(items.map(i => i.label)).size, items.length);
    });
}
function applyWorkspaceEdit(text, edit, uri) {
    const edits = edit.documentChanges?.flatMap(c => c.textDocument.uri === uri ? c.edits : []) || edit.changes?.[uri] || [];
    const offset = p => text.split('\n').slice(0, p.line).reduce((n, l) => n + l.length + 1, 0) + p.character;
    for (const e of edits.sort((a, b) => offset(b.range.start) - offset(a.range.start))) text = text.slice(0, offset(e.range.start)) + e.newText + text.slice(offset(e.range.end));
    return text;
}
for (const [name, source, needle] of [
    ['qualified runtime import', 'fn main() { rt.put_n(7); }', 'put_n'],
    ['qualified compiler import', 'fn main() { run.evals("return 0;"); }', 'evals'],
    ['qualified host import', 'fn main() { return core.syscall(9,0,0,0); }', 'syscall'],
    ['bare runtime call needs qualification too', 'fn main() { put_n(7); }', 'put_n'],
    ['imported module still needs a qualified name', 'include rt; fn main() { put_n(7); }', 'put_n'],
    ['namespace and header are preserved', '// file overview\nnamespace demo;\n\n// prints the result\nfn main() { rt.put_n(7); }', 'put_n'],
    ['crlf stays crlf', 'namespace demo;\r\nfn main() { rt.put_n(7); }\r\n', 'put_n'],
]) test('quickfix', name, () => {
    const uri = 'file:///contracts.zc', before = source.slice(0, source.indexOf(needle));
    const position = { line: before.split('\n').length - 1, character: before.length - before.lastIndexOf('\n') - 1 };
    const r = run(source, [{ needle, method: 'textDocument/codeAction', params: { range: { start: position, end: position }, context: { diagnostics: [] } } }], uri);
    assert.ok(r.result(1)?.capabilities.codeActionProvider);
    const action = r.result(2)?.find(a => a.kind === 'quickfix' && a.edit);
    assert.ok(action, JSON.stringify(r.result(2)));
    const changed = applyWorkspaceEdit(source, action.edit, uri);
    assert.notStrictEqual(changed, source);
    if (source.includes('\r\n')) assert.ok(!/(?<!\r)\n/.test(changed));
    if (source.includes('// prints the result')) assert.ok(changed.includes('// prints the result\nfn main()'));
    if (source.startsWith('include rt;')) assert.strictEqual((changed.match(/include rt;/g) || []).length, 1);
    const checked = run(changed);
    assert.ok(!checked.ds.some(d => d.severity === 1), JSON.stringify({ changed, ds: checked.ds }));
});
for (const source of ['include rt; fn main() { rt.put_n(7); }', 'fn main() { int put_n = 7; return put_n; }', 'fn main() { return unknown_name; }']) test('quickfix', 'no unrelated import edits: ' + source, () => {
    const needle = source.includes('put_n') ? 'put_n' : 'unknown_name';
    const r = run(source, [{ needle, method: 'textDocument/codeAction' }]);
    assert.deepStrictEqual(r.result(2), []);
});
test('qualifiers', 'fixed const array hover preserves both qualifiers and shape', () => {
    const r = run('fn main() { fixed const int[2] data = {1,2}; return data[0]; }', [{ needle: 'data[0]' }]);
    assert.ok(!r.ds.some(d => d.severity === 1), JSON.stringify(r.ds));
    assert.match(r.result(2)?.contents.value || '', /\(const int\[2\]\) let data/);
});
test('qualifiers', 'const fixed global hover preserves its type', () => {
    const r = run('const fixed int[2] data = {1,2}; fn main() { return data[0]; }', [{ needle: 'data[0]' }]);
    assert.match(r.result(2)?.contents.value || '', /\(const int\[2\]\) let data \/\/ global;/);
});
test('qualifiers', 'readonly qualifiers reject aliasing without optional type checking', () => {
    const r = run('typecheck off; fn mutate(int* p) { p[0] = 9; } fn main() { fixed const int[2] data = {1,2}; mutate(data); }');
    assert.ok(r.ds.some(d => d.severity === 1 && /const storage cannot be converted/.test(d.message)), JSON.stringify(r.ds));
});
test('inference', 'typed callee constrains caller arguments and returned values', () => {
    const source = 'int seed(int cap) { return cap + 1; } fn squares(n) { let l = seed(n); return l; } fn main() { return squares(3); }';
    const r = run(source, [{ needle: 'squares(n)' }, { needle: 'seed(n)', offset: 5 }, { needle: 'return l;', offset: 7 }]);
    assert.match(r.result(2)?.contents.value || '', /int squares\(int n\)/);
    assert.match(r.result(3)?.contents.value || '', /\(int\) let n/);
    assert.match(r.result(4)?.contents.value || '', /\(int\) let l/);
});
test('inference', 'twelve caller layers converge in reverse declaration order', () => {
    let source = '';
    for (let i = 12; i > 0; i--) source += `fn layer${i}(n) { return layer${i - 1}(n); }\n`;
    source += 'int layer0(int n) { return n + 1; } fn main() { return layer12(3); }';
    const r = run(source, [{ needle: 'layer12(n)' }, { needle: 'layer7(n)' }, { needle: 'layer1(n)' }]);
    for (const [id, name] of [[2, 'layer12'], [3, 'layer7'], [4, 'layer1']]) assert.match(r.result(id)?.contents.value || '', new RegExp('int ' + name + '\\(int n\\)'));
});
test('inference', 'recursive calls retain an anchored integer return and argument type', () => {
    const source = 'int seed(int n) { return n; } fn recursive(n) { if (n <= 0) return seed(n); return recursive(n - 1); } fn main() { return recursive(3); }';
    const r = run(source, [{ needle: 'recursive(n)' }]);
    assert.match(r.result(2)?.contents.value || '', /int recursive\(int n\)/);
});
test('inference', 'uncalled caller parameters inherit typed callee requirements', () => {
    const r = run('int seed(int n) { return n; } fn wrapper(n) { return seed(n); }', [{ needle: 'wrapper(n)' }]);
    assert.match(r.result(2)?.contents.value || '', /int wrapper\(int n\)/);
});
test('inference', 'conflicting caller types do not become a single inferred signature', () => {
    const r = run('fn identity(value) { return value; } fn main() { identity(3); return identity("text")[0]; }', [{ needle: 'identity(value)' }]);
    assert.match(r.result(2)?.contents.value || '', /fn identity\(value\)/);
});
test('inference', 'unknown reassignment invalidates the previous initializer flow', () => {
    const r = run('int seed(int n) { return n; } fn external(); fn changed(n) { let result = seed(n); result = external(); return result; }', [{ needle: 'changed(n)' }]);
    assert.ok(!/int changed/.test(r.result(2)?.contents.value || ''), JSON.stringify(r.result(2)));
});
test('inference', 'owned list allocation constrains capacity and caller types', () => {
    const text = 'include rt; .u mklist(cap) { let l = rt.new(cap + 1); l[0] = 0; return l; } .u squares(n) { let l = mklist(n); return l; }';
    const r = run(text, [{ needle: 'mklist(cap)' }, { needle: 'squares(n)' }, { needle: 'mklist(n)', offset: 7 }]);
    assert.ok(!r.ds.some(d => d.severity === 1), JSON.stringify(r.ds));
    assert.match(r.result(2)?.contents.value || '', /word\.u mklist\(int cap\)/);
    assert.match(r.result(3)?.contents.value || '', /word\.u squares\(int n\)/);
    assert.match(r.result(4)?.contents.value || '', /\(int\) let n/);
});
test('inference', 'array indices do not constrain the type of an element result', () => {
    const r = run('include rt; fn lookup(index) { let data = rt.new(4); return data[index]; } fn main() { return lookup(1); }', [{ needle: 'lookup(index)' }]);
    assert.ok(!/int lookup/.test(r.result(2)?.contents.value || ''), JSON.stringify(r.result(2)));
});
test('inference', 'typed fields do not constrain the containing parameter to a scalar', () => {
    const r = run('struct point { int x; } fn coordinate(p) { return p.x; }', [{ needle: 'coordinate(p)' }]);
    assert.ok(!/coordinate\(int p\)/.test(r.result(2)?.contents.value || ''), JSON.stringify(r.result(2)));
});
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)));
});
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;'));
});
test('modulehover', 'source library overview is available without a built-in module entry', () => {
    const folder = fs.mkdtempSync(path.join(os.tmpdir(), 'zc-module-hover-'));
    try {
        fs.mkdirSync(path.join(folder, 'src/lib'), { recursive: true });
        fs.writeFileSync(path.join(folder, 'src/lib/widget.zc'), 'namespace widget;\n\n// converts widget coordinates into grid positions.\n\nint position(int x) { return x; }\n');
        const r = run('include widget; fn main() { return widget.position(3); }', [{ needle: 'widget;' }], 'file://' + path.join(folder, 'main.zc'), [[path.join(folder, 'src/lib/widget.zc'), fs.readFileSync(path.join(folder, 'src/lib/widget.zc'), 'utf8')]]);
        assert.ok(!r.ds.some(d => d.severity === 1), JSON.stringify(r.ds));
        assert.match(r.result(2)?.contents.value || '', /converts widget coordinates into grid positions/);
        fs.writeFileSync(path.join(folder, 'src/lib/widget.zc'), 'namespace widget;\n\n/** converts grid positions into widget coordinates. */\n\nint position(int x) { return x; }\n');
        const block = run('include widget; fn main() { return widget.position(3); }', [{ needle: 'widget;' }], 'file://' + path.join(folder, 'main.zc'), [[path.join(folder, 'src/lib/widget.zc'), fs.readFileSync(path.join(folder, 'src/lib/widget.zc'), 'utf8')]]);
        assert.match(block.result(2)?.contents.value || '', /converts grid positions into widget coordinates/);
    } finally { fs.rmSync(folder, { recursive: true, force: true }); }
});
assert.ok(checks, 'no selected protocol contracts: ' + group);
process.exitCode = failures ? 1 : 0;