在日常的项目中,我们经常遇到需要计算的场景。但是JavaScript计算有很大的精度问题,而且在编码代码的时候会经常忽略精度,从而增加QA同学和我们自己的工作量。
为了解决精度问题,社区也为涌现很多优秀的库,在这里推荐一个小而美的库bigjs;我们不能被动的等到出了bug才来解决问题,为了准时下班,在日重的编码中,就需要考虑到精度的问题; 这时候就可以结合团队的一些规范来自定义一个eslint plugin了。
ESLint 是如何工作的?AST 的魔力
在我们开始深入创建 ESLint 规则之前,我们需要了解什么是 AST 以及为什么它们对开发人员如此有用。
AST或抽象语法树将代码表示为计算机可以读取和操作的树。
我们用高级的、人类可以理解的语言为计算机编写代码,例如 C、Java、JavaScript、Elixir、Python、Rust……但计算机不是人类:换句话说,它无法知道我们的意思写。我们需要一种方法让计算机从句法的角度解析您的代码,以理解这const
是一个变量声明,{}
有时标志着对象表达式的开始,有时标志着函数的开始......等等。这是通过 AST 完成的,这是一个必要的步骤.
在 ESLint
中,默认使用 esprima 来解析我们书写的 Javascript
语句,让其生成抽象语法树,然后去 拦截 检测是否符合我们规定的书写方式,最后让其展示报错、警告或正常通过。但目前我们都使用了typescript,所有需要将ts 代码转换为ast语法书,这时候就需要另外一个解析器 @typescript-eslint/parser
来看个简单的例子
可以看到和js代码解析出来的语法树相比,ts代码的抽象语法树带上类型信息。
创建plugin
从 eslint文档可以一个plugin主要是有rules 和 我们平常在eslintrc.js 定义的一些配置相关
module.exports = {
rules: {
"dollar-sign": {
create: function (context) {
// rule implementation ...
}
}
},
config: {
}
};
复制代码
@typescript-eslint/eslint-plugin index.ts
import rules from './rules';
import all from './configs/all';
import base from './configs/base';
import recommended from './configs/recommended';
import recommendedRequiringTypeChecking from './configs/recommended-requiring-type-checking';
import eslintRecommended from './configs/eslint-recommended';
export = {
rules,
configs: {
all,
base,
recommended,
'eslint-recommended': eslintRecommended,
'recommended-requiring-type-checking': recommendedRequiringTypeChecking,
},
};
复制代码
了解了插件的基本结构,我们就可以初始化项目了。
创建测试项目
为了调试和验证插件的功能,我们运行
npm i typescript -D
npx tsc --init`
复制代码
简单初始化一个typecript项目
{
"compilerOptions": {
"target": "ES6",
"module": "CommonJS",
"skipLibCheck": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"esModuleInterop": true,
"sourceMap": false,
"baseUrl": ".",
"checkJs": false,
"lib": ["esnext", "DOM"],
"paths": {
// path alias
"@/*": [
"src/*"
]
},
},
"include": [
"src/**/*.ts",
"eslint-rules/index.ts",
"eslint-rules/no-raw-float-calculation.ts"
],
"exclude": [
"node_modules",
]
}
复制代码
在新建一个eslint-rules
目录存放我们需要自定义的plugin,安装依赖
// eslint 相关
npm i eslint @typescript-eslint/eslint-plugin @typescript-eslint/experimental-utils @typescript-eslint/parser -D
// node 类型提示
npm i @types/node -D
// jest 相关
npm i @types/jest ts-jest jest -D
复制代码
根目录初始化 eslintrc.js
按照提示一步步来即可,注意需要选择在项目中使用typescript
。
module.exports = {
"env": {
"node": true,
"es2021": true,
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
project: ['./tsconfig.json'],
},
"plugins": [
"@typescript-eslint"
]
}
复制代码
所有的步骤完后,大概的目录结构如下:
编写rule
对于如何编写typescript eslint rule, 我们可以参考官方文档的custom rule章节,我们按照文档上的模板完成自定义插件的开发。
import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils';
type MessageIds = 'noRawNumberCalculation';
type Options = [];
// https://typescript-eslint.io/docs/development/custom-rules
const createRule = ESLintUtils.RuleCreator(
// rule 的说明文档链接
name => `https://example.com/rule/${name}`,
);
/**
* Options: rule 支持的参数
* MessageIds: 上传错误提示的id
*/
export default createRule<Options, MessageIds>({
name: 'no-raw-number-calculation',
meta: {
type: 'problem',
docs: {
description:
'避免原生js number类型的四则运算,可以使用bigJs',
recommended: 'error',
requiresTypeChecking: true,
},
messages: {
noRawNumberCalculation:
'避免原生js number类型的四则运算,可以使用bigJs',
},
schema: null // rule参数说明;这里做个简单的demo,不支持参数
},
defaultOptions: [],
create(context) {
const parserServices = ESLintUtils.getParserServices(context);
const checker = parserServices.program.getTypeChecker();
const getNodeType = (node: TSESTree.Expression | TSESTree.PrivateIdentifier) => {
// eslint ast 节点 和 TypeScript ts.Node 等效项的映射
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
// 拿到typescript 源代码的类型信息
// const a: number = 1 拿到a的类型信息
return checker.getTypeAtLocation(tsNode);
}
const checkBinaryExpression = (node: TSESTree.BinaryExpression) => {
const leftNodeType = getNodeType(node.left);
const rightNodetType = getNodeType(node.right);
// 操作符两边都是number类型
if (leftNodeType.isNumberLiteral() && rightNodetType.isNumberLiteral()) {
context.report({
node,
messageId: 'noRawNumberCalculation',
});
}
}
return {
// +-*/ 四则运算
"BinaryExpression[operator='+']": checkBinaryExpression,
"BinaryExpression[operator='-']": checkBinaryExpression,
"BinaryExpression[operator='*']": checkBinaryExpression,
"BinaryExpression[operator='/']": checkBinaryExpression
}
}
});
复制代码
到此,我们完成了一个简单的rule编写,它能检测到代码中number的四则运算;当然这只是一个demo,逻辑也很简单,还有很多运算符没考虑,如:+=
, ++
等等。 另外,只是单纯的判断为number类型有点简单粗暴,比如如果是整型的话,其实是可以直接进行原生的四则运算;一般是涉及到浮点类型
的计算,需要考虑精度运算。 但是 typscript只提供了number类型,我们可以自定义integer
和 float
类型。
declare type integer = number & { readonly __integer__?: unique symbol };
declare type float = number & { readonly __float__?: unique symbol };
declare type int = integer;
复制代码
在真实项目,可以结合团队的脚手架工具 将上面类型加到项目的模板声明文件中。这些暂且不说,回到demo中,我们需要新建一个 index.ts
导出编写的rule,
import noRawNumberCalculation from './no-raw-number-calculation';
export = {
rules: {
'no-raw-number-calculation': noRawNumberCalculation
}
}
复制代码
我们已经编写了一个简单rule,那怎么使用和调试呢?首先回到eslint-rules文件目录: 执行 npm init -y
。
{
"name": "eslint-plugin-demo", // eslint-plugin- 开头
"version": "1.0.0",
"description": "",
"main": "index.js", // index.ts 编译为 index.js
"directories": {
"test": "tests"
},
"scripts": {
"test": "echo 'test'"
},
"keywords": [],
"author": "",
"license": "ISC"
}
复制代码
测试使用rule
回到根目录后,执行 npm run build:rule
,然后在package.json
中添加 eslint-plugin-demo
本地依赖,再执行 npm install
。
{
"name": "custom_rule",
"version": "1.0.0",
"description": "a custom ts lint rule demo",
"scripts": {
"test": "jest", // 单测入口
"build:rule": "tsc", // 编译eslint-rule
"lint": "eslint ./src" // lint code
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/jest": "^27.4.1",
"@types/node": "^17.0.21",
"@typescript-eslint/eslint-plugin": "^5.15.0",
"@typescript-eslint/experimental-utils": "^5.15.0",
"@typescript-eslint/parser": "^5.15.0",
"eslint": "^8.11.0",
"eslint-plugin-demo": "file:./eslint-rules", // 本地依赖
"jest": "^27.5.1",
"ts-jest": "^27.1.3",
"typescript": "^4.6.2"
}
}
复制代码
在eslintrc.js
添加eslint-plugin-demo
"plugins": [
// ...
"demo"
],
"rules": {
'demo/no-number-float-calculation': 'error',
}
复制代码
现在我们可以看到编写rule的效果了:
再执行npm run lint
可以看到插件已经正常运行了。 哈哈,大功告成了!
测试用例
在eslint-rules文件下增加__test__
文件夹:
为什么要建一个file.ts
文件呢? 官方描述
:它是用于正常 TS 测试的空白测试文件。(其实也没很get到这个点,但是必须得加上不然测试用例无法正常跑起来)
测试用例的写法也跟普通eslint rule一样,考虑通过和不通过的场景就好。
import { ESLintUtils } from '@typescript-eslint/utils';
import rule from '../no-raw-number-calculation';
const ruleTester = new ESLintUtils.RuleTester({
parser: '@typescript-eslint/parser',
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json',
},
});
ruleTester.run('my-typed-rule', rule, {
valid: [
"const a = 1; const b = '2'; console.log(a + b);"
],
invalid: [
{
code: "const a = 2; const b = 4; console.log(a + b)",
errors: [{ messageId: 'noRawNumberCalculation' }]
}
],
});
复制代码
单测的tsconfig.json
{
"extends": "../../tsconfig.json", // 继承根目录下ts的配置
"include": [
"file.ts"
]
}
复制代码
最后我们看看jest.config.js
的配置,主要是要支持解析ts文件。
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
testTimeout: 60000,
testRegex: '/__tests__/.*.(jsx?|tsx?)$',
collectCoverage: false,
watchPathIgnorePatterns: ['/node_modules/', '/dist/', '/.git/'],
moduleFileExtensions: ['ts', 'js', 'json', 'node'],
testPathIgnorePatterns: ['/node_modules/'],
};
复制代码
最后在根目录下,运行npm test
测试用例也通过啦!到这里,我们编写一个自定义plugin的开发流程就基本走完了,后续就是发布npm,这些就不演示了,毕竟这只是一个学习demo。
感谢大家能坚持看完哈!