zh CN Lint - chiba233/yume-dsl-token-walker GitHub Wiki
Lint
面向 DSL 源码的最小 lint 框架。规则在结构解析树上运行,上报诊断结果,支持可选的自动修复。
什么时候用:
- 强制约定(不允许空标签、最大嵌套深度、必填属性)
- 渲染前抓错(未闭合标签已经由 parser 处理了,但语义规则由你定义)
- 自动修复常见问题(删除空标签、重命名已弃用标签)
Demo:"禁止空标签"+ 自动修复
一个完整的规则——检测空 inline 标签并提供删除修复:
import {lintStructural, applyLintFixes, type LintRule} from "yume-dsl-token-walker";
const noEmptyTag: LintRule = {
id: "no-empty-tag",
severity: "warning",
check: (ctx) => {
ctx.walk(ctx.tree, (node) => {
if (node.type === "inline" && node.children.length === 0 && node.position) {
ctx.report({
message: `空的 inline 标签: ${node.tag}`,
span: node.position,
node,
fix: {
description: "删除空标签",
edits: [{span: node.position, newText: ""}],
},
});
}
});
},
};
const source = "Hello $$bold()$$ world";
const diagnostics = lintStructural(source, {rules: [noEmptyTag]});
// diagnostics[0].message === "空的 inline 标签: bold"
const fixed = applyLintFixes(source, diagnostics);
// fixed === "Hello world"
lintStructural 工作流程
- 用
trackPositions: true解析源码(始终开启——你不需要手动设置) - 逐条运行规则的
check(ctx)函数 - 收集通过
ctx.report()上报的所有诊断 - 按源码偏移排序后返回
const lintStructural: (source: string, options: LintOptions) => Diagnostic[];
如果你用了自定义语法或 handler,通过 parseOptions 传入——否则 lint 可能接受你的运行时 parser 会拒绝的结构:
const diagnostics = lintStructural(source, {
rules: [noEmptyTag],
parseOptions: {
handlers: myHandlers,
syntax: mySyntax,
allowForms: ["inline"],
},
});
applyLintFixes 工作原理
将可修复的诊断应用到源码。只处理带 fix 字段的诊断。
const applyLintFixes: (source: string, diagnostics: Diagnostic[]) => string;
核心设计:原子化 per-fix,先到先得。
- 每个
Fix有一个或多个TextEdit。如果一个 fix 的任何一条 edit 和之前已接受的 edit 重叠,整个 fix 被跳过——不是只跳重叠的那条。这防止复合修复把源码留在无效的中间状态。 - 两个 fix 的 edit 重叠时,最早出现在源码中的 fix 胜出。后面的 fix 整体被拒绝。
- 自身 edit 内部重叠的 fix(畸形 fix)也会被拒绝。
- 已接受的 edit 按源码倒序应用,保证前面的 offset 不会失效。
规则出错:failFast 和 onRuleError
如果规则在 check 中抛异常,默认静默跳过——其他规则继续运行,你拿到成功规则的诊断结果。
两种 opt-in 机制:
onRuleError——观察并继续
const diagnostics = lintStructural(source, {
rules,
onRuleError: ({ruleId, error}) => {
console.warn(`规则 "${ruleId}" 失败:`, error);
},
});
// 其他规则仍然运行了,它们的诊断结果正常返回
failFast——立即中止
try {
const diagnostics = lintStructural(source, {
rules,
failFast: true,
});
// 可以信任:没有规则崩掉
} catch (error) {
// 某条 lint 规则自己出错了
// error.message 包含 rule id
// error.cause 是原始错误(当原始错误是 Error 实例时)
}
failFast 优先级高于 onRuleError——两者同时设置时,错误直接抛出,不调用 onRuleError。
Demo:多规则 lint + severity 覆盖
import {lintStructural, type LintRule} from "yume-dsl-token-walker";
const noEmptyTag: LintRule = {
id: "no-empty-tag",
severity: "warning",
check: (ctx) => {
ctx.walk(ctx.tree, (node) => {
if (node.type === "inline" && node.children.length === 0 && node.position) {
ctx.report({message: `空标签: ${node.tag}`, span: node.position, node});
}
});
},
};
const maxDepth: LintRule = {
id: "max-depth",
severity: "error",
check: (ctx) => {
ctx.walk(ctx.tree, (node, visitCtx) => {
if (visitCtx.depth >= 3 && node.type === "inline" && node.position) {
ctx.report({
message: `嵌套过深 (${visitCtx.depth})`,
span: node.position,
node,
});
}
});
},
};
const diagnostics = lintStructural("$$bold($$italic($$underline($$strike(x)$$)$$)$$)$$", {
rules: [noEmptyTag, maxDepth],
overrides: {
"no-empty-tag": "off", // 完全禁用这条规则
"max-depth": "warning", // 从 error 降级为 warning
},
});
LintContext
传给每条规则的 check 函数:
interface LintContext {
source: string;
tree: StructuralNode[];
report: (info: ReportInfo) => void;
findFirst: (nodes: StructuralNode[], predicate: StructuralPredicate) => StructuralNode | undefined;
findAll: (nodes: StructuralNode[], predicate: StructuralPredicate) => StructuralNode[];
walk: (nodes: StructuralNode[], visitor: StructuralVisitor) => void;
}
| 字段 | 说明 |
|---|---|
source |
原始源码文本 |
tree |
结构树(始终带 trackPositions: true) |
report |
上报诊断——每找到一个问题就调一次 |
findFirst |
和独立的 findFirst 相同 |
findAll |
和独立的 findAll 相同 |
walk |
和独立的 walkStructural 相同 |
类型参考
LintRule
interface LintRule {
id: string;
severity?: DiagnosticSeverity; // 默认 "warning"
check: (ctx: LintContext) => void;
}
LintOptions
interface LintOptions {
rules: LintRule[];
overrides?: Record<string, DiagnosticSeverity | "off">;
parseOptions?: Omit<StructuralParseOptions, "trackPositions">;
onRuleError?: (context: { ruleId: string; error: unknown }) => void;
failFast?: boolean;
}
Diagnostic
interface Diagnostic {
ruleId: string;
severity: DiagnosticSeverity;
message: string;
span: SourceSpan;
node?: StructuralNode;
fix?: Fix;
}
DiagnosticSeverity
type DiagnosticSeverity = "error" | "warning" | "info" | "hint";
Fix / TextEdit
interface Fix {
description: string;
edits: TextEdit[];
}
interface TextEdit {
span: SourceSpan;
newText: string; // 空字符串表示删除
}
遵循 LSP TextEdit 模型——每条 edit 指定要替换的源码范围。newText 为空则删除,start === end 则插入。
ReportInfo
type ReportInfo = Omit<Diagnostic, "ruleId" | "severity"> & {
severity?: DiagnosticSeverity;
};
ctx.report() 的参数。ruleId 由 runner 自动添加;severity 默认使用规则的 severity。