244 lines
6.7 KiB
Plaintext
244 lines
6.7 KiB
Plaintext
// Expect retcode = 0
|
||
// 诊断收集与渲染:severity + 文件/行/列 + 消息;统计错误/警告数。
|
||
// 消息与文件名按拥有型 Str 保存(深拷贝),Diag 拥有它们直到 clear/drop。
|
||
// 渲染输出到 ByteBuffer;不直接写文件,便于 LSP/测试。
|
||
|
||
type .{ .Error, .Status, .make } = @import("error.spl");
|
||
type .{ .Vec } = @import("vec.spl");
|
||
type .{ .ByteBuffer } = @import("byte_buffer.spl");
|
||
type .{ .Str } = @import("str.spl");
|
||
type .{ .append_usize } = @import("fmt.spl");
|
||
|
||
type Severity = enum {
|
||
Debug,
|
||
Info,
|
||
Warning,
|
||
Error,
|
||
Fatal,
|
||
}
|
||
|
||
type DiagItem = struct {
|
||
severity: Severity,
|
||
line: usize,
|
||
col: usize,
|
||
fname: Str,
|
||
message: Str,
|
||
}
|
||
|
||
type Diag = @self;
|
||
|
||
items: Vec, // 元素类型 DiagItem
|
||
errors: usize,
|
||
warnings: usize,
|
||
|
||
fn new(): Diag {
|
||
ret Diag {
|
||
.items = Vec.blank(@sizeof(DiagItem), 8),
|
||
.errors = 0,
|
||
.warnings = 0,
|
||
};
|
||
}
|
||
|
||
fn item_at(self: *Diag, index: usize): *DiagItem {
|
||
match Vec.get(&self.items, index) {
|
||
.Ok(ptr) => { ret ptr as *DiagItem; },
|
||
.Err(error) => { ret null; },
|
||
}
|
||
ret null;
|
||
}
|
||
|
||
fn count(self: Diag): usize {
|
||
ret self.items.len;
|
||
}
|
||
|
||
fn error_count(self: Diag): usize {
|
||
ret self.errors;
|
||
}
|
||
|
||
fn warning_count(self: Diag): usize {
|
||
ret self.warnings;
|
||
}
|
||
|
||
fn has_error(self: Diag): bool {
|
||
ret self.errors > 0;
|
||
}
|
||
|
||
// 添加一条诊断;失败不改变已有内容。
|
||
fn emit(self: *Diag, fname: []u8, line: usize, col: usize, severity: Severity,
|
||
msg: []u8): Status {
|
||
var item = DiagItem {
|
||
.severity = severity,
|
||
.line = line,
|
||
.col = col,
|
||
.fname = Str.new(),
|
||
.message = Str.new(),
|
||
};
|
||
|
||
match Str.from_slice(fname) {
|
||
.Ok(v) => { item.fname = v; },
|
||
.Err(error) => { ret .Err(error); },
|
||
}
|
||
match Str.from_slice(msg) {
|
||
.Ok(v) => { item.message = v; },
|
||
.Err(error) => {
|
||
Str.drop(&item.fname);
|
||
ret .Err(error);
|
||
},
|
||
}
|
||
match Vec.push(&self.items, &item as *u8) {
|
||
.Ok => {},
|
||
.Err(error) => {
|
||
Str.drop(&item.fname);
|
||
Str.drop(&item.message);
|
||
ret .Err(error);
|
||
},
|
||
}
|
||
|
||
match severity {
|
||
.Error => { self.errors = self.errors + 1; },
|
||
.Fatal => { self.errors = self.errors + 1; },
|
||
.Warning => { self.warnings = self.warnings + 1; },
|
||
_ => {},
|
||
}
|
||
ret .Ok;
|
||
}
|
||
|
||
fn err(self: *Diag, fname: []u8, line: usize, col: usize, msg: []u8): Status {
|
||
ret emit(self, fname, line, col, .Error, msg);
|
||
}
|
||
|
||
fn warn(self: *Diag, fname: []u8, line: usize, col: usize, msg: []u8): Status {
|
||
ret emit(self, fname, line, col, .Warning, msg);
|
||
}
|
||
|
||
fn info(self: *Diag, fname: []u8, line: usize, col: usize, msg: []u8): Status {
|
||
ret emit(self, fname, line, col, .Info, msg);
|
||
}
|
||
|
||
fn level_name(severity: Severity): []u8 {
|
||
match severity {
|
||
.Debug => { ret "debug"; },
|
||
.Info => { ret "info"; },
|
||
.Warning => { ret "warning"; },
|
||
.Error => { ret "error"; },
|
||
.Fatal => { ret "fatal"; },
|
||
}
|
||
ret "?";
|
||
}
|
||
|
||
fn append_str(out: *ByteBuffer, s: *Str): Status {
|
||
ret ByteBuffer.append(out, Str.as_slice(s));
|
||
}
|
||
|
||
// 渲染为 "file:line:col: level: message\n"(file 为空时省略前缀)。
|
||
fn render(self: *Diag, out: *ByteBuffer): Status {
|
||
var i: usize = 0;
|
||
while i < self.items.len {
|
||
var it = item_at(self, i);
|
||
if it == null { ret .Err(make(.OutOfRange)); }
|
||
|
||
if Str.size(it.fname) != 0 {
|
||
match append_str(out, &it.fname) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||
match ByteBuffer.append_byte(out, ':') { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||
}
|
||
match append_usize(out, it.line, 10, false) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||
match ByteBuffer.append_byte(out, ':') { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||
match append_usize(out, it.col, 10, false) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||
match ByteBuffer.append(out, ": ") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||
match ByteBuffer.append(out, level_name(it.severity)) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||
match ByteBuffer.append(out, ": ") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||
match append_str(out, &it.message) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||
match ByteBuffer.append_byte(out, '\n') { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
||
i = i + 1;
|
||
}
|
||
ret .Ok;
|
||
}
|
||
|
||
// 丢弃所有条目内容但保留缓冲区。
|
||
fn clear(self: *Diag) {
|
||
var i: usize = 0;
|
||
while i < self.items.len {
|
||
var it = item_at(self, i);
|
||
if it != null {
|
||
Str.drop(&it.fname);
|
||
Str.drop(&it.message);
|
||
}
|
||
i = i + 1;
|
||
}
|
||
Vec.clear(&self.items);
|
||
self.errors = 0;
|
||
self.warnings = 0;
|
||
}
|
||
|
||
fn drop(self: *Diag) {
|
||
var i: usize = 0;
|
||
while i < self.items.len {
|
||
var it = item_at(self, i);
|
||
if it != null {
|
||
Str.drop(&it.fname);
|
||
Str.drop(&it.message);
|
||
}
|
||
i = i + 1;
|
||
}
|
||
Vec.drop(&self.items);
|
||
self.errors = 0;
|
||
self.warnings = 0;
|
||
}
|
||
|
||
// ---- 测试辅助 ----
|
||
|
||
fn expect_ok(result: Status) {
|
||
match result {
|
||
.Ok => {},
|
||
.Err(error) => { @assert(false, "unexpected diag error"); },
|
||
}
|
||
}
|
||
|
||
fn expect_bytes(out: *ByteBuffer, expected: []u8) {
|
||
var actual = ByteBuffer.as_slice(out.*);
|
||
@assert(actual.len == expected.len, "render length");
|
||
var i: usize = 0;
|
||
while i < expected.len {
|
||
@assert(actual.ptr[i] == expected.ptr[i], "render byte");
|
||
i = i + 1;
|
||
}
|
||
}
|
||
|
||
fn test_collect_and_count() {
|
||
var d = new();
|
||
@assert(count(d) == 0 && !has_error(d), "empty");
|
||
|
||
expect_ok(err(&d, "a.spl", 3, 5, "undefined identifier `x`"));
|
||
expect_ok(warn(&d, "a.spl", 7, 1, "unused variable"));
|
||
expect_ok(info(&d, "a.spl", 9, 2, "note"));
|
||
expect_ok(err(&d, "", 1, 1, "fatal-ish"));
|
||
|
||
@assert(count(d) == 4, "count");
|
||
@assert(error_count(d) == 2, "errors");
|
||
@assert(warning_count(d) == 1, "warnings");
|
||
@assert(has_error(d), "has_error");
|
||
|
||
var out = ByteBuffer.new();
|
||
expect_ok(render(&d, &out));
|
||
expect_bytes(&out, "a.spl:3:5: error: undefined identifier `x`\na.spl:7:1: warning: unused variable\na.spl:9:2: info: note\n1:1: error: fatal-ish\n");
|
||
ByteBuffer.drop(&out);
|
||
drop(&d);
|
||
}
|
||
|
||
fn test_clear_and_reuse() {
|
||
var d = new();
|
||
expect_ok(err(&d, "f", 1, 1, "one"));
|
||
expect_ok(warn(&d, "f", 2, 2, "two"));
|
||
clear(&d);
|
||
@assert(count(d) == 0 && !has_error(d) && warning_count(d) == 0, "cleared");
|
||
|
||
expect_ok(err(&d, "f", 3, 3, "three"));
|
||
@assert(count(d) == 1 && error_count(d) == 1, "reuse");
|
||
drop(&d);
|
||
}
|
||
|
||
fn main() {
|
||
test_collect_and_count();
|
||
test_clear_and_reuse();
|
||
}
|