zh CN Tutorial Editor Lint - chiba233/yume-dsl-token-walker GitHub Wiki

教程:编辑器 Lint + 自动修复

首页 | Lint | 结构查询

构建一个完整的 lint 管线——校验 DSL 源码、上报带源码位置的诊断、自动修复常见错误。这正是你接入 CodeMirror / Monaco 编辑器或 CI 检查时需要的东西。

你将学到:

  1. LintRuleLintContext 写 lint 规则
  2. 给诊断附加自动修复
  3. applyLintFixes 原子化应用修复
  4. 按项目覆盖 severity、禁用规则
  5. onRuleErrorfailFast 处理规则错误
  6. 让 lint 和运行时 parser 共享配置

前置条件: npm install yume-dsl-rich-text yume-dsl-token-walker


第 1 步:最简规则——禁止空标签

最基础的规则:标记 $$bold()$$(零子节点的 inline 标签)为 warning。

import { type LintRule } from "yume-dsl-token-walker";

export 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,
                });
            }
        });
    },
};

ctx.walk 带深度/parent 上下文遍历结构树的每个节点。ctx.report 上报诊断。span 是源码范围——编辑器波浪线和自动修复都需要它。


第 2 步:带自动修复的规则

给诊断加一个 fix——applyLintFixes 会用它:

export const noEmptyTagWithFix: 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: "" }],
                    },
                });
            }
        });
    },
};

每个 Fix 有一个或多个 TextEdit。每条 edit 用新文本替换一段源码范围。newText 为空 = 删除。


第 3 步:嵌套深度规则

const MAX_DEPTH = 3;

export const maxNestingDepth: LintRule = {
    id: "max-nesting-depth",
    severity: "error",
    check: (ctx) => {
        ctx.walk(ctx.tree, (node, visitCtx) => {
            if (
                visitCtx.depth >= MAX_DEPTH &&
                (node.type === "inline" || node.type === "block") &&
                node.position
            ) {
                ctx.report({
                    message: `标签嵌套过深(深度 ${visitCtx.depth},上限 ${MAX_DEPTH})`,
                    span: node.position,
                    node,
                });
            }
        });
    },
};

第 4 步:运行 lint 并应用修复

import { lintStructural, applyLintFixes } from "yume-dsl-token-walker";

const source = `Hello $$bold()$$ world $$italic($$underline($$strike($$code(deep)$$)$$)$$)$$`;

// 运行所有规则
const diagnostics = lintStructural(source, {
    rules: [noEmptyTagWithFix, maxNestingDepth],
});

// 应用所有可修复的诊断
const fixed = applyLintFixes(source, diagnostics);
// 空的 $$bold()$$ 被删除;嵌套错误没有 fix 所以保持原样

applyLintFixes 工作原理:

  • 只处理带 fix 字段的诊断
  • 修复是原子化 per-fix 的——如果一个 fix 的任何一条 edit 和之前已接受的 edit 重叠,整个 fix 被跳过
  • 按源码偏移先到先得
  • 已接受的 edit 按倒序应用,保证前面的 offset 不失效

第 5 步:和运行时 parser 共享配置

如果你的运行时 parser 用了自定义语法或 handler,lint 也要传同一份配置——否则 lint 可能接受你的 parser 会拒绝的结构:

import { createParser, createSimpleInlineHandlers } from "yume-dsl-rich-text";

const parserOptions = {
    handlers: createSimpleInlineHandlers(["bold", "italic"]),
    allowForms: ["inline"] as const,
};

const parser = createParser(parserOptions);

const diagnostics = lintStructural(source, {
    rules: [noEmptyTagWithFix],
    parseOptions: parserOptions,  // 和 parser 同一份配置
});

第 6 步:severity 覆盖和禁用规则

const diagnostics = lintStructural(source, {
    rules: [noEmptyTagWithFix, maxNestingDepth],
    overrides: {
        "no-empty-tag": "off",          // 完全禁用
        "max-nesting-depth": "warning", // 从 error 降级为 warning
    },
});

第 7 步:错误处理

如果规则在 check 中抛异常,默认静默跳过,其他规则继续。

// 观察错误
const diagnostics = lintStructural(source, {
    rules,
    onRuleError: ({ ruleId, error }) => {
        console.warn(`规则 "${ruleId}" 崩了:`, error);
    },
});

// 或者 fail fast——第一个错误立即中止
try {
    const diagnostics = lintStructural(source, { rules, failFast: true });
} catch (error) {
    // error.message 包含 rule id
    // error.cause 是原始错误(当原始错误是 Error 实例时)
}

failFast 优先级高于 onRuleError


完整示例:CI lint 检查

import { lintStructural, type LintRule } from "yume-dsl-token-walker";

const rules: LintRule[] = [noEmptyTagWithFix, maxNestingDepth];

function lintFile(source: string): boolean {
    const diagnostics = lintStructural(source, { rules, failFast: true });

    for (const d of diagnostics) {
        const pos = d.span.start;
        console.log(
            `${d.severity.toUpperCase()} [${d.ruleId}] ${d.message} (${pos.line}:${pos.column})`,
        );
    }

    const hasErrors = diagnostics.some((d) => d.severity === "error");
    return !hasErrors;
}

const ok = lintFile(source);
process.exit(ok ? 0 : 1);

下一步