likes
comments
collection
share

叨叨eslint如何自定义规则

作者站长头像
站长
· 阅读数 19

一、前言

  • ESLint 是一个用于识别和报告ECMAScript/JavaScript代码中发现的模式的工具,其目标是使代码更加一致并避免 bug
  • 随着项目,团队的不断发展,可能会遇到已有规则不能满足现在团队开发的情况。这时候,则可以选择创建一个 ESLint插件然后自定义规则进行规范
  • 带着目的完成这件事,那么我们需要从四步入手
    • 了解eslint的过程
    • 了解AST树
    • 了解如何开发一个eslint规则
    • 了解如何测试该规则

二、eslint的过程

我们只需要了解比较关键的两步

parse

  • ESLint的规则是基于AST树进行检查的,那么肯定有一步是从 源码-> AST树,这个过程就称为 parse
  • 在这个过程是可以切换parser的, Eslint默认使用的是Espree, 也可以通过配置切换成其他的parser, 比如说常用的 @typescript/eslint-parser

使用规则

  • parse之后,会调用 runRules 这个方法对 AST 进行遍历检查(从上至下和从下至上各来一次)
  • runRules会遍历 AST。规则定义了要处理什么,AST 就会监听什么事件,然后处触发listener

三、了解AST树

总结了一下自己见过的用过的, 我觉得对AST的基础归类有一定的了解比直接看AST树astexplorer然后进行开发会更容易上手,也能够较全面的去分析各种状况

整体

  • 包裹在最外面的总是program节点,我们可以通过astexplorer来康康其中出现的属性,点击如图位置的按钮即可调整显示的属性 叨叨eslint如何自定义规则
  • start, end: 记录了开始和结束的位置
  • range: 以数组的方式记录了正文的位置,不包括前后注释
  • body: 存放了该文件下的其他节点
  • sourceType: 可以有两个选择,或是module或是script
  • comments: 该文件下所有注释都存放于此,分为Line和Block

字面量Literal

  • 平时我们的数字,字符串,布尔值,正则,null都是属于字面量 叨叨eslint如何自定义规则

注意: undefined并不是Literal, 而是Idtentifier

标识符Identifier

  • 这个也是经常出现的,我是喜欢把它当作一个变量来看待,可以是对象名,可以是key,可以是值,可以是函数名,也可以是函数参数
  • 这里感觉难以罗举,所以列举了三种比较常见的区分IdentifierLiteral 叨叨eslint如何自定义规则 叨叨eslint如何自定义规则 叨叨eslint如何自定义规则

声明Declaration

  • 这个就很容易辨别了,只要是声明,就是Declaration, 比如说声明一个变量,声明一个函数,声明一个类,或者是导入导出语句 叨叨eslint如何自定义规则
  • 也许你会发现怎么导入语句就只有一种ImportDeclaration, 而导出语句咔咔咔给我整了三种
  • 别急,继续往下看

Specifier

  • 这里主要介绍import的三种Specifier, 对导出的语句是将其导出声明归纳为三种,而对导入语句的声明语句则是统一为一种,其中导入的东西(见荧光部分)再细分为三种 叨叨eslint如何自定义规则

嘿,了解完声明语句那么也许你会好奇,如果我先声明,再赋值的话,这个表达式又是什么节点呢? 继续往下看看吧

表达式Exprssion

比如我们的赋值表达式,函数表达式,一元表达式,二元表达式,对象表达式,逻辑表达式啥啥啥的,还是比较多的,除了以下归纳的还有挺多的比如说NewExpression, UpdateExpression... 叨叨eslint如何自定义规则

看到这里你也许会发现,前面都是把代码拆分成小部件去识别,那么比如switch, for, while这些又是什么呢

语句Statement

我对它的形容就是,总是包裹在外面的大块头或者是能够独立执行的语句 叨叨eslint如何自定义规则

typescrript

  • 目前typescript的普及度也越来越高了,单纯对JS进行规范限制在很多时候都是不够的,故我们可以借助@typescript/eslint-parser,从而对TS代码也能进行一个parse
  • 那么又会多出了很多节点和属性出来,不过大概的基本分类还是以上几种

四、开发规则

先看官网 主要是分成两大部分,第一部分是meta属性,第二部分是create方法

 module.exports = {
   meta: {...}
   create(context) {...};
 }

meta

meta主要用来记录/配置一些元数据

  • type: 指示规则的类型,可以是problem, suggestion, layout;
    • problem就是识别的代码会导致错误嘛,或者导致让人困然,是作为一个问题的存在;
    • suggestion就是可以有更好的方法来完成,建议以更好的方式去完成
    • layout表示比较注重外观,比如说分号,空格....
  • docs:
    • description: 描述
    • url 可以指定完整的文档的URL-就是报错时的信息中出现的文档路径
  • schema: 指定规则配置的类型,如果没有指定的话配置内容是无法生效的
  • fixable: 如果没有该属性的话,fix功能不生效
  • messages: 作为对象的存在,key为messageId, value为报错信息

具体怎么写可以看以下例子

create

不完全是标准的estree,也不能直接使用estree-walk,因为eslint会给每个节点加上父属性

create: function(context) { 
    // 一些公共的事件和变量可以在这里定义
    // 也有比如对一些白名单文件进行过滤,可以直接在这里return {} 这样就不会走下面的操作
   return { 
        // 返回事件钩子
    }; 
}

事件钩子的回调有三种:

  • key为node或者selector的时候: 事件在进入node或者selector的时候触发(最常用到)
  • key为“node:exit”或者“selector:exit”的时候: 事件在退出node或selector的时候出发
  • key为事件名称的时候: ESLint 为代码路径分析调用函数(这个感觉还是比较少用到的,比如在计算圈复杂度的时候可以用到)

这里的selector其实就类似于CSS的selector, 个人感觉还是挺好用的,可以减少一些在回调函数里面的判断,也可以减少对相似类型node的多次调用,比如说:function就涵盖了具体可看selecotr

五、实践规则

个人实践,非最佳实践

  • 一般开发一个规则可以三步走
    • 写一个规则描述md文档, 可以包括一些good Case, bad Case ,description(忽略这一步)
    • 写规则内容:就是上面讲述的内容
    • 写规则单测:就写一些valid code和invalid code去判断规则内容是否符合预期
  • 比如可以实践一个规则, 动态import必须捕获错误,我们可以考虑以下三种情况
  • 前两种我们希望有catch去捕获错误,防止直接报错,后面我们希望有try catch将其包住
// 使用了Promise.all
async function test() {
  return Promise.all([
    // other
    import(modulePath),
  ]).then(() => {
     // doSomething
  }).catch((err) => {
    // doSomeThing
  }) 
}
// 直接使用import
import(modulePath).then(res => {
  // doSomething
}).catch((err) => {
// doSomething
})

// 使用await
try {
  const { MailEditor } = await import(modulePath); 
} catch {
  // doSomething
}
  • 那么就需要有两种报错信息, meta.messages就可以写为
messages: {
  'no-try': '动态import{{modulePath}}时需要用try catch包住',
  'no-catch': '动态import{{modulePath}}时需要catch捕获错误',
}
  • 提一嘴,这里是可以使用变量替换的,用{{xxx}}表示变量,可以传入不同的值
  • 规则内容
// try-catch-import.js
module.exports = {
  meta: {
    type: 'problem', // `problem`, `suggestion`, or `layout`
    docs: {
      description: "动态import需要用try catch包住", // 描述
      url: ‘xxxx’, // 这里写文档对应的链接
    },
    fixable: null, // Or `code` or `whitespace`  如果可以快速修复的话就改为code,然后在下面写如何去修复
    schema: [], // 如果需要配置传入参数的话则在这里配置
    messages: {
      'no-try': '动态import需要用try catch包住',
      'no-catch': '动态import需要catch捕获错误',
    }
  },

  create(context) {
      // 向上寻找判断是否出现TryStatement
    const findParentHasTry = (node) => {
      while(node.type !== 'Program') {
        if (node.type === 'TryStatement') {
          return true;
        }
        node = node.parent;
      }
      return false;
    } 
    // 会找到所有的 .then,.xxx , 找到catch就算ok
    const findCallAndExpression = (node) => {
      const currentUseProperty = [];
      while(node.type !== 'CallExpression' || node.type !== 'MemberExpression') {
        if (node.type === 'MemberExpression' && node.property.type === 'Identifier') {
          currentUseProperty.push(node.property.name);
        }
        if (node.parent &&  node.parent.type === 'CallExpression' && node.parent.parent) {
          node = node.parent.parent;
        } else {
          break;
        }
      }
      return currentUseProperty.some((item) => item === 'catch')
    }
    // import作为argument的情况,也就是Promise.all的情况
    const checkAsArgumentImport = (node) => {
      while(node.type !== 'Program') {
        if (node.type === 'CallExpression' && isPromise(node) && node.parent) {
            return findCallAndExpression(node.parent);
        }
        node = node.parent
      }
      return false
    }
    const isPromise = (node) => {
      return node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' &&  node.callee.object.name === 'Promise'
    }
    return {
    // key为node节点,在进入节点的时候触发value的回调函数
      ImportExpression: (node) => {
        if (node.parent.type === 'AwaitExpression') {
        // context.report用于报错,需要传入node属性,messageId/message
          findParentHasTry(node.parent) || context.report({
            node,
            messageId: 'no-try',  //指定messages中的key,就会报错对应的value
            data: {  // 传入报错信息对应的变量
                modulePath: node.source.value ?? node.source.name
            }
          })
        } else if (node.parent && node.parent.type === 'MemberExpression' && node.parent.property.type === 'Identifier') {
           findCallAndExpression(node.parent) || context.report({
            node,
            messageId: 'no-catch'
          })
        } else {
          // 上面两种情况都没有命中的话则找到最近的MemberExpression
          checkAsArgumentImport(node) || context.report({
            node,
            messageId: 'no-catch',
            data: {
                modulePath: node.source.value ?? node.source.name
            }
          })
        }
      },
    };
  },
};
  • 测试文件
    • 无论是valid, 还是invalid都可以传入一些属性
    • 比如code:用例代码, filename: 代码所在文件路径, options: 传入配置项
    • invalid的每一项必须传入errors,会导致几个错误就需要传入几项
const rule = require('**/try-catch-import');  // 导入以上的规则内容
const RuleTester = require('eslint').RuleTester; // 导入RuleTester

// 一般我习惯使用@typescript-eslint/parser作为parser了,这个规则可以省略这一步
const eslintTester = new RuleTester({
   parser: require.resolve('@typescript-eslint/parser'), 
   parserOptions: {
       ecmaVersion: 6,
       sourceType: 'module',
       ecmaFeatures: {
         jsx:true
       }
     },
});

eslintTester.run('try-catch-import', rule, {
// 这里写一些Good case
   valid: [
    {
      // 使用await
      code: `
      try {
        const { xxxx } = await import('xxxxx'); 
      } catch {
        //
      }
      `
    }, 
    // import(xx).then().catch()
    {
      code : `
      import(modulePath).then(obj => {
        // 
          return obj
        }).catch(err => {
        //
        })
      `
    }, 
    {
      code : `
      import(modulePath).then(obj => {
        //  
          return obj
        }).catch(err => {
        // 
        }).finally(all => {})
      `
    },
    // 使用Promise.all等
    {
         code: `
        async function test() {
          return Promise.all([
            xxxxx,
            import('xxxxx'),
          ]).then() => {
           //
          }).catch((err)=> {})
        }
         `
    }],
    
    // 这里写一些base case
     invalid: [
      {
        code: `
           const { xxx } = await import('xxxxxxx');
        `,
        errors: [ { messageId: 'no-try', type: "ImportExpression"} ]
      }, 
      {
        code: `
        import(modulePath).then(obj => {
          // xxxxx 
            return xxxxx
        })
        `,
        errors: [ { messageId: 'no-catch', type: "ImportExpression"} ]
      },
       {
         code: `  
         async function test() {
          return Promise.all([
            xxx
            import('xxxxx'),
          ]).then(xxx) => {
          //
        }`,
         errors: [{ messageId: 'no-catch', type: "ImportExpression" }],
       },
     ],
});
转载自:https://juejin.cn/post/7209110611858358331
评论
请登录