
當 ESLint v9.0.0 發布時,它將包含數個規則作者需要注意的重大變更。這些變更是實作 語言外掛程式 工作的一部分,這將使 ESLint 能夠優先支援 JavaScript 以外的語言的檢查。我們不得不進行這些變更,因為 ESLint 從一開始就假設它只會用於檢查 JavaScript。因此,對於規則用來與原始碼互動的方法應該放在哪裡,並沒有經過太多思考。當我們為了語言外掛程式工作而重新審視 API 時,我們發現我們在僅限 JavaScript 的世界中能夠容忍的不一致性,在與語言無關的 ESLint 核心中將無法運作。
自動更新您的規則
在解釋 ESLint v9.0.0 中引入的所有變更之前,先了解可以使用 eslint-transforms
工具自動完成本文中描述的大部分變更,這會很有幫助。若要使用此工具,請先安裝它,然後執行 v9-rule-migration
轉換,如下所示:
# install the utility
npm install eslint-transforms -g
# apply the transform to one file
eslint-transforms v9-rule-migration rule.js
# apply the transform to all files in a directory
eslint-transforms v9-rule-migration rules/
並非所有變更都可以透過 eslint-tranforms
解決,因此以下是 API 變更的完整列表以及建議的解決方法。
context
方法變成屬性
當我們展望其他語言的規則所需的 API 時,我們決定將 context
上的一些方法轉換為屬性。下表中的方法僅傳回一些不會變更的資料,因此沒有理由不能改為屬性。
在 context 上已棄用 |
在 context 上的屬性 |
---|---|
context.getSourceCode() |
context.sourceCode |
context.getFilename() |
context.filename |
context.getPhysicalFilename() |
context.physicalFilename |
context.getCwd() |
context.cwd |
我們正在棄用這些方法,改用屬性(在 v8.40.0 中新增)。這些方法將在 v10.0.0 中移除(而非 v9.0.0),因為它們不會阻礙語言外掛程式的工作。以下範例確保使用正確的值:
module.exports = {
create(context) {
const sourceCode = context.sourceCode ?? context.getSourceCode();
const cwd = context.cwd ?? context.getCwd();
const filename = context.filename ?? context.getFilename();
const physicalFilename = context.physicalFilename ?? context.getPhysicalFilename();
return {
Program(node) {
// do something
}
}
}
};
從 context
到 SourceCode
規則作者的大部分重大變更包括將方法從傳遞到規則的 context
物件移至透過 context.sourceCode
(或已棄用的 context.getSourceCode()
;請參閱下文)檢索的 SourceCode
物件。context
與 SourceCode
的職責範圍在 ESLint 的生命週期中發生了變化:一開始,context
是規則需要使用的所有功能的所在地。一旦我們新增了 SourceCode
,我們就開始慢慢地向其中新增更多方法。最終結果是,有些方法存在於 context
上,有些方法存在於 SourceCode
上,而唯一的原因是什麼?方法新增的時間。
在與語言無關的 ESLint 核心中,我們需要重新定義這兩個物件的職責。展望未來,context
是規則需要與核心互動的功能的所在地,而 SourceCode
是規則需要與正在檢查的程式碼互動的功能的所在地。這允許相同的 context
物件用於檢查任何語言,並允許語言外掛程式定義自己的 SourceCode
類別,以提供特定於該語言的方法。
所有這些都是為了說明我們正在棄用 context
上的所有與程式碼相關的方法,並將它們移至 SourceCode
。下表顯示了 context
上的哪些欄位正在移至 SourceCode
。請注意,即使名稱變更,所有這些方法的方法簽名都保持不變。
在 context 上已棄用 |
SourceCode 上的替換項 |
---|---|
context.getSource() |
sourceCode.getText() |
context.getSourceLines() |
sourceCode.getLines() |
context.getAllComments() |
sourceCode.getAllComments() |
context.getNodeByRangeIndex() |
sourceCode.getNodeByRangeIndex() |
context.getComments() |
sourceCode.getCommentsBefore() 、sourceCode.getCommentsAfter() 、sourceCode.getCommentsInside() |
context.getCommentsBefore() |
sourceCode.getCommentsBefore() |
context.getCommentsAfter() |
sourceCode.getCommentsAfter() |
context.getCommentsInside() |
sourceCode.getCommentsInside() |
context.getJSDocComment() |
sourceCode.getJSDocComment() |
context.getFirstToken() |
sourceCode.getFirstToken() |
context.getFirstTokens() |
sourceCode.getFirstTokens() |
context.getLastToken() |
sourceCode.getLastToken() |
context.getLastTokens() |
sourceCode.getLastTokens() |
context.getTokenAfter() |
sourceCode.getTokenAfter() |
context.getTokenBefore() |
sourceCode.getTokenBefore() |
context.getTokenByRangeStart() |
sourceCode.getTokenByRangeStart() |
context.getTokens() |
sourceCode.getTokens() |
context.getTokensAfter() |
sourceCode.getTokensAfter() |
context.getTokensBefore() |
sourceCode.getTokensBefore() |
context.getTokensBetween() |
sourceCode.getTokensBetween() |
context.parserServices |
sourceCode.parserServices |
此表中列出的所有 context
方法都將在 ESLint v9.0.0 中移除,而 SourceCode
上的替換方法已經存在六年了,因此您應該可以順利切換到新方法。(是的,我們棄用了這些方法,然後完全忘記移除它們。)
除了此表中的方法外,還有其他幾個方法也在移動,但需要不同的方法簽名。
context.getScope()
context.getScope()
方法用於檢索目前遍歷節點的範圍物件。此方法一直有點奇怪,因為它使用 ESLint 的內部遍歷狀態來判斷要使用哪個節點作為參考點來檢索範圍物件。這表示它既有限制(因為您無法變更參考節點),又令人困惑(因為並不總是清楚參考的是哪個節點)。因此,我們正在棄用此方法,並將在 ESLint v9.0.0 中移除它。
我們引入了一個新的 SourceCode#getScope(node)
方法,需要您傳入參考節點。此方法已在 ESLint v8.37.0 中新增,因此在過去六個月中已經存在。為了獲得最佳相容性,您可以檢查是否存在此新方法,以判斷要使用哪一個:
module.exports = {
create(context) {
const sourceCode = context.sourceCode ?? context.getSourceCode();
return {
Program(node) {
const scope = sourceCode.getScope
? sourceCode.getScope(node)
: context.getScope();
// do something with scope
}
}
}
};
context.getAncestors()
context.getAncestors()
方法是 context
上的另一個方法,它使用內部遍歷狀態來傳回目前造訪節點的祖先。與 context.getScope()
類似,這表示該方法既有限制又不清楚。我們正在棄用此方法,並將在 v9.0.0 中移除它。替換方法是 SourceCode#getAncestors(node)
(在 v8.38.0 中新增),它需要您傳入要檢索其祖先的節點。以下範例檢查要使用的正確方法:
module.exports = {
create(context) {
const sourceCode = context.sourceCode ?? context.getSourceCode();
return {
Program(node) {
const ancestors = sourceCode.getAncestors
? sourceCode.getAncestors(node)
: context.getAncestors();
// do something with ancestors
}
}
}
};
context.getDeclaredVariables(node)
context.getDeclaredVariables(node)
傳回給定節點宣告的所有變數(例如在 let
陳述式中)。我們正在棄用此方法,並將在 v9.0.0 中移除它。我們正在用 SourceCode#getDeclaredVariables(node)
(在 v8.38.0 中新增)替換它,其運作方式完全相同。以下範例檢查要使用的正確方法:
module.exports = {
create(context) {
const sourceCode = context.sourceCode ?? context.getSourceCode();
return {
Program(node) {
const variables = sourceCode.getDeclaredVariables
? sourceCode.getDeclaredVariables(node)
: context.getDeclaredVariables(node);
// do something with variables
}
}
}
};
context.markVariableAsUsed(name)
context.markVariableAsUsed(name)
方法在目前範圍中尋找具有給定名稱的變數,並將其標記為已使用,使其不會在 no-unused-vars
規則中造成違規。此方法在幕後進行了相當多的魔術,因為它使用遍歷中目前造訪的節點來檢索範圍,然後在該範圍中搜尋具有給定名稱的變數。我們正在棄用此方法,並將在 v9.0.0 中移除它。替換方法是 SourceCode#markVariableAsUsed(name, node)
(在 v8.39.0 中新增),並且需要您傳入要搜尋範圍的參考節點。(範圍最終與呼叫 SourceCode#getScope(node)
的範圍相同。)以下範例檢查要使用的正確方法:
module.exports = {
create(context) {
const sourceCode = context.sourceCode ?? context.getSourceCode();
return {
Program(node) {
const result = sourceCode.markVariableAsUsed
? sourceCode.markVariableAsUsed("foo", node)
: context.markVariableAsUsed("foo");
if (result) {
// the variable was found and marked as used
}
}
}
}
};
CodePath#currentSegments
ESLint 規則的一個鮮為人知的功能是 程式碼路徑分析。ESLint 核心規則在多個規則中使用程式碼路徑分析,不僅驗證程式碼的外觀,還驗證邏輯流程。這是透過存取 CodePath
和 CodePathSegment
物件來完成的。在我們對語言外掛程式進行研究時,我們發現 CodePath#currentSegments
實際上代表了另一個在規則中公開的遍歷狀態。具體來說,CodePath#currentSegments
是一個陣列,它會在整個遍歷過程中隨著您遇到不同的程式碼路徑區段而成長和縮小。由於程式碼路徑分析是 JavaScript 獨有的,因此我們無法再讓核心追蹤此遍歷狀態。在評估了多個選項後,我們決定擁有一個既表示程式碼路徑資料又表示遍歷狀態的物件是不可取的,因此我們正在棄用 CodePath#currentSegments
,並將在 v9.0.0 中移除它。我們需要新增兩個新的事件處理常式 onUnreachableCodePathSegmentStart
和 onUnreachableCodePathSegmentEnd
,以允許存取相同的資料(這些已在 v8.49.0 中新增)。
若要重新建立此資料,您需要手動追蹤遍歷狀態,這可以使用以下程式碼完成:
module.exports = {
meta: {
// ...
},
create(context) {
// tracks the code path we are currently in
let currentCodePath;
// tracks the segments we've traversed in the current code path
let currentSegments;
// tracks all current segments for all open paths
const allCurrentSegments = [];
return {
onCodePathStart(codePath) {
currentCodePath = codePath;
allCurrentSegments.push(currentSegments);
currentSegments = new Set();
},
onCodePathEnd(codePath) {
currentCodePath = codePath.upper;
currentSegments = allCurrentSegments.pop();
},
onCodePathSegmentStart(segment) {
currentSegments.add(segment);
},
onCodePathSegmentEnd(segment) {
currentSegments.delete(segment);
},
onUnreachableCodePathSegmentStart(segment) {
currentSegments.add(segment);
},
onUnreachableCodePathSegmentEnd(segment) {
currentSegments.delete(segment);
}
};
}
};
我們已經在所有 ESLint 核心規則中進行了此變更,以驗證該方法是否按預期運作。
context
屬性:parserOptions
和 parserPath
即將移除
此外,context.parserOptions
和 context.parserPath
屬性已棄用,並將在 v10.0.0 中移除(而非 v9.0.0)。有一個新的 context.languageOptions
屬性,允許規則存取與 context.parserOptions
類似的資料。但一般而言,規則不應依賴 context.parserOptions
或 context.languageOptions
中的資訊來判斷它們應如何運作。
context.parserPath
屬性旨在允許規則透過 require()
檢索 ESLint 正在使用的解析器的實例。但是,新的扁平化設定系統不知道要載入的解析器模組的位置,因此我們無法提供此資料。此外,由於 JavaScript 生態系統正在轉向 ESM,因此從此屬性傳回的任何值都將無法與 import()
搭配使用。此屬性是在 ESLint 生命週期的早期新增的,我們通常建議規則不要嘗試在其中進一步解析 JavaScript 程式碼。如有必要,您可以使用 context.languageOptions.parser
來存取 ESLint 正在使用的解析器。
結論
ESLint 已經存在十年了,在這段時間裡,我們累積了一些 API 雜亂,我們需要清理這些雜亂,以便為 ESLint 的下一個十年做好準備。本文中描述的 API 變更是使 ESLint 能夠檢查非 JavaScript 語言,並更好地區分核心功能與語言特定功能的必要步驟。團隊花費了大量時間規劃 ESLint 生命週期的這個過渡點,我們希望這些變更只會對生態系統造成一點不便。如果您需要協助或對本文中討論的任何內容有疑問,請發起討論或造訪 Discord 與團隊交談。
更新(2024-06-06):新增關於 eslint-tranforms
的章節。