如何提取json中匹配上的字段_如何创建自定义 TSLint 规则

32778f9c18f0dc5e19e0acda0be3fa7c.png

本文将通过一个简单的例子介绍如何为 TSLint 编写自定义规则,在开始之前默认读者对 Typescript 有所了解。本文例子源码

前言

它是什么?

与 ESLint 一样属于程序静态分析工具(linter),不过因为 Typescript 与 Javascript 的抽象语法树(AST)存在差异,所以 TSLint 是仅对 Typescript 代码提供 静态分析 的工具。它提供对应的 CLI 工具,并且大多数 IDE 和相关构建工具默认提供集成和支持。

它能做什么?

  • 约束代码格式,提高可读合维护性 比如最大行数、末尾换行、import 排序
  • 约束不推荐的编码方式或者关键字的使用 比如禁止使用 var 关键字、禁止在同一作用域中重复声明变量
  • 针对 Typescript 特有的规则 比如禁止使用 any 类型、必须定义类型
  • 针对特定场景的高级规则集 比如适用于 RxJS 项目的 rxjs-tslint,rxjs-tslint-rules、适用于 Angular 项目 codelyzer 、为复杂类型提供 immutable 检查的 tslint-immutable

你必须知道的是

未来(2020年)TSLint 将会全部迁移到 ESLint,并且现在(2019/11) 已经停止向核心库添加新功能。目前受社区认可的方案是将 typescript-eslint 作为 ESLint 扩展为其提供 Typescript AST 的支持。

但这不会影响到你阅读本文,因为它们还会同时存在一段时间,并且就开发自定义规则而言除了 API 会不一样之外两者原理上应该没有任何区别。

你的第一个 TSLint Rule

在日常开发中,我们常常会在未完成或者由其他人完成的代码部分加上 TODO 注释,就像这样:

fun() {
  // TODO(HsuanLee): do something
}

为了避免有人忘记在 TODO 中加上受让人 ID,我们可以编写一个 TSLint Rule 来提醒他。

首先我们先初始化一个 npm 项目,名字就叫 my-first-rule 吧,然后安装必须的开发依赖(devDependencies)。

package.json

{
  "devDependencies": {
    "@types/node": "^12.12.14",
    "rimraf": "^3.0.0",
    "tslint": "^5.20.1",
    "tsutils": "^3.17.1",
    "typescript": "^3.7.3"
  }
}

接着创建下面这些文件:

tsconfig.json

TS Compiler 的配置

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "target": "es5",
    "module": "commonjs",
    "declaration": false,
    "noImplicitAny": false,
    "removeComments": true,
    "outDir": "./dist",
    "types": ["node"]
  },
  "files": ["src/index.ts"]
}

src/todoAssigneesRule.ts

文件名是有固定的命名方式,小骆峰规则名+Rule,这里对应的规则名便是 (todo-assignees)。

import * as Lint from 'tslint';
import * as ts from 'typescript';

export class Rule extends Lint.Rules.AbstractRule {
  apply(sourceFile: ts.SourceFile):  Lint.RuleFailure[] {
    return this.applyWithFunction(sourceFile, walk);
  }

}

function walk(ctx: Lint.WalkContext<void>) {
  console.log('  My first rule');
}

导出的规则类名必须为 Rule,并且是 Lint.Rules.AbstractRule 的实现,通常只需实现 apply 方法。

之后根据规则的复杂度抽成方法或者类,并使用 applyWithFunctionapplyWithWalker 调用并返回 Failure 列表。

src/index.ts 规则的入口文件

export { Rule as TodoAssigneesRule } from './todoAssigneesRule';
export const rulesDirectory = '.';

第一行导入规则是将 index.ts 作为入口文件方便编译,第二行则是使用编程方式调用 TSLint 时自定的规则集文件夹位置,导出一个名为 rulesDirectory,这里入口文件和规则集文件夹在同一层,所以就是export const rulesDirectory = '.'

package.json

最后将编译脚本添加到 package.jsonscripts 中。

{
  "scripts": {
    "build": "tsc -p tsconfig.json"
  }
}

现在我们完成了初始化的全部工作,试着在项目目录下运行 npm run build,如果一切正常的话你将会在项目目录下得到一个名为 dist 的文件夹,里面包含我们自定义规则的编译后的 js 文件(是的,TSLint 只能跑 JS )。

调试 TSLint Rule

现在我们来运行 dist 的规则,在项目下新建这些文件:

debug/index.ts

用于调试规则的文件

function fun() {
  // TODO: do something
  // TODO(someone): do something
}

debug/tslint.jsondist 中的规则集包含到配置中, 并启用对应的规则

{
  "rulesDirectory": "../dist",
  "rules": {
    "todo-assignees": true
  }
}

debug/tsconfig.json

然后是 TS 项目必备的配置,帮助 Lint 确定分析的项目。

{
  "compilerOptions": {
    "baseUrl": ".",
    "target": "es5"
  },
  "files": ["index.ts"]
}

现在在命令中输入 tslint -p debug/tsconfig.json 不出意外的话你将得到下面的输出:

$ tslint -p debug/tsconfig.json
   My first rule

编写 Walker

Walker 与 Visit 、Stepper 在定义上是有区别的,感兴趣的同学可以看看 The Roles of Variables 中的介绍。

在 Walker 方法中会有个 WalkContext 类型的参数,它向我们提供了当前文件的上下文信息以及对当前上下文的操作方法。

这里我们重点关注上下文的 sourceFile 属性,以及 addFailure 方法:

  • sourceFile 包含原文件元数据和的 AST 信息
  • addFailure 向当前文件添加错误信息的方法

除了 addFailure 之外还有 addFailureAtaddFailureAtNode 方法,除了确定错误的位置方式不同之外,没有任何区别。

先来看最终的 Walker 方法:

interface TodoCommentRange {
  comment: string;
  start: number;
  end: number;
}

function walk(ctx: Lint.WalkContext<void>) {
  // 未分配的列表
  const unassignedList: TodoCommentRange[] = [];

  for (const node of ctx.sourceFile.statements) {

    // 遍历每个 node 中的注释
    tsutils.forEachComment(node, (fullText: string, range: ts.CommentRange) => {

      // 提取注释文本
      const comment = fullText.slice(range.pos, range.end);
      const regexp = /b(TODO)(?!(.+))b.*/gi;

      let match;
      // 匹配所有 To-Do 并且为分配受让人的注释,并添加到 `unassignedList` 数组中
      while ((match = regexp.exec(comment)) !== null) {
        unassignedList.push({
          comment,
          start: range.pos + match.index,
          end: range.pos + regexp.lastIndex
        });
      }
    })
  }

  unassignedList.forEach(item => {
    // 添加到 Failure 列表
    ctx.addFailure(item.start, item.end, 'This To-Do is not assigned')
  })
}

代码的主要逻辑是找出文件中没有分配受让人的 TODO 注释部分,并且在这些地方添加错误提示,这里我们重点关心如何找出,即如何遍历 AST。

// 遍历每个 node 中的注释
tsutils.forEachComment(node, (fullText: string, range: ts.CommentRange) => {...})

可以看到我们这里使用一个名为 tsutils 的工具库来找到文件中的注释部分,它提供了许多 Typescript AST 的判断和遍历方法。 当然你也可以手动遍历 AST,每个 Node 都具有一个 kind 属性对定它的类型,在很多复杂的场景里你应该会用到 https://astexplorer.net, 这样的 AST 浏览工具来帮助你实现代码。

// 提取注释文本
const comment = fullText.slice(range.pos, range.end);

接着我们在回调函数中得到了完整的文本(fullText: string)以及注释部分的位置信息(range: ts.CommentRange),并利用它们将其注释部分的文本提取出来。

const regexp = /b(TODO)(?!(.+))b.*/gi;

let match;
// 匹配所有 To-Do 并且未分配受让人的注释,并添加到 `unassignedList` 数组中
while ((match = regexp.exec(comment)) !== null) {
    unassignedList.push({
      comment,
      start: range.pos + match.index,
      end: range.pos + regexp.lastIndex
    });
}

然后我们尝试匹配注释中所有没分配受让人的 TODO 部分,并将其位置信息保存在 unassignedList 数组中,这里需要注意一下因为 match 是基于提取后的文本计算的位置,我们需要加上 range.pos 使其对应原始文件的位置。

unassignedList.forEach(item => {
    // 添加到 Failure 列表
    ctx.addFailure(item.start, item.end, 'This To-Do is not assigned')
})

最后我们将它们(未指派的注释)添加到错误提示里,这样当你运行 tslint 或者在支持的 IDE 中时将会在对应位置提示错误信息。

现在试着运行 npm run build 编译规则,然后运行 TSLint 你将会看下这样的输出:

$ tslint -p debug/tsconfig.json

ERROR: debug/index.ts:2:6 - This To-Do is not assigned

添加规则参数

我们还可以为规则添加参数,使用时可以在 tslint.json 配置参数。例如接下来我们希望可以配置受让人列表,如果发现未知的受让人也同样提示错误。

首先在 apply 方法中调用实例的 getOptions 方法取到规则参数,在传给 walk 方法。

interface RuleOption {
  assignableList: string[]
}

export class Rule extends Lint.Rules.AbstractRule {

  apply(sourceFile: ts.SourceFile):  Lint.RuleFailure[] {
    // 获取参数
    const { ruleArguments } = this.getOptions();
    const assignableList = Array.isArray(ruleArguments[0]) ? ruleArguments[0] : [];
    return this.applyWithFunction(sourceFile, walk, { assignableList });
  }

}

接下来修改 walk 方法,在上下文对象(ctx)的 options 属性中取到参数,并且修改我们的判断条件,就像下面这样:

function walk(ctx: Lint.WalkContext<RuleOption>) {
  const unassignedList: TodoCommentRange[] = [];
  const assignableList = ctx.options.assignableList;

  for (const node of ctx.sourceFile.statements) {

    // 遍历每个 node 中的注释
    tsutils.forEachComment(node, (fullText: string, range: ts.CommentRange) => {

      // 提取注释文本
      const comment = fullText.slice(range.pos, range.end);
      const regexp = /b(?:TODO)(?:((.+)))?.+b.*/gi;

      let match;
      // 匹配所有 to-do 并且未分配受让人的注释,并添加到 `unassignedList` 数组中
      while ((match = regexp.exec(comment)) !== null) {
        const assignedID = match[1];
        const nonassignable = assignableList.length && assignableList.indexOf(assignedID) === -1;

        // 如果没有指定 ID,或者不在可分配列表中
        if (!assignedID || nonassignable) {
          unassignedList.push({
            comment,
            start: range.pos + match.index,
            end: range.pos + regexp.lastIndex
          });
        }
      }
    })
  }

  unassignedList.forEach(item => {
    // 添加到 Failure 列表
    ctx.addFailure(item.start, item.end, 'This To-Do assignee incorrect')
  })
}

现在向我们用于调试的 tslint.json 中添加规则参数:

debug/tslint.json

{
  "rulesDirectory": "../dist",
  "rules": {
    "todo-assignees": [true, ["hsuanxyz"]]
  }
}

以及测试文件:

function fun() {
  // TODO: do something
  // TODO(hsuanxyz): do something
  // TODO(someone): do something
}

并重新编译再运行 tslint,将会看到这样的输出:

$ tslint -p debug/tsconfig.json

ERROR: debug/index.ts:2:6 - This To-Do assignee incorrect
ERROR: debug/index.ts:4:6 - This To-Do assignee incorrect

从上面的结果中可以看到,未分配受让人以及受让人错误的位置都能正确提示了。

自动修复

除了提示错误之外,TSLint 还有一大作用就是自动修复错误的代码,你可以使用下面 Lint.Replacement 中的静态方法对原文件进行增删改的操作:

class Replacement {
  // 按节点替换
  static replaceNode(node: ts.Node, text: string, sourceFile?: ts.SourceFile): Replacement;
  // 按位置替换
  static replaceFromTo(start: number, end: number, text: string): Replacement;
  // 按起始位置和长度删除
  static deleteText(start: number, length: number): Replacement;
  // 按起始位置和结束位置删除
  static deleteFromTo(start: number, end: number): Replacement;
  // 在指定位置后添加
  static appendText(start: number, text: string): Replacement;
}

通常不需要调用 Replacement 上的其他方法,因为创建 Failure 对象的方法都提供了一个对应 Replacement 类型参数,将会在你添加 --fix 时自动执行,或者由支持的 IDE 执行。

例如我希望在执行修复(fix)操作时删除不符合条件的注释时,就可以这样:

unassignedList.forEach(item => {
  // 删除对应注释
  const fix = Lint.Replacement.deleteFromTo(item.start, item.end);
  // 将错误消息和 Fix 添加到 Failure 列表
  ctx.addFailure(item.start, item.end, 'This To-Do assignee incorrect')
})

跟随项目的规则

有些时候我们可能对某个项目编写特定的规则,在其他地方派不上用场。这时我们就希望这条规则能够跟随项目一起维护,而不是单独发布。

你当然可以像上面将的一样在项目中添加一个规则项目,你也可以使用我在 github.com/angular/components 中学到的一个小技巧,可以直接让 TSLint 运行 TS 代码,而不用每次都编译。

首先在你的项目中创建一个 rules 文件夹用于存放文件,并且安装 ts-node 依赖,然后在添加下面这个非常关键的规则;

rules/tsLoaderRule.js

const path = require('path');
const Lint = require('tslint');

// Custom rule that registers all of the custom rules, written in TypeScript, with ts-node.
// This is necessary, because `tslint` and IDEs won't execute any rules that aren't in a .js file.
require('ts-node').register({
  project: path.join(__dirname, './tsconfig.json')
});

// Add a noop rule so tslint doesn't complain.
exports.Rule = class Rule extends Lint.Rules.AbstractRule {
  apply() {}
};

以及对应的 rules/tsconfig.json 配置:

{
  "compilerOptions": {
    "lib": ["es2015"],
    "module": "commonjs",
    "moduleResolution": "node",
    "outDir": ".",
    "target": "es5",
    "types": ["node"],
    "baseUrl": "."
  }
}

让我们看看这条规则做了什么... 它什么都没做(几乎),是条空的规则!

在定义 Rule 上面我们看见它使用编程式(Programmatic)的方式调用了 ts-node,并且从 tsconfig.json 可以看到它将会包含这个文件夹下的说有 *.ts 文件。

这意味着只要我运行这条规则,就会编译和它一块儿的 TS 文件,那么现在我们只需要在项目的 tslint.json 中加这条规则,并确保在第一条:

{
  "rulesDirectory": [
  "./rules/"
  ],
  "rules": {
    "ts-loader": true
  }
}

然后在 rules 文件夹中使用 TS 编写你想要的任意规则,然后添加到 tslint.json,它们将会被自动编译并运行,而且不留痕迹 。

最后

我们知道了 TSLint 的作用以及它未来的规划,还创建了一条简单的自定义规则,它包含了规则的定义、Typescript AST 的遍历、添加错误消息以及添加自动修复的内容。另外我们还了解了一种跟随项目的规则创建方式,不需要单独维护也不需要手动编译。

另外 NG-ZORRO 会在 v9 的版本中弃用一级入口,并且在 v10 版本中移除,用户需要对代码做如下修改:

// 修改前
import {
  NzAutocompleteModule,
  NzButtonModule,
  NzCardModule,
  NzTableModule,
  NzToolTipModule
} from 'ng-zorro-antd';

// 修改后
import { NzAutocompleteModule } from 'ng-zorro-antd/auto-complete';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzCardModule } from 'ng-zorro-antd/card';
import { NzTableModule } from 'ng-zorro-antd/table';
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';

所以我们发布了 NG-ZORRO/nz-tslint-rules 规则集, 其中的 nz-secondary-entry-imports 规则将自动完成这一工作。 希望可以帮助到你,Merry Christmas!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值