少一点口头规范,多一层工程护栏

You,23 min read

少一点口头规范,多一层工程护栏

这篇文章整理自一次内部前端代码质量工具建设实践。为了方便公开分享,文中对公司名、包名、内部规则名、私有组件、仓库地址和业务数据都做了脱敏。示例只用于说明思路,不代表任何线上实现。

适合谁看

这篇文章更适合下面几类同学看:

文章不会深入编译原理或 AST 的底层细节,而是从工程视角讲清楚:我们为什么要做、它长什么样、技术上怎么搭起来,以及哪些地方我们也还没想得特别清楚。

dSvkEs

难的不是写规范,而是让规范每天生效

在团队还小的时候,代码质量靠 Code Review、几份文档和工程师的自觉,多半能撑住。

但当项目变多、仓库变大、业务线之间开始互相影响之后,事情会慢慢变难:

我们想做的,其实不是再发一份更完整的规范文档,而是一层更轻量的工程护栏:把那些高频、明确、能被自动判断的问题前置掉,让人可以把注意力留给真正需要判断的部分。

最后落到一个 CLI 工具

最后落下来的形态很朴素:一个命令行工具。

从目标上看,它主要做三件事:

  1. 把团队约定沉淀成可执行的规则。
  2. 把代码问题变成可定位、可统计、可追踪的数据。
  3. 通过“增量扫描”让治理的心理成本可控:新代码先不增加债务,老代码逐步还。

我们有意没有把它做成一个很重的平台。初期阶段,一个可配置、可扩展、可接入流水线的 CLI,是最容易落地、最容易被项目接受的形态。

先从增量开始,别一上来吓到大家

下面这组数据做过区间化和脱敏处理,只用于说明趋势,不代表任何单一项目的真实指标。

从结果看,增量扫描带来的最大变化不是“工具更快”这么简单,而是让质量反馈从“历史问题清单”变成了“这次提交需要关注什么”。这会显著降低团队第一次接入的心理压力。

它大概能看哪些问题

规则大致分成几类(这里使用脱敏后的描述):

我们有一个比较明确的取舍:工具更适合检测“稳定、明确、有共识”的问题。需要很多业务上下文判断的规则,我们宁愿先输出 warning、收集数据,也不轻易做强拦截。

K6eIU6

整体就是一条朴素的流水线

工具内部是一条分层的流水线,整体设计保持克制:

CLI 命令层

配置加载 & 任务编排

文件收集 & 解析

规则执行引擎

问题过滤 & 评分 & 报告生成

每一层的职责都尽量单一:

这种分层的好处是:每一条规则的作者只需要关心“怎么识别问题”,其他事情交给引擎来做。

7HTbeH

为什么不用简单字符串匹配

最早确实尝试过只用字符串或正则。简单的场景足够用,但规则一多就会碰壁:

所以我们最终在 JS / TS / JSX / TSX 等文件上,统一使用一个宽容模式的 AST 解析器,带上常见语法插件(类属性、装饰器、JSX、TypeScript 等),并开启错误恢复。这样即使某些文件的语法风格不完全统一,也不至于直接中断扫描。

一条规则长这个样子(脱敏版):

interface Rule {
  id: string;
  name: string;
  severity: 'error' | 'warn' | 'info';
  enabled: boolean;
  check(context: RuleContext): Promise<void> | void;
}

规则通过 context.report() 上报问题,引擎会统一补齐文件路径、位置、严重程度、规则 ID 等信息。规则本身尽量“只做一件事”,这也是后面能持续加规则而不互相影响的前提。

一个例子:为什么要管空 catch

以“空 catch 检测”为例。这个规则本身不复杂,但很适合自动化,因为它同时满足几个条件:团队共识高、误报相对可控、问题定位明确、修复建议也比较清晰。

一个简化后的问题代码可能是这样:

try {
  await submitForm(values);
} catch (error) {
  // 忽略异常
}

这类代码的问题不在于一定会立刻导致故障,而是它会让失败被静默吞掉。用户侧可能没有反馈,监控侧也可能没有线索,后续排查成本会被放大。

更合理的处理方式通常是至少做一件明确的事:

try {
  await submitForm(values);
} catch (error) {
  logger.warn('submit form failed', error);
  showToast('提交失败,请稍后重试');
}

规则实现时并不需要理解完整业务,只需要在 AST 中找到 CatchClause,再判断 catch body 是否为空、是否只有注释、或者是否只有缺乏后续处理的占位逻辑。为了避免过度打扰,早期可以把这类规则先设为 warning,等命中情况稳定后再决定是否升级。

增量扫描:先守住新代码

如果只能全量扫描,质量工具会遇到一个很现实的问题:老项目里历史问题太多,一扫几百上千条,大家反而不知道该从哪开始改。

所以我们把增量扫描作为核心能力之一,而不是一个“可选项”。

它的思路其实不复杂:

  1. 通过 git diff 找到这次变更的文件、新文件行号范围(hunk)。
  2. 只对变更文件执行规则扫描。
  3. 扫完之后,再按问题所在行号过滤一轮,只保留落在变更范围内的问题。
  4. 同时产出两份结果:
    • 变更文件完整问题(让你看到这个文件整体还欠多少债)。
    • 本次变更行上的问题(真正用于准入判断的)。
git diff

解析变更文件与 hunk 行号

扫描变更文件

规则发现问题

按变更行过滤

输出增量报告

hpLhuP

这个设计的价值不在算法上,而在心理预期上:历史问题可以慢慢治理,但新代码尽量不再继续欠债。这个共识并不难达成,真正落到工具上,关键就是要有这一层过滤。

Diff 解析里的一些细节

增量扫描主要依赖 git diff 的 hunk 信息,例如:

@@ -10,0 +11,3 @@

我们关心的是“新文件中的起始行”和“变更行数”,把它构建成一个按文件聚合的映射:

type DiffMap = Map<string, Array<{
  startLine: number;
  lineCount: number;
}>>;

后续判断一个问题是否“属于本次变更”时,只需要看它的起始行是不是落在任一 hunk 范围内。

实际落到真实仓库,有几个不复杂但很真实的细节需要处理:

这些点单独看都很普通,但如果不处理好,工具会一会儿跑得通、一会儿跑不通,反而影响团队对工具的信任。

配置:默认能用,也允许慢慢调

工具提供几种预设:默认模式、严格模式、精简模式。项目也可以通过一个配置文件覆盖规则启用状态、严重程度、忽略路径、输出格式等。

配置合并遵循这样的优先级:

命令行参数 > 项目配置文件 > 环境变量 > 默认预设

这个顺序比较符合直觉:

脱敏后的配置示例:

module.exports = {
  preset: 'default',
  rules: {
    'no-empty-catch': {
      enabled: true,
      severity: 'error',
    },
    'max-function-lines': {
      enabled: true,
      severity: 'warn',
      options: { max: 120 },
    },
  },
  ignorePatterns: [
    'node_modules/**',
    'dist/**',
    '**/*.test.{ts,tsx}',
  ],
  outputFormat: 'console',
};

有两个细节比较关键:

评分只是参考,别被数字绑架

评分系统的目的不是制造压力,而是给团队一个“粗粒度的健康度参考”。

我们没有按 issue 数量直接扣分,而是按“触发了多少类规则”扣分。原因是:同一个问题模式,在一个文件里可能重复出现几十次,如果按数量扣分,分数会被某个局部问题过度放大,但实际修复成本未必同等增加。

脱敏后的思路如下:

最终得分 = 100 - 错误级规则数 × A - 警告级规则数 × B

分数最后会映射到一个“优秀 / 良好 / 中等 / 及格 / 不及格”的档位,主要用来沟通,不适合做硬指标。

真正有价值的其实是:

报告要让不同人都看得懂

一个工具如果只能输出到终端,很难同时服务本地开发和平台化消费。所以我们有三种报告:

报告里会包含文件路径、规则 ID、严重程度、位置、简要建议等信息。对于本地场景,还会生成指向编辑器的跳转链接(例如 VSCode 的 vscode://file/...),尽量减少“看到问题但找不到位置”的摩擦。

RQX59R

CI 接入:更适合在 PR 阶段提醒

在 CI 上,我们更推荐在 PR 阶段跑增量扫描,而不是每次都全量扫。脱敏后的一个典型配置:

name: Code Quality Check
 
on:
  pull_request:
 
jobs:
  quality-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: 18
      - run: npm install -g your-code-quality-cli
      - run: your-code-quality-cli scan ./src --diff origin/main...HEAD --output json --out-file quality-report.json

这里有几个实践中比较容易忽略的点:

做规则时的一些经验

做完这一轮之后,有几个自己反复验证过的感受:

第一,规则应该从高共识的问题开始。空 catch、危险 API、明文敏感信息、明显偏离基础设施的写法,这类问题大家不太会吵,上线阻力小,也更容易建立对工具的信任。

第二,规则需要分级,不要一开始就全 error。强安全、强稳定问题设 error;风格、复杂度、迁移建议更适合先设 warn。过早阻断容易让团队把工具理解成额外负担,而不是工程辅助。

第三,误报成本要认真对待。工具一旦频繁误报,大家很快就会失去耐心、甚至开始整体忽略。边界不清的规则,我们更倾向先只上报、不阻断,收集数据之后再决定是否升级。

第四,治理节奏比规则数量更重要。一次上线几十条规则看起来很完整,但未必有效。更可持续的是小步上线、观察反馈、修正规则、再推广。

还没做好的地方

说实话,这个工具离“好用”还差不少:

这些问题不影响它现在发挥价值,但会决定它能不能从“能用”真正走向“好用”。

最后回头看

回过头看,这个工具最大的价值其实不是“写了多少条规则”,也不是“扫得多快”,而是:

把团队中原本分散在文档、评审经验、群聊里的工程共识,变成了一种可以运行、可以反馈、可以持续迭代的机制。

代码治理这件事,很难靠一次专项行动完成。更现实的路径是先守住新代码,再逐步改善旧代码;先自动化明确问题,再把人的注意力留给复杂判断。

如果一定要用一句话概括的话,大概是:我们不是想做一个“挑错工具”,而是想在日常开发里多加一层温和但稳定的工程护栏。它不替代人,但可以让人少被重复问题打扰。


2026 © Lizhenyui.