實作一個 Redocly CLI lint 規則
將一個用白話文寫成的檢查(例如「每個陣列 schema 都必須宣告 items」)轉換成一個可運作且經過測試的 Redocly lint 規則。
階梯法。 Redocly 提供了三種執行檢查的方式。第一種的維護成本最低。
- 內建規則 —— 由 內建規則 中之一執行檢查。
- 可配置規則 —— 在
redocly.yaml中,由一個或多個rule/*條目使用斷言來執行檢查。 - 自訂插件 —— 使用訪問者模式 (visitor pattern) 實作你自己的規則的 JavaScript 程式碼。最後才考慮使用這個階層。
一次只處理一個檢查。 每個檢查都從階梯 1 開始。 只有當當前階層無法執行檢查時,才往下一個階層移動。 所有語法都參考官方文件或食譜。 相信文件,不要憑記憶。 使用現代語法,例如使用 ESM 而非 CommonJS。如果專案有自己的風格,請遵循專案風格。
1. 分類
針對每個檢查,寫下三件事:它查看的節點、必須為真的條件,以及違規情況的樣貌。 將每個檢查分開分類。不同的檢查可能落在不同的階層上。
從 inspect-node-types 命令取得節點名稱,而不是憑記憶。
類型樹在每個規格和每個版本中都不同。
它印出的名稱是可配置規則的 subject.type,以及插件的訪問者鍵。
| 想要了解 | 執行指令 |
|---|---|
| 某個位置上的類型 | --pointer='#/paths/~1orders' |
| 每個節點,或某個類型的所有節點 | 不加 flag,或 --type=Schema |
| 描述中使用了哪些類型 | --summary |
| 一個類型可以放在哪裡 | --type=<Type> --parents |
| 一個節點的所有祖先節點 | --pointer=<pointer> --parents |
最後兩行賦予規則其作用域。
當一個類型存在於多個父節點之下時,規則需要一個 where 門控。
沒有這個門控,規則也會回報你不想要的分支。 這個指令只顯示眼前的描述。它並未顯示規格允許的所有情況。 當指令沒有印出某個分支時,表示該分支未被證實,並非不存在。 這個指令不會縮短步驟 3。它給你類型名稱,但它不會告訴你你的斷言是否正確表達了檢查。
對每個檢查依序嘗試階梯:
- 搜尋內建規則列表中的相關概念。
路徑中的尾部斜線對應
no-path-trailing-slash。 當配置已經 extends 一個開啟了該內建規則的規則集時,告知使用者。不要再次添加該規則。 當內建規則的描述符合檢查時,就使用它,即使它比你要取代的規則回報更多內容。 內建規則通常是同一概念更好的版本,因此它額外的發現代表檢查被正確地執行。 當你手動編寫更狹窄的行為時,你等於是用一個需要你維護的規則換成了需要你自己維護的程式碼。 僅當內建規則檢查的是不同事物時,才往下一個階層移動。 回報你採用了哪個內建規則,以及它額外增加了什麼。 - 確保內建規則支援專案要 lint 的規格版本。
有些內建規則只支援一個版本。
no-invalid-media-type-examples只支援 OpenAPI 3.x,因此它不會對 Swagger 2.0 的檢查進行強制執行。 當一個啟用的規則無法應用於某份文件時,Redocly 會印出Unused rules found in redocly.yaml。 修正那一行。它不是雜訊。 - 嘗試使用斷言和
where門控。請參閱下方的「可配置規則」。 一個檢查可能需要多個rule/*條目,每個案例或位置各一個。這仍然比寫一個插件好。 - 僅當可配置規則(即使組合使用)也無法表達檢查時,才編寫插件。 首先,嘗試下面的三個調節鈕。 插件可以執行任何檢查,但它是程式碼。它會增加複雜性、維護成本以及自身的錯誤。
- 僅當沒有任何可配置規則能填補真正的功能缺口時,才建議使用者提交 issue。
當每個檢查都分配到一個階層後,分類就完成了。 為每個檢查提供一個解決方案:能運作的最上層階層。 只回報使用者必須採取行動的事項:描述與實作之間的不匹配、未經證實的規則,或可能朝其他方向發展的決定。 在對話中回報。切勿寫摘要檔案。 配置記錄了其餘部分,因此不要逐項說明每個檢查的階層。
從其他 linter 遷移
- 遷移有效的規則集,而不僅僅是寫在檔案中的規則。
一個
extends或繼承的基礎規則集,會以預設的嚴重程度開啟更多規則。這些規則中的每一個也都是一個檢查。 在開始之前,先解析該基礎規則集並計算規則數量。這樣你才能知道工作的真正規模。 - 從兩個地方獲取每個檢查:規則的描述以及其實作。 切勿根據你對規則名稱的記憶來獲取。 當兩者不一致時,使用描述。描述說明了作者想要的行為,而實作可能包含錯誤。 告知使用者這個差異。
- 跳過嚴重程度為
off的規則。不要遷移它,甚至不要作為一個已停用的條目。 當啟用的規則完成後,將每個跳過的規則列在一行。然後使用者可以自行決定。 - 對於 Spectral,請先閱讀遷移指南中的對照表。 許多 Spectral 的核心規則對應到一個內建規則。一個空白儲存格表示你必須往下走階梯。
- 當你從原始碼複製一個函式時,你同時也將它的 bug 複製到一個更嚴格的宿主中。
Redocly 會在規則拋出錯誤時停止整個文件的處理,但原始 linter 可能只會停止一個規則。
一個未經防護的比對,例如對一個可能比對失敗的值執行
v.match(/…/)[0],會移除該檔案中的所有發現。 為你複製的程式碼加入一個防護措施。然後對一個包含困難形狀的文件進行 lint。
2. 配置
寫入使用者的 redocly.yaml。請使用這個確切的名稱,不要加前導點。
當檔案存在時,先讀取它,並保留現有的 extends 和規則。
將每個規則設為 error,除非使用者要求其他級別。
當你進行遷移時,保留原始嚴重程度,並選擇最接近的 Redocly 級別。\n只有 error(會導致執行失敗)、warn 和 off 存在,因此 info 和 hint 會變成 warn。
從配置中引用你建立的每個檔案,並在 lint 執行中對其進行測試。不要留下死檔案。
沒有 extends 的配置不會繼承任何東西 —— 不會繼承 recommended,甚至不會繼承 struct。
只有當沒有設定檔案存在時,才會套用預設的基礎規則集。
因此,一個 redocly.yaml 只會執行它明確指定的規則。
內建規則階層只在兩種情況下存在:extends 指名了一個規則集,或者配置自己列出了每個內建規則。
如果兩者都沒有,那麼一份違反了規格三次的文件會通過 lint 而不產生任何錯誤。
連接每一個規則。
plugins: 中的一個插件,如果其規則沒有出現在 rules: 中,則不會產生任何作用,而且 Redocly 也不會發出警告。
在撰寫好配置後,列出必須啟用的規則 ID。
確認每個 ID 都存在於 rules: 或一個 spec 作用域區塊中。
當所有規則都就位後,將你的列表與預先定義的規則集進行比較:minimal、recommended、recommended-strict 和 spec。
- 當你的規則與某個規則集很接近時,就擴展它。 僅保留差異作為覆寫:其他嚴重程度、要關閉的規則,以及規則集中沒有的檢查。
- 一個規則集也可能會開啟使用者沒有要求的規則。
閱讀它增加了什麼,並覆寫每個與你撰寫的規則相矛盾或重複的規則。
一個規則集開啟了原始設定為
off的檢查,這是一個需要修正的錯誤,而不是一項好處。 - 當覆寫的規則數量與規則數量一樣多或更多時,保留純規則列表,不要使用
extends。
3. 驗證
一個規則在通過實際執行使其回報之前,是未經證實的。
使用 npx redocly lint <api-file> 從兩個方向驗證每個規則:
- 一個違規的測試案例:規則會回報,並且在正確的位置;
- 一個合規的測試案例:執行過程保持靜默。
誤報是常見的失敗模式,因此只通過違規測試案例的規則仍是未經證實的。 編寫違規測試案例時,要將此規則與其相鄰的規則區分開來:也就是你正在取代的規則,或配置中與其相鄰的規則。 有兩個錯誤需要這種區分:
- 在違規測試案例中放入一個虛假值。
一個必須拒絕空白
description的規則,必須在description: ''時回報。 如果你只在合規測試案例中放入'',你什麼也沒證明,因為存在性斷言會接受它。 那麼規則看起來是正確的,但問題仍然存在。 - 一個取代了兩個來源檢查的內建規則,需要一個只違反第二個檢查的測試案例。
no-unsafe-markdown會回報<script>,但從不回報eval(。 一個同時包含兩者的測試案例,會讓這個內建規則獲得一個它其實沒有執行的檢查的功勞。
為了保持輸出乾淨,使用 --config 傳遞一個小的臨時配置。
完成後刪除該配置和測試案例。
你提供的每個程式碼片段都必須是正確的:它必須能被解析為 YAML,並且行為必須如你所聲稱。
當每個檢查都指定了一個已回報的違規測試案例和一個保持靜默的合規測試案例時,你就完成了。
在你的報告中指定這些測試案例的名稱。
一個數字不是證據。
在專案自己的檔案上通過乾淨的執行也不是證據:它只顯示了在那裡沒有回報,而這對於一個正確的規則和一個失效的規則都是成立的。
當你只驗證了有風險的規則而假設其他規則正確時,交付的結果是未經證實的。請說明這一點,並指出每個未經證實的規則。
一個合規測試案例只能證實它內部包含的形狀。
你沒有嘗試過的規格版本,或者 --parents 顯示但你沒有測試的位置,都仍然是未經測試的。請回報這一點。
如果無法進行實際執行,請將規則標記為未驗證。不要聲稱它可以運作。
當所有請求的檢查都已存在於配置或跳過清單中,並且專案中只包含你建立的配置和插件檔案時,你也算完成。
可配置規則
文件:可配置規則。 實作範例:cookbook/configurable-rules。
一個規則是在 rules: 下的一個條目,其名稱以 rule/ 開頭:
rules:
rule/my-check:
subject:
type: Schema # 來自文件類型列表的節點類型
property: title # 可選
assertions:
defined: true
where: [] # 可選門控
message: 自訂問題文字。
severity: error
普通的 rules 應用於專案要 lint 的每份文件。預設情況下,規則應放在此處。
僅在檢查需要存在於某個規格版本中的東西時(例如 OAS 3.0 中的 nullable),或者使用者要求特定作用域時,才將規則移到一個 spec 作用域的金鑰下。
這些金鑰是 oas2Rules、oas3_0Rules、oas3_1Rules、oas3_2Rules、async2Rules、async3Rules、arazzo1Rules 和 overlay1Rules。
一個在金鑰內的規則不會在其他版本上執行,因此一個專案需要在兩個版本上應用的檢查,需要一個兩個版本都能觸及的條目。
相反地,在插件內部,請在插件的 rules 映射中對插件規則進行作用域設定。
斷言測試的內容
有兩個事實導致了大多數的誤報和靜默遺漏。
缺失值會通過斷言。
幾乎每個斷言在它所檢查的值缺失時都會成立。
assertions: { const: array } 加上 subject.property: type 會匹配類型為 array 的 schema,也會匹配每個沒有 type 的 schema。
因此,當一個 where 條目測試一個屬性值時,如果該屬性可能缺失,請在值斷言旁邊加上 defined: true。
存在不等於值。
defined、required、requireAny、disallowed 和 mutuallyExclusive 測試一個鍵是否存在。
它們對內容不做任何斷言,因此 description 的 defined: true 對 description: '' 也會通過。
請改為斷言其值:nonEmpty: true 會拒絕空字串,而 const、enum 和 pattern 則測試內容。
這是實務中最常見的缺陷。
它在審查中看起來是對的,只有一個包含 '' 的違規測試案例才能揭露它。
這兩個事實的兩個結果:
const: <value>也是表達「這個欄位必須是虛假值」的方式,因為該斷言對那個值和一個缺失的欄位都成立。requireAny只計算鍵的數量。 「這些欄位中至少有一個,並且不能為空」沒有對應的斷言形式。這個檢查是一個真正的插件案例。
斷言檢查的對象
property: name—— 該欄位在主題節點上的值。property: [name, url, email]—— 每個列出的欄位各自獨立。 每個失敗的欄位都會變成自己的問題,並帶有自己的 JSON 指標,例如#/info/contact/name。property不存在 —— 主題節點的鍵。 例如,subject: { type: Paths }搭配assertions: { notPattern: /.+\/$/ }會在路徑字串本身(鍵)上執行。
瞄準主題,然後進行門控
三個調節鈕用來瞄準規則。請按此順序解析它們。
1. 主題:擁有你所斷言內容的節點。
- 關於一個映射所持有的鍵的檢查,目標鎖定在該映射類型本身。
回應、標頭、schema 屬性和路徑的映射各自是它們自己的類型。
因此,「這個映射必須持有那個鍵」是在一個主題上的一個
required。不要檢查父節點。 - 關於一個值的檢查,目標鎖定在持有該欄位的節點,並加上
property。 type: any搭配一個property會在所有出現該屬性的地方檢查它。- 請使用
inspect-node-types在目標規格版本的文件上確認類型名稱。 一個命名的類型可能存在於一個版本而不存在於另一個版本。HeadersMap是 OpenAPI 3 中的一個類型。在 OpenAPI 2 中,回應的headers映射是匿名的,沒有類型名稱,因此oas2Rules下的HeadersMap主題永遠不會回報。 Redocly 會對未解析的規則 ID 發出警告,但不會對未解析的subject.type發出警告,因此錯誤的類型會給你一個從不回報的規則。
[已截斷以利翻譯]