diff options
| author | 2026-10-12 08:46:23 +0330 | |
|---|---|---|
| committer | 2026-10-12 08:46:23 +0330 | |
| commit | ecc350ca5c2eb50be8309c5f14f8bf0d87db9a02 (patch) | |
| tree | 70e0af76a1b13affff7c6cc8aac7b3e6ef960478 /tests/drivers/storage_analysis_test.js | |
a
Diffstat (limited to 'tests/drivers/storage_analysis_test.js')
| -rw-r--r-- | tests/drivers/storage_analysis_test.js | 71 |
1 files changed, 71 insertions, 0 deletions
diff --git a/tests/drivers/storage_analysis_test.js b/tests/drivers/storage_analysis_test.js new file mode 100644 index 0000000..da967f5 --- /dev/null +++ b/tests/drivers/storage_analysis_test.js @@ -0,0 +1,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'); |
