iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 30
1
Modern Web

前端建置工具完全手冊系列 第 30

Day 30: 實作個 eslint plugin

這篇的完整的程式可以到 https://github.com/DanSnow/ironman-2020/tree/master/build-tool/packages/eslint-plugin

今天要來寫個 eslint plugin ,雖說是要寫個 plugin ,不過實際上是寫 eslint 的規則,這邊用我之前寫過的一個規則來示範,有時候會希望當 js 的物件是像這樣的時候 (比如是 vue 的 object 時):

export default {
  name: 'Foo',

  props: {},

  data: () => ({}),
}

像上面這樣中間都有個空行,這邊用很簡單的方式來判斷是不是 vue 的 object ,就兩種:

  • export default
  • 包在 Vue.extend

eslint 的規則大致分成兩個部份 metacreate:

  • meta: 這個規則的資料,如果這個規則可以被自動修復,也必須要定義在這裡
  • create: 建立規則的 AST visitor ,規則的檢查是在這邊做的

跟之前寫 babel 的 plugin 很像,第一步是先打開 AST Explorer ,選 eslint 用的 parser espree ,來選要使用的節點,這邊選用的是 ObjectExpress

module.exports = {
  meta: {
    // 可以被修復的規則一定要定義,這邊除了 'whitespace' 外還有 'code' ,不過這只是分類上的問題
    // 這邊因為是加換行,所以選 'whitespace'
    fixable: 'whitespace',
    // 可以定義可能會出現的訊息,這個就可以在統一的地方做管理,這是 eslint 推薦的作法
    // 當然,你也可以直接把訊息寫在 context.report
    messages: {
      requireNewline: 'require newline between',
    },
  },
  create: function (context) {
    return {
      ObjectExpression(node) {
        if (
          // 判斷父節點是 export default
          node.parent.type === 'ExportDefaultDeclaration' ||
          // 或父節點是 `Vue.extend`
          (node.parent.type === 'CallExpression' && isVueExtend(node.parent.callee))
        ) {
          // 取得 source code 物件,等下的 fixer 需要用到
          const sourceCode = context.getSourceCode()
          // 用 for 迴圈把物件的屬性兩兩抓一組,檢查中間有沒有加空行
          for (let i = 0; i < node.properties.length - 1; ++i) {
            // 這邊判斷的方法很簡單,前一個屬性結尾的行號,必須跟下一個屬性結尾的行號差 2 以上
            // 也就是中間有 2 個以上的空行
            if (node.properties[i + 1].loc.start.line - node.properties[i].loc.end.line < 2) {
              context.report({
                // 用預先定義的訊息
                messageId: 'requireNewline',
                // 或是你可以直接把訊息寫進來
                // message: 'require newline between',
                // 指定出錯的位置,因為是在兩個屬性的中間,所以就用前一個的 end 與後一個的 start 來指定
                loc: {
                  start: node.properties[i].loc.end,
                  end: node.properties[i + 1].loc.start,
                },
                // 如果出錯的位置正好是某個 AST 的節點,那也可以傳入節點
                // node: node
                fix(fixer) {
                  // 這邊是自動修復的部份,晚點再來講
                },
              })
            }
          }
        }
      },
    }
  },
}

正常來說 eslint 的 plugin 需要照著 eslint 的套件命名規則才有辦法載入,不過這邊為了方便測試,就直接呼叫 eslint 的函式把自訂的規則加入:

// eslint 的 Linter
const { Linter } = require('eslint')
// 我們的規則
const rule = require('./space-between-properties')

const linter = new Linter()

// 幫規則設定一個 id
const id = 'space-between-properties'
// 加入規則的定義,也就是上面的那個東西
linter.defineRule(id, rule)

// 執行規則
const res = linter.verify(
  `
export default {
  name: 'Foo',
  props: {},
}
`,

  {
    rules: {
      [id]: 'error',
    },
    parserOptions: {
      sourceType: 'module',
      ecmaVersion: 2015,
    },
  }
)

// 如果有錯誤的話就印出來
if (res.length) {
  console.log(res)
}

沒意外的話應該會看到上面有印出東西來,再來是加上自動修復的部份:


// 接續上面的 fix 的部份
fix(fixer) {
  // 取得兩個節點中間的 token
  const tokens = sourceCode.getTokensBetween(node.properties[i], node.properties[i + 1])
  // 「通常」中間只會有逗號,所以唯一的節點就是逗號
  const comma = tokens[0]
  // 要求 eslint 在逗號後加上換行
  return fixer.insertTextAfterRange(comma.range, '\n')
}

這邊修復也很簡單,就是在逗號後加上換行而已,不過上面也特別說了「通常」,其實這個 plugin 你只要在 , 後加上註解就會出問題了

eslint 會在最後一次把修復加上去,然後再跑一次所有規則,如果還是有可以修復的問題就再跑一次,直到沒有可以自動修復的問題,所以也不用擔心會弄壞其它的 plugin 提供的規則,不過如果有規則互相衝突的話不知道會怎麼樣,有興趣的人可以自己試試

這是這系列主要的技術文章的最後一篇了


上一篇
Day 29: eslint
系列文
前端建置工具完全手冊30

尚未有邦友留言

立即登入留言