1. 程式人生 > 其它 >給 eslint 寫一個外掛

給 eslint 寫一個外掛

eslint 是很有名的 linter,地球上每一個JavaScript程式設計師都應該知道。

linter 是一種程式碼靜態分析工具,它可以幫你找到程式碼中可能存在的錯誤與 bug,也能找出程式碼風格的問題,不過因為只是靜態分析,對js這種動態型別的語言所能做的就比較有限了,畢竟在js中,變數的型別如果不執行就不容易知道,有些錯誤就不那麼容易被找出來,雖然如此,能做的檢查還是很多了。

安裝

安裝 eslint 本身只需要安裝 eslint 本身就夠了,而且 eslint 自帶一些規則,不安裝任何外掛就做到基本的檢查,但一般還是需要安裝一些外掛。

$ yarn add --dev eslint

eslint 除了可以安裝外掛外,還可以安裝另外兩個東西,總共有 3 種:

  • plugin:eslint 的外掛可以幫 eslint 增加規則,另外也可以通過配置檔案讓程式設計師新增自己的規則,外掛可以提供一份預設的推薦配置
  • config:可以重複使用的規則配置檔案,比較有名的是 standard 和 airbnb 的規則,配置檔案有可能會有依賴的外掛,需要自己去安裝
  • parser:用來擴充 eslint 可以處理的語法,有用 babel 轉換 js 的 babel-eslint ,讓 eslint 可以處理實驗性的語法;@typescript-eslint/parser 可以讓 eslint 處理 Typescript;還有vue-eslint-parser 用來處理vue程式碼。

使用

雖然安裝很簡單,但不對 eslint 進行配置是什麼都不能做的,所以還要提供一個基本的配置,而 eslint 提供一個簡單的初始化命令,通過執行這個命令並回答幾個問題,eslint 就會產生一個基本的配置:

$ yarn eslint --init

eslint 的配置檔案可以是 js、json 或 yml 的格式,在這裡我們用 js 格式,檔案要取名為 .eslintrc.js,這裡就用基本的配置,即只用 eslint:recommended 這組設定,如果有其它的外掛也像這樣進行基本的設定:

module.exports = {
  extends: 'eslint:recommended',
}

eslint 的配置檔案有幾個基本的配置項,在這裡也順便說幾個我常用的 config 的外掛,首先是 config 的部分:

  • eslint-config-standard:很有名的配置,它還需要另外安裝 4 個外掛
  • eslint-config-prettier:用來關掉排版相關配置項的配置檔案,因為要交給 prettier 處理,關掉就不會引發衝突了。

我還沒有列出 standard 所相依的外掛:

  • eslint-plugin-simple-import-sort:能夠自動排序 import 的一個外掛
  • eslint-plugin-eslint-comments:用來檢查 eslint 的特殊註解的一個外掛,eslint 可以用特殊的註解開關規則,這些等下會講到,這個外掛的用途是不允許關閉了規則後不再開啟,以及關掉所有規則。

把上面的內容都寫到配置檔案中應該是這樣:

module.exports = {
  extends: [
    'standard',
    // 加上 prettier 的配置,關掉部份樣式檢查,順序很重要
    'prettier',
    'prettier/standard',
    // 如果是外掛提供的配置項需要以 `plugin:` 開始
    'plugin:eslint-comments/recommended',
  ],
  // 額外的規則,這裡也可以決定是否要關掉某些規則
  rules: {
    // 設定 plugin `eslint-plugin-simple-import-sort` 的 `sort` 規則是 `error`,也就是不符合時是會報錯的
    // 另外還可以設定為 `warn` 只警告,或是 `off` 關掉
    // 有的規則也有選項,這是就要用 ['error', {<options>}] 這種像 babel 的格式了
    'simple-import-sort/sort': 'error',
  },
  // 配置指定的環境,這會影響到判斷哪些是全域性變數
  env: {
    browser: true
  },
  // 設定 eslint 自己的 parser 用的是哪一版本的 js ,一般設定為 eslint --init 就行了
  parserOptions: {
    ecmaVersion: 12,
  },
}

運作原理

eslint 跟 babel 很相似,都是先把檔案轉成 AST,如果想檢視 eslint 轉出來的 AST ,可以到AST Explorer選擇espree解析器,這是 eslint 內建的解析器,它和 babel 的解析器不太一樣,應該說是 babel 的解析器和別人不一樣才對,ECMAScript 定義了一套 js 的 AST 該怎樣定義的規則,是 babel 和別人不同,另外 eslint 的解析器需要很詳細的資訊,不能只有程式碼的同步而已,而這樣才能做好 lint 的工作。

它的運作方式也像 babel 一樣,讓 plugin XML visitor 對特定的節點進行檢查,如果發現有問題就通過它的 API 來報告,也可以通過它的 API 提供修正的程式。

https://www.houdianzi.com/ logo設計公司

寫一個自己的 eslint 外掛

接下來寫一個 eslint 外掛,雖說是寫外掛,但實際上寫的是 eslint 的規則,假設我們希望 js 的物件是這樣的(比如 vue 的 object):

export default {
  name: 'Foo',

  props: {},

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

像上面這樣中間都有個空行,可以用兩種很簡單的方法來判斷是不是 vue 的物件:

  • 在 export default 之後
  • 包含在 Vue.extend 中

eslint 的規則大致分為meta 和 create 兩個部分:

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

與 babel 外掛很像,第一步是先開啟AST Explorer,選 eslint 用的解析器 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 以上
            // 也就是中間有兩個以上的空行
            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 的外掛需要照著 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')
}

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

eslint 會在最後一次把修復加上去,然後再跑一次所有規則,如果還是有可以修復的問題就再跑一次,直到沒有可以自動修復的問題為止,所以也不用擔心會破壞其他外掛所提供的規則。不過如果出現了規則互相沖突會怎樣呢,如果有興趣的話可以自己來試試。