///|
pub(all) struct SourceLocation {
scalar_offset : Int
utf16_offset : Int
line : Int
column : Int
utf16_column : Int
} derive(Eq, Debug)
///|
pub(all) struct SourceRange {
start : SourceLocation
end : SourceLocation
} derive(Eq, Debug)
///|
pub fn locate_source(source : String, scalar_offset : Int) -> SourceLocation {
let mut offset = 0
let mut utf16 = 0
let mut line = 1
let mut column = 1
let mut utf16_column = 1
for c in source {
if offset >= scalar_offset {
break
}
offset += 1
utf16 += c.utf16_len()
if c == '\n' {
line += 1
column = 1
utf16_column = 1
} else {
column += 1
utf16_column += c.utf16_len()
}
}
{ scalar_offset: offset, utf16_offset: utf16, line, column, utf16_column }
}
///|
pub fn Diagnostic::source_range(
self : Diagnostic,
source : String,
) -> SourceRange {
let start = locate_source(source, self.span.start)
let end = locate_source(
source,
if self.span.end < self.span.start {
self.span.start
} else {
self.span.end
},
)
{ start, end }
}
///|
fn terminal_safe(text : String) -> String {
let out = StringBuilder()
for c in text {
if c.is_control() {
out.write_char('�')
} else {
out.write_char(c)
}
}
out.to_string()
}
///|
pub fn diagnostic_hint(code : String) -> String {
match code {
"lex.string" | "lex.escape" =>
"补齐双引号;Sieve 中反斜杠引用下一个字符,不把反斜杠 n 当换行。"
"lex.text" =>
"text: 后换行,并用单独的点号行终止;正文行开头的点需要再写一个点。"
"lex.comment" => "检查 /* 注释是否有对应的 */。"
"lex.number" =>
"数量支持 K/M/G,结果必须在 0 到 2147483647 之间。"
"parse.string_list" =>
"使用非空字符串列表,例如 [\"from\", \"to\"],不要加尾随逗号。"
"parse.expected" =>
"检查分号、括号、花括号和列表分隔逗号。"
"parse.orphan_branch" => "elsif/else 必须紧跟同一个 if 结构。"
"check.require" => "在脚本开头加入对应的 require 声明。"
"check.require_order" =>
"把 require 放到顶层所有执行语句之前。"
"check.capability" =>
"调用 supported_capabilities() 查看当前实现支持的扩展;不支持的扩展会拒绝而不是静默跳过。"
"check.arity" | "check.single" | "check.string" =>
"对照命令签名检查参数数量,以及单个字符串和字符串列表的区别。"
"check.tag_order" => "把所有标签及其值放在位置参数之前。"
"check.conflicting_tags" | "check.duplicate_tag" =>
"每组选项只保留一种,例如 :is 与 :contains 不能同时出现。"
"check.numeric_match" =>
"数值比较器只支持相等或关系比较,不支持通配符和子串查找。"
"message.header_value" =>
"由 MIME/邮件解析器先展开折叠行并解码头字段,再传入规范化值。"
"message.envelope" | "runtime.redirect" =>
"传入裸邮箱地址,不要包含显示名、多个地址或换行。"
"limit.comparisons" =>
"缩短通配符和输入,或在受信任边界内合理调整 comparisons;不要禁用预算。"
"limit.depth" => "减少规则或注释嵌套层数。"
"limit.expansion" =>
"减少变量拼接后的长度;变量替换不是递归展开。"
"limit.actions" =>
"减少单封邮件产生的不同目的地;不会返回半份投递计划。"
_ =>
"错误带有稳定 code 和源码范围;调用方应按 code 分类,而不是匹配展示文本。"
}
}
///|
pub fn Diagnostic::render(
self : Diagnostic,
source : String,
filename? : String = "