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 工作流程

  1. trackPositions: true 解析源码(始终开启——你不需要手动设置)
  2. 逐条运行规则的 check(ctx) 函数
  3. 收集通过 ctx.report() 上报的所有诊断
  4. 按源码偏移排序后返回
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 不会失效。

规则出错:failFastonRuleError

如果规则在 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。