最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • ESLint 之插件 & 原理

    正文概述 掘金(小被子)   2021-02-04   658

    新建一个 ESLint 插件

    插件目标:禁止项目中 setTimeout 的第二个参数是数字。例如:setTimeout(() => {}, 2) 是违背规则,const num = 2;setTimeout(() => {}, num) 是 ok 的。

    项目初始化

    • 全局安装 eslint plugin 脚手架工具,ESLint 官方为了方便开发者开发插件,提供了使用 Yeoman 模板 (generator-eslint) 。

      npm install -g yo generator-eslint
      
    • 初始化项目目录

      mkdir eslint-plugin-irenePugin
      cd eslint-plugin-irenePugin
      yo eslint:plugin
      

      下面进入命令行交互流程,结束后会生成自定义插件的项目目录

      ? What is your name? irene
      ? What is the plugin ID? irenelint // 插件ID
      ? Type a short description of this plugin: for testing creating a eslint plugin // 插件描述
      ? Does this plugin contain custom ESLint rules? Yes // 是否包含自定义 ESLint 规则
      ? Does this plugin contain one or more processors? No // 是否包含一个或多个处理器
         create package.json
         create lib/index.js
         create README.md
      
    • 创建规则

      yo eslint:rule
      

      下面进入命令行交互流程,结束后会生成一个规则文件的模板

      ? What is your name? irene
      ? Where will this rule be published? ESLint Plugin // 规则将在哪里发布
      ❯ ESLint Core    // 官方核心规则
        ESLint Plugin  // ESLint 插件
      ? What is the rule ID? settimeout-no-number // 规则ID
      ? Type a short description of this rule: the second param of setTimeout is forbidden to use number // 规则描述
      ? Type a short example of the code that will fail: setTimeout(() => {}, 2) // 失败例子的代码
         create docs/rules/settimeout-no-number.md
         create lib/rules/settimeout-no-number.js
         create tests/lib/rules/settimeout-no-number.js
      
    • 生成的项目目录如下

      ├── README.md
      ├── docs // 使用文档
      │   └── rules // 所有规则的文档
      │       └── settimeout-no-number.md // 具体规则文档
      ├── lib // eslint 规则开发
      │   ├── index.js 引入 + 导出 rules 文件夹的规则
      │   └── rules // 此目录下可以构建多个规则
      │       └── settimeout-no-number.js // 规则细节
      ├── package.json
      └── tests // 单元测试
          └── lib
              └── rules
                  └── settimeout-no-number.js // 测试该规则的文件
      
    • 安装项目依赖

      npm install
      

    规则模版

    打开 lib/rules/settimeout-no-number.js,可以看到通过上述命令行操作后生成的模版。

    module.exports = {
      meta: {
        docs: {
          description: "the second param of setTimeout is forbidden to use number",
          category: "Fill me in",
          recommended: false
        },
        fixable: null,  // or "code" or "whitespace"
        schema: [
          // fill in your schema
        ]
      },
      create: function(context) {
        return {
          // give me methods
        };
      }
    };
    

    create 方法返回的是一个 key 为选择器,value 为回调函数(参数是 AST node)的对象,例如:{ 'CallExpression': (node) => {} },ESLint 会收集所有生效规则监听的选择器以及对应的回调函数,在遍历 AST 时,每当匹配到选择器,就会触发该选择器对应的回调。

    AST:Abstract Syntax Tree

    ESLint 是通过将代码解析成 AST 并遍历它实现代码校验和格式化的,具体将在下面讨论。现在我们先看下 setTimeout(() => {}, 2) 解析成的 AST 是什么样子。在线AST

    ESLint 之插件 & 原理

    编写规则

    通过观察生成的 AST,过滤出我们要选中的代码,对代码的值进行判断。

    // lib/rules/settimeout-no-number.js
    module.exports = {
      meta: {
        docs: {
          description: "the second param of setTimeout is forbidden to use number",
          category: "Fill me in",
          recommended: false
        },
        fixable: null,  // or "code" or "whitespace"
        schema: [
          // fill in your schema
        ]
      },
      create: function(context) {
        return {
          // give me methods
          'CallExpression': (node) => {
            if (node.callee.name !== 'setTimeout') return // 不是 setTimeout 直接过滤
    
            const timeNode = node.arguments && node.arguments[1] // 获取第二个参数
            if (!timeNode) return
    
            if (timeNode.type === 'Literal'  && typeof timeNode.value === 'number') {
              context.report({
                  node,
                  message: 'setTimeout 第二个参数禁止是数字'
              })
            }
          }
        };
      }
    };
    

    测试用例

    提供一些违背和通过规则的测试代码

    // tests/lib/rules/settimeout-no-number.js
    var rule = require("../../../lib/rules/settimeout-no-number"), RuleTester = require("eslint").RuleTester;
    
    var ruleTester = new RuleTester();
    ruleTester.run("settimeout-no-number", rule, {
      valid: [
        {
          code: "let num = 1000; setTimeout(() => { console.log(2) }, num)",
        },
      ],
      invalid: [
        {
          code: "setTimeout(() => {}, 2)",
          errors: [
            {
              message: "setTimeout 第二个参数禁止是数字", // 与 rule 抛出的错误保持一致
              type: "CallExpression", // rule 监听的对应钩子
            },
          ],
        },
      ],
    });
    

    自动修复

    • fixable: 'code' 开始修复功能;
    • context.report() 提供一个 fix 函数;
    // lib/rules/settimeout-no-number.js
    module.exports = {
      meta: {
        docs: {
          description: "the second param of setTimeout is forbidden to use number",
          category: "Fill me in",
          recommended: false
        },
        fixable: 'code',
      },
      create: function(context) {
        return {
          // give me methods
          'CallExpression': (node) => {
            if (node.callee.name !== 'setTimeout') return // 不是 setTimeout 直接过滤
    
            const timeNode = node.arguments && node.arguments[1] // 获取第二个参数
            if (!timeNode) return
    
            if (timeNode.type === 'Literal'  && typeof timeNode.value === 'number') {
              context.report({
                node,
                message: 'setTimeout 第二个参数禁止是数字',
                fix(fixer) {
                  const numberValue = timeNode.vlaue;
                  const statementString = `const num = ${numberValue}\n`;
                  return [
                    fixer.replaceTextRange(node.arguments[1].range, 'num'),
                    fixer.insertTextBeforeRange(node.range, statementString)
                  ]
                }
              })
            }
          }
        };
      }
    };
    

    调试

    点击 debug,然后选中项目。

    点击设置,会打开一个 launch.json,program 字段填上要 debug 的文件。

    ESLint 之插件 & 原理

    在 lib/rules/settimeout-no-number.js 打 debugger,点击启动程序。

    ESLint 之插件 & 原理

    发布插件

    • 登陆 npm:npm login

    • 发布 npm 包:npm publish

    使用

    • 安装插件

      npm install --save-dev eslint-plugin-irenelint
      
    • 引入插件并开启规则

      • 通过 plugins

        // .eslintrc.js
        module.exports = {
          plugins: [ 'irenelint' ],
          rules: {
            'irenelint/settimeout-no-number': 'error'
          }
        }
        
      • 通过 extends

        因为 plugins 中的规则默认是不启用的,需要一条条的在 rules 中开启,当规则比较多的时候,写起来太麻烦,这时就可以使用 extends。

        首先,我们需要修改下 lib/index.js

        // lib/index.js
        var requireIndex = require("requireindex");
        
        const output = {
          rules:  requireIndex(__dirname + "/rules"), // 所有规则
          configs: {
            recommended: {
              plugins: ['irenelint'], // 引入插件
              rules: {
                'irenelint/settimeout-no-number': 'error' // 开启规则
              }
            }
          }
        }
        
        module.exports = output;
        

        然后使用 extends

        // .eslintrc.js
        module.exports = {
          extends: [ 'plugin:irenelint/recommended' ]
        }
        

    测试

    修复前:第一条提示就是自动修复的提示

    ESLint 之插件 & 原理

    修复后:如果配置了保存时自动修复,就会在保存的时候自动改正。

    ESLint 之插件 & 原理

    ESLint 原理

    假设待校验的文件内容是

    console.log('irene');
    

    依据文件内容生成如下 AST ,将每一个节点传入 nodeQueue 队列中,每个会被传入两次;在线AST

    ESLint 之插件 & 原理

    nodeQueue = [
      {
        isEntering: true,
        node: {
          type: 'Program',
          body: [Array],
          sourceType: 'module',
          range: [Array],
          loc: [Object],
          tokens: [Array],
          comments: [],
          parent: null
        }
      },
      {
        isEntering: true,
        node: {
          type: 'ExpressionStatement',
          expression: [Object],
          range: [Array],
          loc: [Object],
          parent: [Object]
        }
      },
      {
        isEntering: true,
        node: {
          type: 'CallExpression',
          callee: [Object],
          arguments: [Array],
          optional: false,
          range: [Array],
          loc: [Object],
          parent: [Object]
        }
      },
      {
        isEntering: true,
        node: {
          type: 'MemberExpression',
          object: [Object],
          property: [Object],
          computed: false,
          optional: false,
          range: [Array],
          loc: [Object],
          parent: [Object]
        }
      },
      {
        isEntering: true,
        node: {
          type: 'Identifier',
          name: 'console',
          range: [Array],
          loc: [Object],
          parent: [Object]
        }
      },
      {
        isEntering: false,
        node: {
          type: 'Identifier',
          name: 'console',
          range: [Array],
          loc: [Object],
          parent: [Object]
        }
      },
      {
        isEntering: true,
        node: {
          type: 'Identifier',
          name: 'log',
          range: [Array],
          loc: [Object],
          parent: [Object]
        }
      },
      {
        isEntering: false,
        node: {
          type: 'Identifier',
          name: 'log',
          range: [Array],
          loc: [Object],
          parent: [Object]
        }
      },
      {
        isEntering: false,
        node: {
          type: 'MemberExpression',
          object: [Object],
          property: [Object],
          computed: false,
          optional: false,
          range: [Array],
          loc: [Object],
          parent: [Object]
        }
      },
      {
        isEntering: true,
        node: {
          type: 'Literal',
          raw: "'irene'",
          value: 'irene',
          range: [Array],
          loc: [Object],
          parent: [Object]
        }
      },
      {
        isEntering: false,
        node: {
          type: 'Literal',
          raw: "'irene'",
          value: 'irene',
          range: [Array],
          loc: [Object],
          parent: [Object]
        }
      },
      {
        isEntering: false,
        node: {
          type: 'CallExpression',
          callee: [Object],
          arguments: [Array],
          optional: false,
          range: [Array],
          loc: [Object],
          parent: [Object]
        }
      },
      {
        isEntering: false,
        node: {
          type: 'ExpressionStatement',
          expression: [Object],
          range: [Array],
          loc: [Object],
          parent: [Object]
        }
      },
      {
        isEntering: false,
        node: {
          type: 'Program',
          body: [Array],
          sourceType: 'module',
          range: [Array],
          loc: [Object],
          tokens: [Array],
          comments: [],
          parent: null
        }
      }
    ]
    

    遍历所有整合好的规则,如果该条规则不为 0 或 'off'(即规则是开启的),获取该条规则的 rule 对象,执行 create 函数返回监听对象,它表明了这条规则监听了哪些 AST 节点,当遍历这些节点的时候就会执行对应的回调函数;

    // 假设整合好的规则如下
    configuredRules = {
      '@typescript-eslint/no-explicit-any': [ 0 ], // ruleId: [severity]
      '@typescript-eslint/explicit-module-boundary-types': [ 0 ],
      'prettier/prettier': [ 'error' ],
      '@typescript-eslint/no-unused-vars': [ 'warn' ],
      ...
    }
    ruleObj = {
      meta: 
      create: (context) => {
        return {
          'CallExpression:exit': func1,
          'Identifier': func2
        }
      }
    }
    

    遍历该条规则的监听对象,为每个 AST 节点注册监听函数

    listeners: {
      'CallExpression:exit': [func1],
      Identifier: [func2]
    }
    

    遍历规则结束之后,我们得到了一个 listeners 对象,key 是 AST 节点,value 是回调函数数组;

    遍历第一步获取到的 nodeQueue,触发 listeners 中对应的回调函数,比如遍历到 CallExpression 的时候,去执行 listeners.CallExpression 数组里的函数,函数会检测当前节点是否违背规则,如果违背,则报告 warn/error,存于 lintingProblems 中;

    返回 lintingProblems

    参考

    blog.csdn.net/obkoro1/art…

    其他

    • ESLint 之解析包名
    • ESLint 之与 Prettier 配合使用

    起源地下载网 » ESLint 之插件 & 原理

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元