開發 eslint 規則

前端的平常開發離不開各類 lint 的支持,使用 lint 的一種誤解是:我的能力不足,必須 lint 規範才能寫出規範的代碼,實際上規範的定義主要取決於開源項目做者的習慣,或者公司團隊編碼的習慣,即便兩個前端專家,寫出的代碼規範也會有差異。前端

今天主題聊聊 eslint,做爲最主流的 JavaScript lint 工具深受你們喜好,而 JSHint 卻逐漸淡出了你們的視線,使用的比較少了vue

經常使用的 eslint 擴展有 standard,airbnb 等node

剖析 eslint 擴展

擴展無非就做兩個事情react

  • 在原有的 eslint 的基礎上配置些 config(具體規則參數,全局變量,運行環境等)
  • 自定義些本身的 rule,以知足需求

原理就是利用 eslint 的繼承模式,理論上能夠無限繼承並覆蓋上一級的規則git

第一條不詳細介紹了,eslint 官網說的十分詳細,基本每一條規則都支持自定義參數,覆蓋面也特別廣,基本上全部的語法都有 rulegithub

第二條的自定義 rule 纔是本文的重頭戲,由於特殊的業務場景靠 eslint 自身配置已經沒法知足業務需求了,如:算法

  • eslint-plugin-vue
  • eslint-plugin-react
  • eslint-plugin-jest

通常特殊場景的自定義規則都使用 eslint-plugin-* 的命名,使用時能夠方便的寫成express

{
  plugins: [
    'vue',
    'react',
    'jest'
  ]
}

固然 eslint-config-* 同理,不過配置時須要寫成npm

{
  extends: 'standard'
}

下面介紹下開發流程json

建立 eslint plugin 工程

官方推薦使用 yeoman 生成項目,感受生成的項目比較守舊,推薦下習慣個人項目結構

eslint-plugin-skr
  |- __tests__
  |  |- rules
  |  |- utils
  |
  |- lib
  |  |- rules
  |  |- utils
  |  |- index.js
  |
  |- jest.config.js
  |
  |- package.json
  |
  |- README.md

總體看下來發現多了 jest 配置文件,是的 yeoman 生成的項目默認採用 Mocha 做爲測試框架,我的感受調試起來麻煩,沒有 jest 靈活,vscode 輕鬆搞定調試

教程一搜一大把哈,給伸手黨一個連接 debugging-jest-tests

關於 jest 的 config 文件也po出來一下,都是些基本的配置,複雜的用不到,下面會詳細介紹測試部分

module.exports = {
  testEnvironment: 'node',
  roots: ['__tests__'],
  resetModules: true,
  clearMocks: true,
  verbose: true
}

自定義的規則所有在 lib/rules 下面,每條規則單獨一個文件足以

下面一個簡單的例子打通任督二脈

開發一個規則

前期準備

這個官方文檔寫的密密麻麻,好幾十個屬性,其實只是冰山一角,有不少複雜場景須要考慮

有人疑問:必定須要精通 AST?

個人回答是固然不須要,簡單瞭解即是,最起碼知道解析出來的語法樹大致結構長什麼樣子

那就隨便給本身一個命題寫吧!寫個超級簡單的

module.exports = {
  meta: {
    docs: {
      description: '禁止塊級註釋',
      category: 'Stylistic Issues',
      recommended: true
    }
  },

  create (context) {
    const sourceCode = context.getSourceCode()

    return {
      Program () {
        const comments = sourceCode.getAllComments()

        const blockComments = comments.filter(({ type }) => type === 'Block')

        blockComments.length && context.report({
          message: 'No block comments'
        })
      }
    }
  }
}

具體寫法官方文檔有介紹哈,就不贅述了,例子也十分簡單,調用了環境變量 context 中的方法獲取所有註釋

稍微複雜點的場景

如須要 lint bar 對象中屬性的順序,以下假設一個規則

// good
const bar = {
  meta: {},
  double: num => num * 2
}

// bed
const bar = {
  double: num => num * 2,
  meta: {},
}

這個第一次些會有些蒙,官網沒有提供具體的例子,解決辦法很簡單,推薦一個利器 astexplorer

點進去彆着急複製代碼查看 AST 結果,首先選擇 espree(eslint 使用的語法解析庫),以下

這短短的四行代碼會對應着一個抽象語法樹,以下圖:

因爲全展開太長了哈,感興趣的自行嘗試,會發現層級嵌套的特別深,找到 bar 的屬性須要 Program.body[0].declarations[0].init.properties

固然不至於每次都從最頂級的 Program 找下來,從上面的例子能夠看出 create 方法的 return 返回的是一個 object,裏面能夠定義不少檢測類型,如官網的例子:

function checkLastSegment (node) {
  // report problem for function if last code path segment is reachable
}

module.exports = {
  meta: { ... },
  create: function(context) {
    // declare the state of the rule
    return {
      ReturnStatement: function(node) {
        // at a ReturnStatement node while going down
      },
      // at a function expression node while going up:
      "FunctionExpression:exit": checkLastSegment,
      "ArrowFunctionExpression:exit": checkLastSegment,
      onCodePathStart: function (codePath, node) {
        // at the start of analyzing a code path
      },
      onCodePathEnd: function(codePath, node) {
        // at the end of analyzing a code path
      }
    }
  }
}

這裏可使用 VariableDeclarator 類型做爲檢察目標,從下面的解析樹能夠分析出篩選條件

VariableDeclarator 對象做爲當前的 node

當變量名爲 bar,即 node.id.name === 'bar',且值爲對象,即 node.init.type === 'ObjectExpression',代碼以下:

module.exports = {
  meta: { ... },
  create (context) {
    return {
      VariableDeclarator (node) {
        const isBarObj = node.id.name === 'bar' &&
          node.init.type === 'ObjectExpression'

        if (!isBarObj) return

        // checker
      }
    }
  }
}

就這樣成功取到 bar 對象後就能夠檢測屬性的順序了,排序算法一大把,挑一個喜歡的用就好了,這裏不囉嗦了,直接上結果:

const ORDER = ['meta', 'double']

function getOrderMap () {
  const orderMap = new Map()

  ORDER.forEach((name, i) => {
    orderMap.set(name, i)
  })

  return orderMap
}

module.exports = {
  create (context) {
    const orderMap = getOrderMap()

    function checkOrder (propertiesNodes) {
      const properties = propertiesNodes
        .filter(property => property.type === 'Property')
        .map(property => property.key)

      properties.forEach((property, i) => {
        const propertiesAbove = properties.slice(0, i)
        const unorderedProperties = propertiesAbove
          .filter(p => orderMap.get(p.name) > orderMap.get(property.name))
          .sort((p1, p2) => orderMap.get(p1.name) > orderMap.get(p2.name))

        const firstUnorderedProperty = unorderedProperties[0]

        if (firstUnorderedProperty) {
          const line = firstUnorderedProperty.loc.start.line

          context.report({
            node: property,
            message: `The "{{name}}" property should be above the "{{firstUnorderedPropertyName}}" property on line {{line}}.`,
            data: {
              name: property.name,
              firstUnorderedPropertyName: firstUnorderedProperty.name,
              line
            }
          })
        }
      })
    }

    return {
      VariableDeclarator (node) {
        const isBarObj = node.id.name === 'bar' &&
          node.init.type === 'ObjectExpression'

        if (!isBarObj) return

        checkOrder(node.init.properties)
      }
    }
  }
}

這裏代碼有點多,耐心看完其實挺簡單的,大體解釋下

getOrderMap 方法將數組轉成 Map 類型,方面經過 get 獲取下標,這裏也能夠處理多緯數組,例如兩個 key 但願在相同的排序等級,不分上下,能夠寫成:

const order = [
  'meta'
  ['double', 'treble']
]

function getOrderMap () {
  const orderMap = new Map()

  ORDER.forEach((name, i) => {
    if (Array.isArray(property)) {
      property.forEach(p => orderMap.set(p, i))
    } else {
      orderMap.set(property, i)
    }
  })

  return orderMap
}

這樣 doubletreble 就擁有相同的等級了,方便後面擴展,固然實際狀況會有 n 個屬性的排序規則,也能夠在這個規則上輕鬆擴展,內部的 sort 邏輯就不贅述了。

開發就介紹到這裏,經過上面安利的在線語法解析工具能夠輕鬆反推出 lint 邏輯。

若是 rule 比較複雜,就須要大量的 utils 支持,否則每一個 rule 都會顯得一團糟,比較考驗公共代碼提取的能力

測試

如前面所講建議使用 jest 進行測試,這裏的測試和普通的單元測試還不太同樣,eslint 是基於結果的測試,什麼意思呢?

lint 只有兩種狀況,經過與不經過,只須要把經過和不經過的狀況整理成兩個數組,剩下的工做交給 eslint 的 RuleTester 處理就好了

上面的屬性排序 rule,測試以下:

const RuleTester = require('eslint').RuleTester
const rule = require('../../lib/rules/test')

const ruleTester = new RuleTester({
  parserOptions: {
    ecmaVersion: 6
  }
})

ruleTester.run('test rule', rule, {
  valid: [
    `const bar = {
      meta: {},
      double: num => num * 2
    }`
  ],
  invalid: [
    {
      code: `const bar = {
        double: num => num * 2,
        meta: {},
      }`,
      errors: [{
        message: 'The "meta" property should be above the "double" property on line 2.'
      }]
    }
  ]
})

valid 中是但願經過的代碼,invalid 中是不但願經過的代碼和錯誤信息,到這裏一個 rule 算是真正完成了。

打包輸出

最後寫好的 rules 須要發一個 npm 包,以便於在項目中使用,這裏就不贅述怎麼發包了,簡單聊聊怎麼優雅的把 rules 導出來。

直接上代碼:

const requireIndex = require('requireindex')

// import all rules in lib/rules
module.exports.rules = requireIndex(`${__dirname}/rules`)

這裏使用了三方依賴 requireindex,對於批量的導出一個文件夾內的全部文件顯得簡潔不少。

固然前提是保證 rules 文件夾下都是 rule 文件,不要把 utils 寫進去哈

總結

行文目的是國內外對於自定義 eslint rule 的相關資源較少,但願分享一些寫自定義規則的經驗。

千萬不要在學習 AST 上浪費時間,不一樣的庫對 AST 的實現是不一樣的,下次寫 babel 插件又要學其它的 AST 規則,再次安利一下 AST 神器 astexplorer,只要把須要驗證的代碼放到 astexplorer 裏跑一遍,而後總結出規律,邏輯其實十分簡單,對 AST 結果進行判斷就好了。

從團隊層面講,但願全部的團隊都有本身的 eslint 規則庫,能夠大大下降 code review 的成本,又能保證代碼的一致性,一勞永逸的事情。

相關文章
相關標籤/搜索