aboutsummaryrefslogtreecommitdiff
path: root/tests/drivers/storage_analysis_test.js
blob: da967f54684ed605ec592531b89036cb833c7854 (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
// definite storage findings use the compiler's parser and the editor protocol.
const assert = require('assert');
const cp = require('child_process');
const server = process.argv[2];
function check(text, needle) {
    const uri = 'file:///storage-analysis.zc';
    const messages = [{ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} },
        { jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, text } } }];
    if (needle) {
        const at = text.lastIndexOf(needle), before = text.slice(0, at);
        assert.ok(at >= 0);
        messages.push({ jsonrpc: '2.0', id: 2, method: 'textDocument/hover', params: { textDocument: { uri }, position: { line: before.split('\n').length - 1, character: before.length - before.lastIndexOf('\n') - 1 } } });
    }
    messages.push({ jsonrpc: '2.0', method: 'exit' });
    const input = Buffer.concat(messages.map(m => { const b = Buffer.from(JSON.stringify(m)); return Buffer.concat([Buffer.from('Content-Length: ' + b.length + '\r\n\r\n'), b]); }));
    const r = cp.spawnSync(server, process.argv.slice(3), { input, timeout: 20000, maxBuffer: 8 << 20 });
    assert.strictEqual(r.status, 0, String(r.stderr));
    const replies = []; let data = r.stdout;
    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]);
        replies.push(JSON.parse(data.subarray(end + 4, end + 4 + n)));
        data = data.subarray(end + 4 + n);
    }
    const diagnostics = replies.find(m => m.method === 'textDocument/publishDiagnostics').params.diagnostics;
    return { diagnostics, warnings: diagnostics.filter(d => d.severity === 2), hover: replies.find(m => m.id === 2)?.result?.contents?.value };
}
const prefix = 'unchecked fn main() {\n';
for (const [type, length] of [['int', 3], ['u8', 3], ['fixed []int', 3], ['[]u8', 3]]) {
    const r = check(prefix + ` let p = new(${type}, ${length});\n let alias = p;\n return alias[${length}];\n}\n`, 'alias[');
    assert.strictEqual(r.warnings.length, 1, JSON.stringify(r.diagnostics));
    assert.match(r.warnings[0].message, /out-of-bounds.*index 3.*length 3/);
    assert.match(r.hover, /size: 3 elements.*lifetime: heap storage; live here/);
    assert.strictEqual(r.warnings[0].range.start.line, 3);
}
const negative = check(prefix + ' let p = new(3); return p[-1];\n}\n');
assert.match(negative.warnings[0]?.message || '', /index -1.*length 3/);
const freed = check(prefix + ' let p = new(int, 3);\n let q = p;\n free(p);\n return q[0];\n}\n', 'q[');
assert.strictEqual(freed.warnings.length, 1, JSON.stringify(freed.diagnostics));
assert.match(freed.warnings[0].message, /use after free.*line 4/);
assert.match(freed.hover, /size: 3 elements.*lifetime: ended on line 4/);
const dereference = check(prefix + ' let p = new(int); free(p); return *p;\n}\n');
assert.match(dereference.warnings[0]?.message || '', /use after free/);
const field = check('struct item { int value; };\n' + prefix + ' let p = new(item); free(p); return p.value;\n}\n');
assert.match(field.warnings[0]?.message || '', /use after free/);
const scope = check(prefix + ' int* p;\n { let a[3]; p = a; }\n return p[0];\n}\n', 'p[');
assert.match(scope.warnings[0]?.message || '', /use after free.*line 3/);
assert.match(scope.hover, /lifetime: ended on line 3/);
const safe = [
 prefix + ' let p = new(int, 3); let q = p as u8*; return q[23]; }',
 prefix + ' let p = new(int, 3); let q = (u8*)p; return q[23]; }',
 prefix + ' int n = 2; let p = &n; let q = n; return q[2]; }',
 prefix + ' let p = new(int, 3); let ignored = 0 && free(p); return p[0]; }',
 prefix + ' let p = new(int, 3); let ignored = 1 || free(p); return p[0]; }',
 prefix + ' let p = new(int, 50); return p[(u8)300]; }',
 prefix + ' let p = new(int, 50); return p[300 as u8]; }',
 prefix + ' let p = new(int, 3); let q = p; q[2] = 7; free(p); return 0; }',
 prefix + ' let p = new(3); free(p); p = new(3); return p[0]; }',
 'fn unknown(int* p) {}\n' + prefix + ' let p = new(int, 3); unknown(p); return p[9]; }',
 'fn maybe(int n) { return n; }\n' + prefix + ' let p = new(int, 3); return p[maybe(9)]; }',
 prefix + ' let p = new(int, 3); if (1) free(p); return p[0]; }',
 prefix + ' let p = new(int, 3); while (0) free(p); return p[0]; }',
 prefix + ' let p = new(int, 3); let f = fn() { free(p); return 0; }; return p[0]; }',
 prefix + ' let p = new([]int, 0); push(p, 9); return p[0]; }',
];
for (const source of safe) assert.deepStrictEqual(check(source).warnings, [], source);
const unknown = check('int read(int* p) { return p[0]; }\nfn main() { int n = 1; return read(&n); }\n', 'p[');
assert.match(unknown.hover, /size: unknown.*lifetime: unknown/);
const stack = check('fn main() { let a[3]; return a[2]; }\n', 'a[');
assert.match(stack.hover, /size: 3 elements.*lifetime: stack storage/);
console.log('ok   definite bounds, free and scope lifetimes, alias hovers, and conservative unknown cases');