///|
pub(all) enum FindingCategory {
Syntax
Correctness
Maintainability
Portability
Style
} derive(Debug, Eq)
///|
pub(all) struct Finding {
code : String
severity : Severity
category : FindingCategory
title : String
detail : String
suggestion : String
span : SourceSpan
} derive(Debug, Eq)
///|
pub(all) struct QualityStats {
sections : Int
properties : Int
standard_properties : Int
custom_properties : Int
errors : Int
warnings : Int
information : Int
} derive(Debug, Eq)
///|
pub(all) struct QualityReport {
score : Int
grade : String
stats : QualityStats
findings : Array[Finding]
} derive(Debug, Eq)
///|
fn finding_from_diagnostic(diagnostic : Diagnostic) -> Finding {
{
code: diagnostic.code,
severity: diagnostic.severity,
category: if diagnostic.severity is Error {
Syntax
} else {
Correctness
},
title: diagnostic.message,
detail: diagnostic.message,
suggestion: diagnostic.hint,
span: diagnostic.span,
}
}
///|
fn push_finding(
findings : Array[Finding],
code : String,
severity : Severity,
category : FindingCategory,
title : String,
detail : String,
suggestion : String,
span : SourceSpan,
) -> Unit {
findings.push({ code, severity, category, title, detail, suggestion, span })
}
///|
fn count_properties(config : EditorConfig) -> Int {
let mut count = config.preamble.length()
for section in config.sections {
count += section.properties.length()
}
count
}
///|
fn audit_root(config : EditorConfig, findings : Array[Finding]) -> Unit {
if !config.root {
let span = make_span(config.path, 1, 1, 1, 0)
push_finding(
findings,
"EC4001",
Information,
Maintainability,
"未声明 root = true",
"解析器会继续搜索父目录,最终配置可能受到项目外文件影响。",
"独立项目通常应在仓库根配置中添加 `root = true`。",
span,
)
}
}
///|
fn audit_empty_sections(
config : EditorConfig,
findings : Array[Finding],
) -> Unit {
for section in config.sections {
if section.properties.length() == 0 {
push_finding(
findings,
"EC4002",
Warning,
Maintainability,
"空配置节",
"模式 `[" +
section.pattern +
"]` 没有包含任何属性,不会改变配置结果。",
"删除该节,或补充它应设置的属性。",
section.span,
)
}
}
}
///|
fn audit_duplicate_patterns(
config : EditorConfig,
findings : Array[Finding],
) -> Unit {
for index, section in config.sections {
for previous in 0.. Unit {
for section in config.sections {
for property in section.properties {
if property.raw_key != lower_ascii(property.raw_key) {
push_finding(
findings,
"EC4004",
Information,
Style,
"属性名建议使用小写",
"EditorConfig 属性不区分大小写,但统一使用小写更便于搜索和审查。",
"将 `" + property.raw_key + "` 改为 `" + property.key + "`。",
property.span,
)
}
}
}
}
///|
fn audit_encoding(config : EditorConfig, findings : Array[Finding]) -> Unit {
for section in config.sections {
for property in section.properties {
if property.key == "charset" && lower_ascii(property.value) == "utf-8-bom" {
push_finding(
findings,
"EC4005",
Information,
Portability,
"UTF-8 BOM 可能影响部分工具",
"现代工具通常更适合无 BOM 的 UTF-8;脚本解释器也可能把 BOM 当作内容。",
"若无兼容性要求,考虑使用 `charset = utf-8`。",
property.span,
)
}
}
}
}
///|
fn audit_line_length(config : EditorConfig, findings : Array[Finding]) -> Unit {
for section in config.sections {
for property in section.properties {
if property.key == "max_line_length" &&
parse_decimal(property.value) is Some(value) &&
value > 500 {
push_finding(
findings,
"EC4006",
Warning,
Maintainability,
"最大行宽异常大",
"max_line_length = " +
property.value +
" 很可能无法发挥格式约束作用。",
"确认该数值是否是笔误,或使用更符合团队规范的上限。",
property.span,
)
}
}
}
}
///|
fn audit_catch_all(config : EditorConfig, findings : Array[Finding]) -> Unit {
if config.sections.length() == 0 {
return
}
let covered = config.sections.any(section => {
section.pattern == "*" || section.pattern == "**"
})
if !covered {
push_finding(
findings,
"EC4007",
Information,
Maintainability,
"缺少通用配置节",
"当前配置只覆盖特定文件模式,新类型文件可能不会获得编码和换行设置。",
"考虑添加 `[*]` 并放置 charset、end_of_line 等通用属性。",
config.sections[0].span,
)
}
}
///|
fn report_stats(
config : EditorConfig,
findings : Array[Finding],
) -> QualityStats {
let mut standard = 0
let mut custom = 0
for section in config.sections {
for property in section.properties {
if property_schema(property.key) is Some(_) {
standard += 1
} else {
custom += 1
}
}
}
let mut errors = 0
let mut warnings = 0
let mut information = 0
for finding in findings {
match finding.severity {
Error => errors += 1
Warning => warnings += 1
Information => information += 1
}
}
{
sections: config.sections.length(),
properties: count_properties(config),
standard_properties: standard,
custom_properties: custom,
errors,
warnings,
information,
}
}
///|
fn quality_grade(score : Int) -> String {
if score >= 95 {
"优秀"
} else if score >= 85 {
"良好"
} else if score >= 70 {
"可用"
} else if score >= 50 {
"需改进"
} else {
"高风险"
}
}
///|
/// Perform a static quality audit without changing the source document.
pub fn audit(
config : EditorConfig,
initial? : Array[Diagnostic] = [],
) -> QualityReport {
let findings : Array[Finding] = []
for diagnostic in initial {
findings.push(finding_from_diagnostic(diagnostic))
}
for diagnostic in validate(config) {
findings.push(finding_from_diagnostic(diagnostic))
}
audit_root(config, findings)
audit_empty_sections(config, findings)
audit_duplicate_patterns(config, findings)
audit_property_spelling(config, findings)
audit_encoding(config, findings)
audit_line_length(config, findings)
audit_catch_all(config, findings)
let stats = report_stats(config, findings)
let score = (100 -
stats.errors * 22 -
stats.warnings * 8 -
stats.information * 2).clamp(min=0, max=100)
{ score, grade: quality_grade(score), stats, findings }
}
///|
pub fn QualityReport::has_blockers(self : QualityReport) -> Bool {
self.stats.errors > 0
}
///|
pub fn QualityReport::summary_zh(self : QualityReport) -> String {
"质量评分 " +
self.score.to_string() +
"/100(" +
self.grade +
");" +
self.stats.errors.to_string() +
" 个错误," +
self.stats.warnings.to_string() +
" 个警告," +
self.stats.information.to_string() +
" 个提示。"
}