第六章 - 编写 TypeScript 代码检查工具
从零开始学习如何编写自定义 TypeScript 代码检查工具
本章将运用之前学到的知识,教你如何创建自定义 TypeScript 代码检查工具!掌握这些内容后,你就能为自己的代码库编写检查工具,不再依赖 ESLint 等第三方工具。
代码检查工具的核心功能是:根据项目配置的规则进行检查,并通过错误或警告的形式反馈问题。高级的检查工具还能自动修复它发现的简单问题。
检查规则
我们的检查工具目前只配置了一个规则:"必须使用 isNaN() 来检查 NaN,禁止直接比较"。在 JavaScript 和 TypeScript 中,NaN 表示"非数字",可能出现在用户输入或数学计算中。我们经常需要检查变量是否为 NaN,如果是就可以执行其他逻辑。
这个工具会找出错误的 NaN 检查写法,比如 if (myNumber == NaN) {},并提示错误。它还会建议改用 isNaN(myNumber),甚至能自动修复。
选择
NaN作为例子是因为它容易引发困惑。很多开发者不知道直接比较(如myNumber === NaN)可能产生 bug,因为NaN永远不等于自身,这种比较总会返回false。使用isNaN(...)才能得到可靠结果。
说明:我们并非反对使用 ESLint。ESLint 是非常强大的工具,有完善的自定义规则文档。但为了学习目的,我们要从头实现一个简化版。
准备工作
我们将复用第四章的代码结构:
- 用
index.mjs编写检查逻辑 - 用
fun.ts作为待检查的代码 运行npm run compile时,控制台会显示检查出的错误。
在 fun.ts 中粘贴以下测试代码:
export class Math {
add(first: number, second: number): number {
if (first === NaN || second === NaN) {
return NaN
}
return first + second
}
}
const myNumber = 8
const isMyNumberNaN = myNumber == NaN
const arrowIsNaN = () => myNumber === NaN
function FnIsNaN() {
function InnerNaN() {
return myNumber == NaN
}
return InnerNaN()
}
这个例子很好,它展示了 NaN 检查可能出现在:
- 类方法中
- 变量声明里
- 箭头函数内
- 普通函数及其嵌套函数中
同时包含了
==和===两种比较方式,以及if条件语句。这些情况都需要处理。
实现检查工具
打开 index.mjs 文件,从以下基础代码开始:
import ts from "typescript"
const program = ts.createProgram(["fun.ts"], {
module: ts.ModuleKind.ESNext
})
for (const rootFileName of program.getRootFileNames()) {
const sourceFile = program.getSourceFile(rootFileName)
if (sourceFile && !sourceFile.isDeclarationFile) {
try {
lintSourceFile(sourceFile)
} catch (node) {
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
console.error(
`${sourceFile.fileName} (${line + 1},${character + 1}): 检查错误:禁止直接使用 NaN。请改用 isNaN 方法。`
)
}
}
}
/**
* @param {ts.SourceFile} sourceFile
*/
function lintSourceFile(sourceFile) {
// 检查逻辑将放在这里
}
这段代码会遍历项目文件,调用 lintSourceFile 进行检查。目前只检查 fun.ts 文件。
我们跳过了 .d.ts 类型声明文件,因为它们不包含运行时代码。错误处理会捕获异常并显示位置信息。
在 lintSourceFile 函数开头添加文件日志:
console.log("正在检查文件:", sourceFile.fileName)
然后添加遍历子节点的逻辑:
sourceFile.forEachChild(lintNode)
对每个子节点,我们会调用 lintNode 函数根据节点类型分发处理:
/**
* @param {ts.Node} node
*/
function lintNode(node) {
switch (node.kind) {
case ts.SyntaxKind.VariableStatement:
return lintVariableStatement(node)
case ts.SyntaxKind.VariableDeclarationList:
return lintVariableDeclarationList(node)
case ts.SyntaxKind.VariableDeclaration:
return lintVariableDeclaration(node)
case ts.SyntaxKind.FunctionDeclaration:
return lintFunctionDeclaration(node)
case ts.SyntaxKind.ClassDeclaration:
return lintClassDeclaration(node)
case ts.SyntaxKind.MethodDeclaration:
return lintMethodDeclaration(node)
case ts.SyntaxKind.Block:
return lintBlock(node)
case ts.SyntaxKind.Constructor:
return lintConstructor(node)
case ts.SyntaxKind.IfStatement:
return lintIfStatement(node)
case ts.SyntaxKind.ReturnStatement:
return lintReturnStatement(node)
case ts.SyntaxKind.BinaryExpression:
return lintBinaryExpression(node)
case ts.SyntaxKind.ArrowFunction:
return lintArrowFunction(node)
}
}
接下来实现各个节点类型的处理函数:
/** 处理 if 语句 */
function lintIfStatement(node) {
lintNode(node.expression)
lintBlock(node.thenStatement)
if (node.elseStatement) {
lintBlock(node.elseStatement)
}
}
/** 处理 return 语句 */
function lintReturnStatement(node) {
if (node.expression) {
lintNode(node.expression)
}
}
/** 处理变量声明语句 */
function lintVariableStatement(node) {
lintVariableDeclarationList(node.declarationList)
}
/** 处理变量声明列表 */
function lintVariableDeclarationList(node) {
node.declarations.forEach(lintVariableDeclaration)
}
/** 处理单个变量声明 */
function lintVariableDeclaration(node) {
if (node.initializer) {
lintNode(node.initializer)
}
}
/** 处理函数声明 */
function lintFunctionDeclaration(node) {
if (node.body) {
lintBlock(node.body)
}
}
/** 处理箭头函数 */
function lintArrowFunction(node) {
if (node.body) {
lintNode(node.body)
}
}
/** 处理类声明 */
function lintClassDeclaration(node) {
node.forEachChild(lintNode)
}
/** 处理方法声明 */
function lintMethodDeclaration(node) {
if (node.body) {
lintBlock(node.body)
}
}
/** 处理代码块 */
function lintBlock(node) {
node.forEachChild(lintNode)
}
/** 处理构造函数 */
function lintConstructor(node) {
if (node.body) {
lintBlock(node.body)
}
}
/** 处理二元表达式 - 核心逻辑 */
function lintBinaryExpression(node) {
throwIfNaN(node.left)
throwIfNaN(node.right)
lintNode(node.left)
lintNode(node.right)
}
/** 检查是否为 NaN 标识符 */
function throwIfNaN(node) {
if (ts.isIdentifier(node) && node.text === "NaN") {
throw node
}
}
核心逻辑在 lintBinaryExpression 和 throwIfNaN:
- 遍历所有二元表达式
- 检查左右节点是否为
NaN标识符 - 如果发现违规就抛出异常
运行后会看到错误信息:
fun.ts (3,19): 检查错误:禁止直接使用 NaN。请改用 isNaN 方法。
这表示在第 3 行第 19 列发现了问题。恭喜!你已成功用 TypeScript 编译器 API 实现了第一个代码检查工具!
改进空间
当前实现有几个可以优化的地方:
- 应收集所有错误后再报告,而不是遇到第一个错误就停止
- 需要覆盖更多可能包含
NaN检查的场景 - 可以增加自动修复功能,将错误写法改为
isNaN()
这些正是 ESLint 等工具的优势所在。它们提供了完整的处理流程,能深度集成到编辑器中。
本章总结
我们完成了:
- 制定代码检查规则
- 遍历 AST 找出违规代码
- 在控制台报告问题
下一章我们将探索如何自动生成代码!