概述

本文介绍如何自定义ESLint规则,并将其整合到SonarQube平台进行统一的代码质量管理。通过这种方式,可以将团队的编码规范和最佳实践落地到实际项目中,在编码阶段对开发者进行提示和约束。

背景介绍

ESLint简介

ESLint是目前最流行的JavaScript代码静态分析工具,通过设定的语法规则来检查代码,约束代码风格,提高代码的健壮性,避免因代码不规范导致应用出现bug。

核心特点:

  • 规则可自定义,适应团队特定需求
  • 支持使用社区热门规则集(如Airbnb、Standard等)
  • 可扩展的插件机制

SonarQube简介

SonarQube是一个开源的代码质量管理平台,用于持续检测代码质量和安全漏洞。

支持语言:

  • Java, C#, C/C++, PL/SQL, Cobol
  • JavaScript, TypeScript, Python, Go
  • 等二十几种主流编程语言

为什么需要自定义规则

在实际业务中,通过自定义规则可以:

  1. 落地编码规范:将团队约定的编码规范转化为可执行的检查规则
  2. 提前发现问题:在编码阶段即时提示,而非等到代码审查
  3. 统一代码风格:多人协作时保持一致的代码风格
  4. 集中管理:通过SonarQube统一管理所有项目的代码质量

技术选型

根据SonarQube官方文档,JavaScript项目建议使用ESLint进行整合。

参考资料:

自定义ESLint规则开发

ESLint工作原理

ESLint使用Espree解析器将代码转换为AST(抽象语法树),然后基于AST进行代码检查。

示例代码分析:

let age = 10 为例,使用AST Explorer查看其AST结构:

AST结构解析:

  • 顶层节点:VariableDeclaration(变量声明)
  • 子节点包含:
    • let 关键字
    • age 标识符(Identifier)
    • 10 字面量值

通过AST,可以精确定位和检查代码的每个节点。

项目初始化

本示例将创建一个检查SQL代码中LEFT JOIN使用次数的ESLint规则。

参考项目:

安装脚手架工具

ESLint官方提供了Yeoman模板生成器,方便快速创建插件项目。

1
npm install -g yo generator-eslint

创建项目目录

目录命名格式:eslint-plugin-<plugin-name>

1
2
mkdir eslint-plugin-demo
cd eslint-plugin-demo

初始化插件项目

1
yo eslint:plugin

交互式配置:

1
2
3
4
5
? What is your name? xiaowu                           # 作者名字
? What is the plugin ID? eslint-plugin-demo # 插件ID
? Type a short description of this plugin: 最大左连接个数 # 插件描述
? Does this plugin contain custom ESLint rules? Yes # 是否包含自定义规则
? Does this plugin contain one or more processors? No # 是否包含处理器

创建具体规则

生成规则模板

1
yo eslint:rule

交互式配置:

1
2
3
4
5
6
? What is your name? xiaowu
? Where will this rule be published?
❯ ESLint Plugin # 选择ESLint插件
? What is the rule ID? no-more-left-join # 规则ID
? Type a short description of this rule: 检查SQL中LEFT JOIN的使用次数
? Type a short example of the code that will fail: # 失败示例(可跳过)

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
├── README.md
├── docs
│ └── rules
│ └── no-more-left-join.md # 规则文档
├── lib
│ ├── index.js # 入口文件
│ └── rules
│ └── no-more-left-join.js # 规则源码
├── package.json
└── tests
└── lib
└── rules
└── no-more-left-join.js # 测试文件

编写规则代码

理解核心对象

参考文档:ESLint自定义规则

核心对象说明:

  1. meta对象:包含规则的元数据

    • type:规则类型(problem/suggestion/layout)
    • docs:规则文档描述
    • fixable:是否支持自动修复
    • schema:规则配置参数
  2. context对象:提供额外功能

    • AST节点信息
    • 规则配置项
    • 报告问题的方法

AST节点分析

将需要检查的SQL代码粘贴到AST Explorer进行分析:

**分析结果:**需要监听的AST节点类型为 ExportNamedDeclaration

规则实现代码

文件路径:lib/rules/no-more-left-join.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* @fileoverview 限制SQL中LEFT JOIN的最大数量
* @author xiaowu
*/
"use strict";

const { joinValid } = require("../utilities/index");

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description: "限制SQL文件中的LEFT JOIN数量",
recommended: true,
url: null,
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
maxLeftJoin: {
type: 'number',
},
},
additionalProperties: false,
},
],
messages: {
exceedMaxJoin: 'SQL文件中的{{joinType}}数量({{count}})超过限制({{max}}次)',
},
},

create(context) {
return {
ExportNamedDeclaration(node) {
if (node.declaration?.declarations?.length) {
node.declaration.declarations.forEach(declaration => {
joinValid(declaration.init, context, 'left join');
});
}
},
};
},
};

辅助函数实现

文件路径:lib/utilities/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* 验证JOIN语句数量
* @param {Object} node - AST节点
* @param {Object} context - ESLint上下文
* @param {string} joinStr - JOIN类型(如'left join')
*/
function joinValid(node, context, joinStr) {
if (!node) {
return;
}

// 获取配置的最大JOIN数量,默认为3
let maxJoin = 3;
if (context.options[0]?.maxLeftJoin !== undefined && joinStr === 'left join') {
maxJoin = context.options[0].maxLeftJoin;
}
if (context.options[0]?.maxRightJoin !== undefined && joinStr === 'right join') {
maxJoin = context.options[0].maxRightJoin;
}

// 处理模板字符串
if (node.type === 'TaggedTemplateExpression') {
node = node.quasi;
}

let literal = "";

// 处理箭头函数(带参数的SQL)
if (node.type === 'ArrowFunctionExpression') {
if (node.body.type === 'TemplateLiteral' && node.body.quasis.length) {
literal = node.body.quasis.map(quasi => quasi.value.raw).join('');
}
}
// 处理模板字面量(不带参数的SQL)
else if (node.type === 'TemplateLiteral' && node.quasis.length) {
literal = node.quasis.map(quasi => quasi.value.raw).join('');
}

// 检查是否为SQL查询
if (literal.length > 0 && isSqlQuery(literal)) {
literal = sqlFormat(literal);
const joinCount = literal.split(joinStr).length - 1;

if (joinCount > maxJoin) {
context.report({
node,
message: `SQL文件中的${joinStr}数量超过${maxJoin}次`,
});
}
}
}

module.exports = {
joinValid,
};

单元测试

文件路径:tests/lib/rules/no-more-left-join.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* @fileoverview 最大LEFT JOIN数量测试
* @author xiaowu
*/
"use strict";

const rule = require("../../../lib/rules/no-more-left-join");
const RuleTester = require("eslint").RuleTester;

const ruleTester = new RuleTester({
parserOptions: {
sourceType: 'module',
ecmaVersion: 2015,
},
});

ruleTester.run("no-more-left-join", rule, {
// 应该报错的测试用例
invalid: [
{
code: `export const getContractsSql = \`
SELECT *
FROM x
LEFT JOIN a ON a.id = x.id
LEFT JOIN b ON b.id = x.id
WHERE 1=1 \${sqlConditions}
ORDER BY update_time DESC
\${sqlLimit}
\`;`,
options: [{ maxLeftJoin: 1 }],
errors: [{
message: 'SQL文件中的 left join 数量超过 1 次',
}],
},
],

// 不应该报错的测试用例
valid: [
{
code: "import {inspect} from 'util';",
options: [{ maxLeftJoin: 1 }],
},
{
code: `export const getSimpleSql = \`
SELECT *
FROM x
LEFT JOIN a ON a.id = x.id
WHERE 1=1
\`;`,
options: [{ maxLeftJoin: 1 }],
},
],
});

运行测试:

1
npm run test

预期输出:2 passing

项目发布

详细的npm包发布流程请参考:npm包发布指南

发布步骤概览:

  1. 确保package.json配置正确
  2. 登录npm账号:npm login
  3. 发布包:npm publish

规则应用

安装依赖

在需要使用该规则的项目中安装:

1
npm install eslint-plugin-demo -D

配置ESLint

文件路径:.eslintrc.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
// ...其他配置

// 引入插件(eslint-plugin-前缀可省略)
plugins: ['demo'],

// 配置规则
rules: {
// 限制LEFT JOIN最大数量为1次
"demo/no-more-left-join": ["error", { maxLeftJoin: 1 }],
},

// ...其他配置
};

规则配置说明:

  • "error":违反规则时报错
  • { maxLeftJoin: 1 }:最大允许1次LEFT JOIN

SonarQube整合

导出ESLint报告

在项目构建阶段导出ESLint检查报告:

1
2
3
# 执行ESLint检查并导出JSON格式报告
# 使用 exit 0 确保即使有错误也不影响后续流程
eslint ./ --ext .ts,.js -f json -o eslint_report.json; exit 0

命令参数说明:

  • ./:检查当前目录
  • --ext .ts,.js:检查TypeScript和JavaScript文件
  • -f json:输出JSON格式
  • -o eslint_report.json:输出到指定文件
  • ; exit 0:确保命令总是成功退出

SonarQube导入报告

使用SonarScanner导入ESLint报告到SonarQube:

1
2
3
4
5
6
/usr/local/src/sonar-scanner/bin/sonar-scanner \
-Dsonar.projectKey=${CI_PROJECT_NAME} \
-Dsonar.sources=. \
-Dsonar.eslint.reportPaths=eslint_report.json \
-Dsonar.host.url=${SONAR_HOST} \
-Dsonar.login=${SONAR_LOGIN}

参数说明:

  • sonar.projectKey:项目唯一标识
  • sonar.sources:源代码路径
  • sonar.eslint.reportPaths:ESLint报告文件路径
  • sonar.host.url:SonarQube服务器地址
  • sonar.login:认证令牌

效果展示

导入成功后,可以在SonarQube平台查看检查结果:

展示内容包括:

  • 代码问题总览
  • 具体违规代码位置
  • 问题严重程度
  • 建议修复方案

最佳实践

规则设计原则

  1. 简单明确:规则逻辑清晰,易于理解
  2. 可配置:提供配置选项,适应不同场景
  3. 性能优化:避免过度复杂的AST遍历
  4. 错误提示:提供清晰的错误信息和修复建议

团队协作建议

  1. 统一配置:在项目根目录维护统一的ESLint配置
  2. 文档完善:为自定义规则编写详细的使用文档
  3. 持续优化:根据实际使用情况不断优化规则
  4. 版本管理:使用语义化版本管理规则插件

CI/CD集成

建议将ESLint检查和SonarQube分析集成到CI/CD流程中:

1
2
3
4
5
6
7
# GitLab CI示例
lint:
stage: test
script:
- npm run lint
- eslint ./ --ext .ts,.js -f json -o eslint_report.json; exit 0
- sonar-scanner -Dsonar.projectKey=$CI_PROJECT_NAME ...

总结

通过自定义ESLint规则并整合SonarQube,我们可以:

  1. 自动化检查:将编码规范自动化,减少人工审查成本
  2. 提前发现问题:在开发阶段就发现潜在问题
  3. 统一标准:团队使用统一的代码质量标准
  4. 可视化管理:通过SonarQube平台直观查看代码质量

这种方案特别适合中大型团队,能够有效提升代码质量和开发效率。