乐趣区

关于ast:从AST原理到ESlint实践

AST(形象语法树)

为什么要谈 AST?

如果你查看目前任何支流的我的项目中的 devDependencies,会发现前些年的成千上万的插件诞生。咱们演绎一下有:ES6 转译、代码压缩、css预处理器、eslintprettier等。这些模块很多都不会用到生产环境,然而它们在开发环境中起到很重要的作用,这些工具的诞生都是建设在了 AST 这个伟人的肩膀上。

什么是 AST?

It is a hierarchical program representation that presents source code structure according to the grammar of a programming language, each AST node corresponds to an item of a source code.

形象语法树(abstract syntax code,AST)是源代码的形象语法结构的树状示意,树上的每个节点都示意源代码中的一种构造,这所以说是形象的,是因为形象语法树并不会示意出实在语法呈现的每一个细节,比如说,嵌套括号被隐含在树的构造中,并没有以节点的模式出现。形象语法树并不依赖于源语言的语法,也就是说语法分析阶段所采纳的上下文无文文法,因为在写文法时,常常会对文法进行等价的转换(打消左递归,回溯,二义性等),这样会给文法剖析引入一些多余的成分,对后续阶段造成不利影响,甚至会使合个阶段变得凌乱。因些,很多编译器常常要独立地结构语法分析树,为前端,后端建设一个清晰的接口。

从纯文本转换成树形构造的数据,也就是AST,每个条目和树中的节点一一对应。

AST 的流程

此局部将让你理解到从源代码到词法剖析生成 tokens 再到语法分析生成 AST 的整个流程。

从源代码中怎么失去 AST 呢?当下的编译器帮着做了这件事,那编译器是怎么做的呢?

一款编译器的编译流程(将高级语言转译成二进制位)是很简单的,但咱们只须要关注词法剖析和语法分析,这两步是从代码生成 AST 的关键所在。

第一步,词法分析器,也称为扫描器 ,它会先对整个代码进行扫描,当它遇到空格、操作符或特殊符号时,它决定一个单词实现,将辨认出的一个个单词、操作符、符号等以对象的模式({type, value, range, loc})记录在tokens 数组中,正文会另外寄存在一个 comments 数组中。

比方 var a = 1;@typescript-eslint/parser 解析器生成的 tokens 如下:

tokens: [
    {
      "type": "Keyword",
      "value": "var",
      "range": [112, 115],
      "loc": {
        "start": {
          "line": 11,
          "column": 0
        },
        "end": {
          "line": 11,
          "column": 3
        }
      }
    },
    {
      "type": "Identifier",
      "value": "a",
      "range": [116, 117],
      "loc": {
        "start": {
          "line": 11,
          "column": 4
        },
        "end": {
          "line": 11,
          "column": 5
        }
      }
    },
    {
      "type": "Punctuator",
      "value": "=",
      "range": [118, 119],
      "loc": {
        "start": {
          "line": 11,
          "column": 6
        },
        "end": {
          "line": 11,
          "column": 7
        }
      }
    },
    {
      "type": "Numeric",
      "value": "1",
      "range": [120, 121],
      "loc": {
        "start": {
          "line": 11,
          "column": 8
        },
        "end": {
          "line": 11,
          "column": 9
        }
      }
    },
    {
      "type": "Punctuator",
      "value": ";",
      "range": [121, 122],
      "loc": {
        "start": {
          "line": 11,
          "column": 9
        },
        "end": {
          "line": 11,
          "column": 10
        }
      }
    }
]

第二步,语法分析器,也称为解析器 ,将词法剖析失去的tokens 数组转换为树形构造示意,验证语言语法并抛出语法错误(如果产生这种状况)

var a = 1;tokens 数组转换为树形构造如下所示:

{
  type: 'Program',
  body: [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "a",
            "range": [
              116,
              117
            ],
            "loc": {
              "start": {
                "line": 11,
                "column": 4
              },
              "end": {
                "line": 11,
                "column": 5
              }
            }
          },
          "init": {
            "type": "Literal",
            "value": 1,
            "raw": "1",
            "range": [
              120,
              121
            ],
            "loc": {
              "start": {
                "line": 11,
                "column": 8
              },
              "end": {
                "line": 11,
                "column": 9
              }
            }
          },
          "range": [
            116,
            121
          ],
          "loc": {
            "start": {
              "line": 11,
              "column": 4
            },
            "end": {
              "line": 11,
              "column": 9
            }
          }
        }
      ],
      "kind": "var",
      "range": [
        112,
        122
      ],
      "loc": {
        "start": {
          "line": 11,
          "column": 0
        },
        "end": {
          "line": 11,
          "column": 10
        }
      }
    }
  ]
}

在生成树时,解析器会剔除掉一些不必要的标记(例如冗余括号),因而创立的“形象语法树”不是 100% 与源代码匹配,但足以让咱们晓得如何解决它。另一方面,齐全笼罩所有代码构造的解析器生成的树称为“具体语法树”

编译器拓展

想理解更多对于编译器的常识?the-super-tiny-compiler,这是一个用 JavaScript 编写的编译器。大略 200 行代码,其背地的想法是将Lisp 编译成 C 语言,简直每行都有正文。

LangSandbox,一个更好的我的项目,它阐明了如何发明一门编程语言。当然,设计编程语言这样的书市面上也一坨坨。所以,这我的项目更加深刻,与 the-super-tiny-compiler 的我的项目将 Lisp 转为 C 语言不同,这个我的项目你能够写一个你本人的语言,并且将它编译成 C 语言或者机器语言,最初运行它。

能间接用三方库来生成 AST 吗?当然能够!有一堆三方库能够用。你能够拜访 astexplorer,而后挑你喜爱的库。astexplorer是一个很棒的网站,你能够在线玩转 AST,而且除了JavaScript 之外,它还蕴含许多其余语言AST

我想特别强调其中的一个,在我看来它是十分好的一个,babylon

它在 Babel 中应用,兴许这也是它受欢迎的起因。因为它是由 Babel我的项目反对的,所以它会始终与最新的 JS 个性放弃同步,能够大胆地应用。另外,它的 API 也十分的简略,容易应用。

OK,当初您晓得如何将代码生成 AST,让咱们持续探讨事实中的用例。

我想议论的第一个用例是代码转译,当然是 Babel

Babel is not a‘tool for having ES6 support’. Well, it is, but it is far not only what it is about.

BabelES6/7/8 个性的反对有很多关联,这就是咱们常常应用它的起因。但它仅仅是一组插件,咱们还能够将它用于代码压缩、React 相干的语法转换(例如 JSX)、Flow 插件等。

Babel 是一个 JavaScript编译器,它的编译有三个阶段:解析(parsing)、转译(transforming)、生成(generation)。你给 Babel一些 JavaScript 代码,它批改代码并生成新的代码,它是如何批改代码?没错!它构建 AST,遍历它,依据 babel-plugin 批改它,而后从批改后的AST 生成新代码。

让咱们在一个简略的代码示例中看到这一点。

正如我之前提到的,Babel 应用 Babylon,所以,咱们首先解析代码生成 AST,而后遍历 AST 并反转所有变量名称,最初生成代码。正如咱们看到的,第一步(解析)和第三步(代码生成)阶段看起来很常见,每次都会做的。所以,Babel 接管了这两步,咱们真正感兴趣的是 AST 转换(Babel-plugin批改)。

当开发 Babel-plugin 时,你只须要形容节点 “visitors”,它会扭转你的AST。将它退出你的babel 插件列表中,设置你 webpackbabel-loader配置或者 .babelrc 中的 plugins 即可

如果你想理解更多对于如何创立 babel-plugin,你能够查看 Babel-handbook。

AST 在 ESLint 中的使用

在正式写 ESLint 插件前,你须要理解下 ESLint 的工作原理。其中 ESLint 应用办法大家应该都比拟相熟,这里不做解说,不理解的能够点击官网文档 如何在我的项目中配置 ESLint。

在我的项目开发中,不同开发者书写的源码是各不相同的,那么 ESLint 如何去剖析每个人写的源码呢?

没错,就是 AST (Abstract Syntax Tree(形象语法树)),再祭上那张看了几百遍的图。

ESLint 中,默认应用 esprima 来解析 Javascript,生成形象语法树,而后去 拦挡 检测是否合乎咱们规定的书写形式,最初让其展现报错、正告或失常通过。ESLint 的外围就是规定(rules),而定义规定的外围就是利用 AST 来做校验。每条规定互相独立,能够设置禁用off、正告warn⚠️和报错error❌,当然还有失常通过不必给任何提醒。

手把手教你写 Eslint 插件

指标 & 波及知识点

本文 ESLint 插件旨在 校验代码正文是否写了正文

  • 每个申明式函数、函数表达式都须要正文;
  • 每个 interface 头部和字段都须要正文;
  • 每个 enum 头部和字段都须要正文;
  • 每个 type 头部都须要正文;
  • ……

知识点

  • AST 形象语法树
  • ESLint
  • Mocha单元测试
  • Npm 公布

脚手架搭建我的项目

这里咱们利用 yeomangenerator-eslint 来构建插件的脚手架代码,装置:

npm install -g yo generator-eslint

本地新建文件夹eslint-plugin-pony-comments

mkdir eslint-plugin-pony-comments
cd eslint-plugin-pony-comments

命令行初始化 ESLint 插件的我的项目构造:

yo eslint:plugin

上面进入命令行交互流程,流程完结后生成 ESLint 插件我的项目框架和文件

$ yo eslint:plugin
? What is your name? xxx // 作者
? What is the plugin ID? eslint-plugin-pony-comments // 插件名称
? Type a short description of this plugin: 查看代码正文 // 插件形容
? Does this plugin contain custom ESLint rules? (Y/n) Y
? Does this plugin contain custom ESLint rules? Yes // 这个插件是否蕴含自定义规定
? Does this plugin contain one or more processors? (y/N) N
? Does this plugin contain one or more processors? No // 该插件是否须要处理器
   create package.json
   create lib\index.js
   create README.md

此时文件的目录构造为:

.
├── README.md
├── lib
│   ├── processors // 处理器,抉择不须要时没有该目录
│   ├── rules // 自定义规定目录
│   └── index.js // 导出规则、处理器以及配置
├── package.json
└── tests
    ├── processors // 处理器,抉择不须要时没有该目录
    └── lib
        └── rules // 编写规定的单元测试用例

装置依赖:

npm install // 或者 yarn

至此,环境搭建结束。

创立规定

以实现”每个 interface 头部和字段都须要正文“为例创立规定,终端执行:

yo eslint:rule // 生成默认 eslint rule 模版文件

上面进入命令行交互流程:

$ yo eslint:rule
? What is your name? xxx // 作者
? Where will this rule be published? ESLint Plugin // 抉择生成插件模板
? What is the rule ID? no-interface-comments // 规定名称
? Type a short description of this rule: 校验 interface 正文 // 规定形容
? Type a short example of the code that will fail: 
   create docs\rules\no-interface-comments.md     
   create lib\rules\no-interface-comments.js      
   create tests\lib\rules\no-interface-comments.js

此时我的项目构造为:

.
├── README.md
├── docs // 阐明文档
│   └── rules
│       └── no-interface-comments.md
├── lib // eslint 规定开发
│   ├── index.js
│   └── rules // 此目录下能够构建多个规定,本文只拿一个规定来解说
│       └── no-interface-comments.js
├── package.json
└── tests // 单元测试
    └── lib
        └── rules
            └── no-interface-comments.js

ESLint 中的每个规定都有三个以其标识符命名的文件(例如,no-interface-comments)。

  • lib/rules 目录中:一个源文件(例如,no-interface-comments.js
  • tests/lib/rules 目录中:一个测试文件(例如,no-interface-comments.js
  • docs/rules 目录中:一个 Markdown 文档文件(例如,no-interface-comments

在正式进入开发规定之前先来看看生成的规定模板 no-interface-comments.js

/**
 * @fileoverview no-interface-comments
 * @author xxx
 */
"use strict";

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

module.exports = {
    meta: {
        docs: {description: "no console.time()",
            category: "Fill me in",
            recommended: false
        },
        fixable: null,  // or "code" or "whitespace"
        schema: [// fill in your schema]
    },

    create: function(context) {

        // variables should be defined here

        //----------------------------------------------------------------------
        // Helpers
        //----------------------------------------------------------------------

        // any helper functions should go here or else delete this section

        //----------------------------------------------------------------------
        // Public
        //----------------------------------------------------------------------

        return {// give me methods};
    }
};

这个文件给出了书写规定的模版,一个规定对应一个可导出的 node 模块,它由 metacreate 两局部组成。

  • meta:代表了这条规定的元数据,如其类别,文档,可接管的参数的 schema 等等。
  • create:如果说 meta 表白了咱们想做什么,那么 create 则用表白了这条 rule 具体会怎么剖析代码;

create 返回一个对象,其中最常见的 键名 AST形象语法树中的 选择器,在该选择器中,咱们能够获取对应选中的内容,随后咱们能够针对选中的内容作肯定的判断,看是否满足咱们的规定。如果不满足,可用 context.report 抛出问题,ESLint 会利用咱们的配置对抛出的内容做不同的展现。详情参考:context.report

在编写 no-interface-comments 规定之前,咱们在 AST Explorer 看看 interface 代码解析成 AST 的构造是怎么样的?

依据下面 AST 构造,咱们创立两个选择器校验代码正文,TSInterfaceDeclaration选择器校验 interface 头部是否有正文,TSPropertySignature选择器校验字段是否有正文。遍历 AST 可能须要用到以下API,详情参考官网:

  • fixer.insertTextAfter(nodeOrToken, text) – 在给定的节点或标记之后插入文本
  • fixer.insertTextBefore(nodeOrToken, text) – 在给定的节点或标记之前插入文本
  • sourceCode.getAllComments() – 返回源代码中所有正文的数组
  • context.getSourceCode() – 获取源代码
/**
 * @fileoverview interface 定义类型正文校验
 * @author xxx
 */
'use strict';

const {
  docsUrl,
  getLastEle,
  getAllComments,
  judgeNodeType,
  getComments,
  genHeadComments,
  report,
  isTailLineComments,
  getNodeStartColumn,
  genLineComments,
} = require('../utils');

module.exports = {
  meta: {
    /**
     * 规定的类型
     * "problem" 意味着规定正在辨认将导致谬误或可能导致混同行为的代码。开发人员应将此视为优先解决的问题。* "suggestion" 意味着规定正在确定能够以更好的形式实现的事件,但如果不更改代码,则不会产生谬误。* "layout" 意味着规定次要关怀空格、分号、逗号和括号,程序的所有局部决定了代码的外观而不是它的执行形式。这些规定实用于 AST 中未指定的局部代码。*/
    type: 'layout',
    docs: {
      description: 'interface 定义类型正文校验', // 规定形容
      category: 'Fill me in',
      recommended: true, // 是配置文件中的 "extends": "eslint:recommended" 属性是否启用规定
      url: 'https://github.com/Revelation2019/eslint-plugin-pony-comments/tree/main/docs/rules/no-interface-comments.md', // 该规定对应在 github 上的文档介绍
    },
    fixable: 'whitespace',  // or "code" or "whitespace"
    schema: [// 指定选项,比方 'pony-comments/no-interface-comments: [2,'always', { leadingCommentType:'Block', propertyComments: { pos:'lead', commentsType:'Block'}}]'
      {'enum': ['always', 'never'],
      },
      {
        'type': 'object',
        'properties': {
          /** 
           * 是否须要头部正文
           * 'No':示意不须要头部正文
           * 'Line': 示意头部须要单行正文
           * 'Block':示意头部须要多行正文
           */
          'leadingCommentType': {'type': 'string',},
          /** 字段正文采纳单行还是多行正文 */
          'propertyComments': {
            'type': 'object',
            'properties': {
              'pos': {'type': 'string', // lead || tail 示意正文地位是行头还是行尾},
              'commentsType': {'type': 'string', // No || Line || Block 示意正文是单行还是多行,或者不须要正文},
            },
          },
        },
        'additionalProperties': false,
      },
    ],
  },

  create: function(context) {
    // 获取选项
    const options = context.options;
    const leadingCommentsType = options.length > 0 ? getLastEle(options).leadingCommentType : null;
    const propertyComments = options.length > 0 ? getLastEle(options).propertyComments : {};
    const {pos, commentsType} = propertyComments;
    /** 获取所有的正文节点 */
    const comments = getAllComments(context);
    // 无效的选项值
    const commentsTypeArr = ['No', 'Line', 'Block'];

    return {
      /** 校验 interface 定义头部正文 */
      'TSInterfaceDeclaration': (node) => {
        /** 不须要头部正文 */
        if (leadingCommentsType === 'No' || !commentsTypeArr.includes(leadingCommentsType)) return;
        const {id} = node;
        const {name} = id;
        // 判断 interface 的父节点是否是 export
        if (judgeNodeType(node, 'ExportNamedDeclaration')) {/** export interface XXX {} */
          const {leading} = getComments(context, node.parent);
          if (!leading.length) {
            // 没有头部正文,抛出断言
            report(context, node.parent, '导出的类型头部没有正文', genHeadComments(node.parent, name, leadingCommentsType));
          }
        } else {/** enum interface {} */
          const {leading} = getComments(context, node); // 获取节点头部和尾部正文
          if (!leading.length) {
            // 没有头部正文,抛出断言
            report(context, node, '类型头部没有正文', genHeadComments(node, name, leadingCommentsType));
          }
        }
      },
      /** 校验 interface 定义字段正文 */
      'TSPropertySignature': (node) => {if (commentsType === 'No' || !commentsTypeArr.includes(commentsType)) return;
        /** 防止 export const Main = (props: { name: string}) => {} */
        if (judgeNodeType(node, 'TSInterfaceBody')) {const { key} = node;
          const {name} = key;
          const {leading} = getComments(context, node); // 获取节点头部和尾部正文
          const errorMsg = '类型定义的字段没有正文';
          if (isTailLineComments(comments, node) || (leading.length &&  getNodeStartColumn(getLastEle(leading)) === getNodeStartColumn(node))) {
            /** 
             * 节点尾部已有正文 或者 头部有正文并且正文结尾与节点结尾列数雷同 
             * 这里判断节点开始地位 column 与正文结尾地位 column 是因为 getComments 获取到的头部正文可能是不是以后节点的,比方
             interface xxx { 
                 id: string; // id
                 name: string; // name
             } 
             leading 拿到的是 // id,但这个正文不是 name 字段的
             */
            return;
          }
          // 依据选项报出断言,并主动修复
          if (commentsType === 'Block' || (commentsType === 'Line' && pos === 'lead')) {
            // 主动增加行头多行正文
            report(context, node, errorMsg, genHeadComments(node, name, commentsType));
          } else {
            // 主动增加行尾单行正文
            report(context, node, errorMsg, genLineComments(node, name));
          }
        }
      },
    };
  },
};

主动修复函数:

/**
 * @description 在函数头部加上正文
 * @param {Object} node 以后节点
 * @param {String} text 正文内容
 * @returns
 */
const genHeadComments = (node, text, commentsType) => {if (!text) return null;
  const eol = require('os').EOL; // 获取换行符,window 是 CRLF,linux 是 LF
  let content = '';
  if (commentsType && commentsType.toLowerCase === 'line') {content = `// ${text}${eol}`;
  } else if (commentsType && commentsType.toLowerCase === 'block') {content = `/** ${text} */${eol}`;
  } else {content = `/** ${text} */${eol}`;
  }
  return (fixer) => {
    return fixer.insertTextBefore(
      node,
      content,
    );
  };
};
/**
 * @description 生成行尾单行正文
 * @param {Object} node 以后节点
 * @param {String} value 正文内容
 * @returns
 */
const genLineComments = (node, value) => {return (fixer) => {
    return fixer.insertTextAfter(
      node,
      `// ${value}`,
    );
  };
};

至此,no-interface-comments规定编写就根本实现了

插件中的配置

您能够通过在 configs 键下指定它们来将配置捆绑在插件中。当您不仅要提供代码款式,而且要提供一些反对它的自定义规定时,这会很有用。每个插件反对多种配置。请留神,无奈为给定插件指定默认配置,用户必须在其配置文件中指定何时应用。参考官网

// lib/index.js
module.exports = {
    configs: {
        recommended: {
            plugin: 'pony-comments',
            parserOptions: {
                sourceType: 'module',
                ecmaVersion: 2018,
            },
            rules: {'pony-comments/no-interface-comments': [2, 'always', { leadingCommentType: 'Block', propertyComments: { pos: 'tail', commentsType: 'Line'} }],
            }
        },
    }
};

插件规定将能够通过 extends 配置继承:

{"extends": ["plugin:pony-comments/recommended"]
}

留神:请留神,默认状况下配置不会启用任何插件规定,而是应视为独立配置。这意味着您必须在 plugins 数组中指定您的插件名称以及您要启用的任何规定,这些规定是插件的一部分。任何插件规定都必须以短或长插件名称作为前缀

创立处理器

处理器能够通知 ESLint 如何解决 JavaScript 以外的文件,比方从其余类型的文件中提取 JavaScript 代码,而后让 ESLintJavaScript 代码进行 lint,或者处理器能够出于某种目标在预处理中转换 JavaScript 代码。参考官网

// 在 lib/index.js 中导出自定义处理器,或者将其抽离
module.exports = {
    processors: {
        "markdown": {
            // takes text of the file and filename
            preprocess: function(text, filename) {
                // here, you can strip out any non-JS content
                // and split into multiple strings to lint

                return [ // return an array of code blocks to lint
                    {text: code1, filename: "0.js"},
                    {text: code2, filename: "1.js"},
                ];
            },

            // takes a Message[][] and filename
            postprocess: function(messages, filename) {
                // `messages` argument contains two-dimensional array of Message objects
                // where each top-level array item contains array of lint messages related
                // to the text that was returned in array from preprocess() method

                // you need to return a one-dimensional array of the messages you want to keep
                return [].concat(...messages);
            },

            supportsAutofix: true // (optional, defaults to false)
        }
    }
};

要在配置文件中指定处理器,请应用 processor 带有插件名称和处理器名称的连贯字符串的键(由斜杠)。例如,以下启用 pony-comments 插件提供的 markdown 处理器:

{"plugins": ["pony-comments"],
    "processor": "pony-comments/markdown"
}

要为特定类型的文件指定处理器,请应用 overrides 键和 processor 键的组合。例如,以下应用处理器 pony-comments/markdown 解决 *.md 文件。

{"plugins": ["pony-comments"],
    "overrides": [
        {"files": ["*.md"],
            "processor": "pony-comments/markdown"
        }
    ]
}

处理器可能会生成命名代码块,例如 0.js1.jsESLint 将这样的命名代码块作为原始文件的子文件解决。您能够 overridesconfig 局部为命名代码块指定其余配置。例如,以下 strict 代码禁用 .jsmarkdown 文件结尾的命名代码块的规定。

{"plugins": ["pony-comments"],
    "overrides": [
        {"files": ["*.md"],
            "processor": "pony-comments/markdown"
        },
        {"files": ["**/*.md/*.js"],
            "rules": {"strict": "off"}
        }
    ]
}

ESLint 查看命名代码块的文件门路,如果任何 overrides 条目与文件门路不匹配,则疏忽那些。肯定要加的overrides,如果你想皮棉比其余命名代码块的条目*.js

文件扩展名处理器

如果处理器名称以 结尾 .,则 ESLint 将处理器作为 文件扩展名 处理器来解决,并主动将处理器利用于文件类型。人们不须要在他们的配置文件中指定文件扩展名的处理器。例如:

module.exports = {
    processors: {
        // This processor will be applied to `*.md` files automatically.
        // Also, people can use this processor as "plugin-id/.md" explicitly.
        ".md": {preprocess(text, filename) {/* ... */},
            postprocess(messageLists, filename) {/* ... */}
        }
    }
}

编写单元测试

eslint.RuleTester是一个为 ESLint 规定编写测试的实用程序。RuleTester构造函数承受一个可选的对象参数,它能够用来指定测试用例的默认值(官网)。例如,如果能够指定用 @typescript-eslint/parser 解析你的测试用例:

const ruleTester = new RuleTester({parser: require.resolve('@typescript-eslint/parser') });

当须要解析 .tsx 文件时,就须要指定特定的解析器,比方 @typescript-eslint/parser,因为eslint 服务默认应用的解析器是 esprima,它不反对对typescriptreact

如果在执行测试用例时报如下谬误:

AssertionError [ERR_ASSERTION]: Parsers provided as strings to RuleTester must be absolute paths

这是因为解析器须要用绝对路径

/**
 * @fileoverview interface 定义类型正文校验
 * @author xxx
 */
'use strict';

const rule = require('../../../lib/rules/no-interface-comments');

const RuleTester = require('eslint').RuleTester;
const ruleTester = new RuleTester({parser: require.resolve('@typescript-eslint/parser'),
  parserOptions: {
    ecmaVersion: 2018,
    sourceType: 'module',
    ecmaFeatures: {jsx: true,},
    comment: true,
    useJSXTextNode: true,
  },
});
ruleTester.run('no-interface-comments', rule, {
  // 无效测试用例
  valid: [
    {
      code: `
        export const Main = (props: { name: string}) => {}
      `,
      options: ['always', { leadingCommentType: 'Block', propertyComments: { pos: 'lead', commentsType: 'Block'} }],
    },
    {
      code: `
        /** 类型 */
        export interface IType {
          id: string; // id
          name: string; // 姓名
          age: number; // 年龄
        }
      `,
      options: ['always', { leadingCommentType: 'Block', propertyComments: { pos: 'tail', commentsType: 'Line'} }],
    },
    {
      code: `
        /** 类型 */
        interface IType {
          /** id */
          id: string;
          /** 姓名 */
          name: string;
          /** 年龄 */
          age: number;
        }
      `,
      options: ['always', { leadingCommentType: 'Block', propertyComments: { pos: 'lead', commentsType: 'Block'} }],
    },
  ],
  // 有效测试用例
  invalid: [
    {
      code: `
        export interface IType {
          /** id */
          id: string;
          /** 姓名 */
          name: string;
          /** 年龄 */
          age: number;
        }
      `,
      errors: [{
        message: 'interface 头部必须加上正文',
        type: 'TSInterfaceDeclaration',
      }],
      options: ['always', { leadingCommentType: 'Block', propertyComments: { pos: 'lead', commentsType: 'Block'} }],
      output: `
        /** 类型 */
        export interface IType {
          /** id */
          id: string;
          /** 姓名 */
          name: string;
          /** 年龄 */
          age: number;
        }
      `,
    },
    {
      code: `
        /** 类型 */
        interface IType {
          id: string;
          name: string;
          age: number;
        }
      `,
      errors: [{
        message: 'interface 字段必须加上正文',
        type: 'TSPropertySignature',
      }],
      options: ['always', { leadingCommentType: 'Block', propertyComments: { pos: 'lead', commentsType: 'Block'} }],
      output: `
        /** 类型 */
        interface IType {
          /** id */
          id: string;
          /** 姓名 */
          name: string;
          /** 年龄 */
          age: number;
        }
      `,
    },
  ],
});

yarn test 执行测试用例,控制台输入:

课外常识:Lint 简史

Lint 是为了解决代码不谨严而导致各种问题的一种工具。比方 ===== 的混合应用会导致一些奇怪的问题。

JSLint 和 JSHint

2002 年,Douglas Crockford 开发了可能是第一款针对 JavaScript 的语法检测工具 —— JSLint,并于 2010 年开源。

JSLint 面市后,的确帮忙许多 JavaScript 开发者节俭了不少排查代码谬误的工夫。然而 JSLint 的问题也很显著—— 简直不可配置 ,所有的代码格调和规定都是内置好的;再加上 Douglas Crockford 推崇道系「爱用不必」的优良传统,不会向开发者斗争凋谢配置或者批改他感觉是对的规定。于是 Anton Kovalyov 吐槽:「JSLint 是让你的代码格调更像 Douglas Crockford 的而已」,并且在 2011 年 Fork 原我的项目开发了 JSHint。《Why I forked JSLint to JSHint》

JSHint 的特点就是 可配置,同时文档也绝对欠缺,而且对开发者敌对。很快大家就从 JSLint 转向了 JSHint

ESLint 的诞生

起初几年大家都将 JSHint 作为代码检测工具的首选,但转折点在 2013 年,Zakas 发现 JSHint 无奈满足本人制订规定需要,并且和 Anton 探讨后发现这基本不可能在JShint 上实现,同时 Zakas 还构想创造一个基于 ASTlint。于是 2013 年 6 月份,Zakas 公布了全新 lint 工具——ESLint。《Introducing ESLint》

ESLint 晚期源码:

var ast = esprima.parse(text, { loc: true, range: true}),
    walk = astw(ast);

walk(function(node) {api.emit(node.type, node);
});

return messages;

ESLint 的逆袭

ESLint 的呈现并没有撼动 JSHint 的霸主位置。因为前者是利用 AST 解决规定,用 Esprima 解析代码,执行速度要比只须要一步搞定的 JSHint 慢很多 ;其次过后曾经有许多编辑器对 JSHint 反对欠缺,生态足够弱小。真正让 ESLint 逆袭的是 ECMAScript 6 的呈现。

2015 年 6 月,ES2015 标准正式公布。然而公布后,市面上浏览器对最新规范的反对状况极其无限。如果想要提前体验最新规范的语法,就得靠 Babel 之类的工具将代码编译成 ES5 甚至更低的版本,同时一些实验性的个性也能靠 Babel 转换。但这时候的 JSHint 短期内无奈提供反对,而 ESLint 却只须要有适合的解析器就能持续去 lint 查看。Babel 团队就为 ESLint 开发了一款代替默认解析器的工具,也就是当初咱们所见到的 babel-eslint,它让 ESLint 成为率先反对 ES6 语法的 lint 工具。

也是在 2015 年,React 的利用越来越宽泛,诞生不久的 JSX 也更加风行。ESLint 自身也不反对 JSX 语法。然而因为可扩展性,eslint-plugin-react 的呈现让 ESLint 也能反对过后 React 特有的规定。

2016 年,JSCS 开发团队认为 ESLint JSCS 实现原理太过类似,而且须要解决的问题也都统一,最终抉择合并到 ESLint,并进行 JSCS 的保护。

以后市场上支流的 lint 工具以及趋势图:

从此 ESLint 一统江湖,成为代替 JSHint 的前端支流工具。

参考:

平庸前端码农之变质 — AST

【AST 篇】手把手教你写 Eslint 插件

配置 ESLint RuleTester 以应用 Typescript Parser

退出移动版