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