深刻理解 ESLint

前言

虽然如今已经有不少实用的 ESLint 插件了,但随着项目不断迭代发展,你可能会遇到已有 ESLint 插件不能知足如今团队开发的状况。这时候,你须要本身来建立一个 ESLint 插件。javascript

本文我将带你了解各类Lint工具的大体历史,而后一步一步地建立一个属于你本身的 ESLint 插件,以及教你如何利用AST抽象语法树来制定这个插件的规则。前端

以此来带你了解 ESLint 的实现原理。java

课外知识:Lint 简史

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

JSLint 和 JSHint

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

JSLint 面市后,确实帮助许多 JavaScript 开发者节省了很多排查代码错误的时间。可是 JSLint 的问题也很明显—— 几乎不可配置,全部的代码风格和规则都是内置好的;再加上 Douglas Crockford 推崇道系「爱用不用」的优良传统,不会向开发者妥协开放配置或者修改他以为是对的规则。因而 Anton Kovalyov 吐槽:「JSLint 是让你的代码风格更像 Douglas Crockford 的而已」,而且在 2011 年 Fork 原项目开发了 JSHint。《Why I forked JSLint to JSHint》git

JSHint 的特色就是可配置,同时文档也相对完善,并且对开发者友好。很快你们就从 JSLint 转向了 JSHint。github

ESLint 的诞生

后来几年你们都将 JSHint 做为代码检测工具的首选,但转折点在2013年,Zakas 发现 JSHint 没法知足本身制定规则需求,而且和 Anton 讨论后发现这根本不可能在JShint上实现,同时 Zakas 还设想发明一个基于 AST 的 lint。因而 2013年6月份,Zakas 发布了全新 lint 工具——ESLint。《Introducing ESLint》npm

ESLint早期源码json

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 的出现。api

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 的前端主流工具。

目标&涉及知识点

本文 ESLint 插件目标是在项目开发中禁用:console.time()

  • AST 抽象语法树
  • ESLint
  • Npm 发布
  • 单元测试

插件脚手架构建

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

npm install -g yo generator-eslint
复制代码

本地新建文件夹eslint-plugin-demofortutorial:

mkdir eslint-plugin-demofortutorial
cd eslint-plugin-demofortutorial
复制代码

初始化 ESLint 插件的项目结构:

yo eslint:plugin // 搭建一个初始化的目录结构
复制代码

此时文件的目录结构为:

.
├── README.md
├── lib
│   ├── index.js
│   └── rules
├── package.json
└── tests
    └── lib
        └── rules
复制代码

安装依赖:

npm install
复制代码

至此,环境搭建完毕。

建立规则

终端执行:

yo eslint:rule // 生成默认 eslint rule 模版文件
复制代码

此时项目结构为:

.
├── README.md
├── docs // 使用文档
│   └── rules
│       └── no-console-time.md
├── lib // eslint 规则开发
│   ├── index.js
│   └── rules // 此目录下能够构建多个规则,本文只拿一个规则来说解
│       └── no-console-time.js
├── package.json
└── tests // 单元测试
    └── lib
        └── rules
            └── no-console-time.js
复制代码

上面结构中,咱们须要在 ./lib/ 目录下去开发 Eslint 插件,这里是定义它的规则的位置。

AST 在 ESLint 中的运用

在正式写 ESLint 插件前,你须要了解下 ESLint 的工做原理。其中 ESLint 使用方法你们应该都比较熟悉,这里不作讲解,不了解的能够点击官方文档如何在项目中配置 ESLint

在公司团队项目开发中,不一样开发者书写的源码是各不相同的,那么在 ESLint 中,如何去分析每一个人写的源码呢?

做为开发者,面对这类问题,咱们必须懂得要使用 抽象的手段 !那么 Javascript 的抽象性如何体现呢?

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

ESLint 中,默认使用 esprima 来解析咱们书写的 Javascript 语句,让其生成抽象语法树,而后去 拦截 检测是否符合咱们规定的书写方式,最后让其展现报错、警告或正常经过。 ESLint 的核心就是规则(rules),而定义规则的核心就是利用 AST 来作校验。每条规则相互独立,能够设置禁用off、警告warn⚠️和报错error❌,固然还有正常经过不用给任何提示。

规则建立

上面讲完了 ESLintAST 的关系以后,咱们能够正式进入开发具体规则。先来看以前生成的 lib/rules/no-console-time.js:

/**
 * @fileoverview no console.time()
 * @author Allan91
 */
"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 会利用咱们的配置对抛出的内容作不一样的展现。

具体参数配置详情见官方文档

本文建立的 ESLint 插件是为了避免让开发者在项目中使用 console.time(),先看看这段代码在抽象语法树中的展示:

其中,咱们将会利用如下内容做为判断代码中是否含有 console.time:

那么咱们根据上面的AST(抽象语法书)在 lib/rules/no-console-time.js 中这样书写规则:

/** * @fileoverview no console.time() * @author Allan91 */
"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
        ],
        // 报错信息描述
        messages: {
            avoidMethod: "console method '{{name}}' is forbidden.",
        },
    },

    create: function(context) {
        return {
            // 键名为ast中选择器名
            'CallExpression MemberExpression': (node) => {
                // 若是在ast中知足如下条件,就用 context.report() 进行对外警告⚠️
                if (node.property.name === 'time' && node.object.name === 'console') {
                    context.report({
                        node,
                        messageId: 'avoidMethod',
                        data: {
                            name: 'time',
                        },
                    });
                }
            },
        };
    }
};

复制代码

再修改 lib/index.js

"use strict";

module.exports = {
    rules: {
        'no-console-time': require('./rules/no-console-time'),
    },
    configs: {
        recommended: {
            rules: {
                'demofortutorial/no-console-time': 2, // 能够省略 eslint-plugin 前缀
            },
        },
    },
};
复制代码

至此,Eslint 插件建立完成。接下去你须要作的就是将此项目发布到 npm平台。 根目录执行:

npm publish
复制代码

打开npm平台,能够搜索到上面发布的 eslint-plugin-demofortutorial 这个 Node 包。

如何使用

发布完以后在你须要的项目中安装这个包:

npm install eslint-plugin-demofortutorial -D
复制代码

而后在 .eslintrc.js 中配置:

"extends": [
    "eslint:recommended",
    "plugin:eslint-plugin-demofortutorial/recommended",
],
"plugins": [
    'demofortutorial'
],
复制代码

若是以前没有.eslintrc.js 文件,能够执行下面命令生成:

npm install -g eslint
eslint --init
复制代码

此时,若是在当前项目的 JS 文件中书写 console.time,会出现以下效果:

单元测试(完善)

对于完整的 npm 包来讲,上面还只算是个“半成品”,咱们须要写单元测试来保证它的完整性和安全性。

下面来完成单元测试,在 ./tests/lib/rules/no-console-time.js 中编写以下代码:

'use strict';

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

let rule = require('../../../lib/rules/no-console-time');

let RuleTester = require('eslint').RuleTester;

// ------------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------------

let ruleTester = new RuleTester({
    parserOptions: {
        ecmaVersion: 10,
    },
});

ruleTester.run('no-console-time', rule, {

    valid: [ // 合法示例
        '_.time({a:1});',
        "_.time('abc');",
        "_.time(['a', 'b', 'c']);",
        "lodash.time('abc');",
        'lodash.time({a:1});',
        'abc.time',
        "lodash.time(['a', 'b', 'c']);",
    ],

    invalid: [ // 不合法示例
        {
            code: 'console.time()',
            errors: [
                {
                    messageId: 'avoidMethod',
                },
            ],
        },
        {
            code: "console.time.call({}, 'hello')",
            errors: [
                {
                    messageId: 'avoidMethod',
                },
            ],
        },
        {
            code: "console.time.apply({}, ['hello'])",
            errors: [
                {
                    messageId: 'avoidMethod',
                },
            ],
        },
        {
            code: 'console.time.call(new Int32Array([1, 2, 3, 4, 5]));',
            errors: 1,
        },
    ],
});

复制代码

上面测试代码详细介绍见官方文档

根目录执行:

npm run test
复制代码

至此,这个包的开发完成。其它规则开发也是相似,好比您能够继续制定其它规范,好比 ️console.log()debugger警告等等。

其它

因为自动生成ESLint的项目中依赖的 eslint 版本还在 3.x 阶段,会对单元测试语法解析形成以下报错:

'Parsing error: Invalid ecmaVersion.'
复制代码

建议将该包升级到 "eslint": "^5.16.0"



以上。

查看Github上的项目仓库

查看Npm上发布的包





参考资料:

zhuanlan.zhihu.com/p/32297243 en.wikipedia.org/wiki/Lint_(… octoverse.github.com/ jslint.com medium.com/@anton/why-… www.nczonline.net/blog/2013/0… eslint.org jscs.info github.com/babel/babel… github.com/yannickcr/e… www.nczonline.net/blog/2016/0… medium.com/@markelog/j…

相关文章
相关标签/搜索