則:對象字面量重復鍵名檢測的完整實戰(zhàn)指南)
Rome 的 noDuplicateObjectKeys 規(guī)則對象字面量重復鍵名檢測的完整實戰(zhàn)指南【免費下載鏈接】toolsUnified developer tools for JavaScript, TypeScript, and the web項目地址: https://gitcode.com/gh_mirrors/to/tools本指南圍繞 Rome 工具鏈Unified developer tools for JavaScript, TypeScript, and the web中內置的noDuplicateObjectKeys校驗規(guī)則展開它用于阻止對象字面量中出現多個同名屬性聲明屬于suspicious可疑分類且被 Rome 官方推薦開啟。閱讀本文后你將掌握該規(guī)則觸發(fā)與放行的完整邊界、診斷與自動修復的行為細節(jié)、其底層靜態(tài)分析實現原理以及如何在實際項目中配置與按需禁用。規(guī)則概述noDuplicateObjectKeys自v11.0.0起加入 Rome是一個被官方標記為recommended: true推薦啟用的 lint 規(guī)則完整規(guī)則標識為lint/suspicious/noDuplicateObjectKeys。其官方文檔位于 website/src/pages/lint/rules/noDuplicateObjectKeys.md對應的源碼實現在 crates/rome_js_analyze/src/analyzers/suspicious/no_duplicate_object_keys.rs。該規(guī)則解決的核心問題源于 JavaScript 對象語義本身當同一個對象字面量中某個屬性名被定義了多次getter 與 setter 配對這一合法例外除外只有最后一次定義會真正進入對象之前的定義會被靜默忽略。這幾乎總是一個筆誤或合并沖突遺留且不會產生任何運行時錯誤——因此需要靜態(tài)檢查在編碼階段將其攔截。為什么重復鍵名是危險的考慮以下代碼const config { retries: 3, timeout: 5000, retries: 5, // 靜默覆蓋前面的 3 };運行時config.retries的值是5第一行retries: 3被 JavaScript 引擎靜默丟棄。這種覆蓋行為不會拋出異常也不會產生警告排錯成本極高常見于多分支合并、復制粘貼、配置文件被手改后若覆蓋順序依賴代碼書寫位置重構時調整成員順序甚至會改變程序行為。noDuplicateObjectKeys正是要在提交代碼之前把這些注定無效的聲明暴露出來。無效示例與診斷輸出詳解場景一普通屬性重復const obj { a: 1, a: 2, }Rome 會在第 2 行被覆蓋的那個屬性給出診斷。從倉庫中的測試快照 invalid.jsonc.snap 可以看到真實終端輸出此處以更緊湊的形式呈現invalid.jsonc:1:4 lint/suspicious/noDuplicateObjectKeys FIXABLE ━━━━━━━━━━━━ ? This property value named a is later overwritten by an object member with the same name. 1 │ ({ a: 1, a: 2 }); │ ^^^^ ? Overwritten with this value. 1 │ ({ a: 1, a: 2 }); │ ^^^^ ? If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored. ? Suggested fix: Remove this property value named a 1 │ ({·a:·1,·a:·2·}); │ ------診斷的四個層次值得注意主消息明確指出被覆蓋的成員高亮的是a: 1而非a: 2因為被覆蓋者才是問題所在詳情Overwritten with this value定位到真正生效的成員a: 2讓開發(fā)者一眼看到誰贏了note復述規(guī)則的設計動機重復定義的覆蓋語義Suggested fix給出自動修復刪除被覆蓋的那個屬性。場景二setter 與普通屬性沖突const obj { set a(v) {}, a: 2, }此時診斷消息變?yōu)門his setter named a is later overwritten by an object member with the same name.自動修復為Remove this setter named a。源碼中通過MemberDefinition::fmtno_duplicate_object_keys.rs根據成員類型動態(tài)生成 getter / setter / method / property value / shorthand property 等描述詞再拼接named 屬性名。更多無效組合來自測試用例倉庫的 invalid.jsonc 共收錄了 13 個無效用例覆蓋全部沖突形態(tài)[ ({ a: 1, a: 2 });, ({ a: 1, a: 2, a: 3 });, // 三重重復報告前兩處 ({ : 1, : 2 });, // 空字符串鍵名 ({ z: 1, z: 2 });, ({ get a() {}, get a() {} });, // 雙 getter ({ set a(v) {}, set a(v) {} });, // 雙 setter ({ a: 1, get a() {} });, // 屬性 getter ({ a: 1, set a(v) {} });, // 屬性 setter ({ get a() {}, a: 1 });, ({ set a(v) {}, a: 1 });, ({ a: 1, get a() {}, set a(v) {} });, // 屬性被 getter/setter 組合覆蓋 ({ get a() {}, a: 1, set a(v) {} });, ({ get a() {}, set a(v) {}, a: 1 }); // gettersetter 之后又被普通屬性覆蓋 ]特別值得注意的是({ a: 1, a: 2, a: 3 })快照顯示它會同時產生兩條診斷a: 1被a: 3覆蓋、a: 2也被a: 3覆蓋而不是只報告一處——這正體現了找出所有注定無效的聲明的設計意圖。有效示例什么情況下允許同名原文檔給出了兩類合法代碼const obj { a: 1, b: 2, }const obj { get a() { return 1; }, set a(v) {}, }第二例是規(guī)則明確豁免的唯一場景getter 與 setter 恰好各一個時構成合法的存取器accessor配對二者共同描述同一個屬性不應報警告。這一點在源碼的DefinedProperty::extend_with中有精確體現見下文原理章節(jié)。此外valid.jsonc 中的 17 個用例進一步劃定了規(guī)則的檢測邊界[ // 數字字面量寫法不同當前版本未做規(guī)范化比較視為合法 ({ 0x1: 1, 1: 2 });, ({ 012: 1, 10: 2 });, ({ 0b1: 1, 1: 2 });, ({ 0o1: 1, 1: 2 });, ({ 1n: 1, 1: 2 });, ({ 1_0: 1, 10: 2 });, // 計算屬性即使是簡單字符串字面量不做求值分析 ({ a: 1, [a]: 1 });, // 名稱確實不同 ({ a: 1, b: 1 });, ({ : 1, : 1 });, ({ 012: 1, 12: 1 });, ({ 1_0: 1, 1: 1 });, // 計算屬性名稱未知無法靜態(tài)判定 ({ a: 1, [a]: 1 });, ({ [a]: 1, [a]: 1 });, // getter/setter 配對 ({ get a() {}, set a(v) {} });, // spread 不是單一屬性聲明 ({ a: 1, ...a });, ({ a: 1, b: { a: 1, b: 1 } });, // 解構模式不在本規(guī)則范圍 var { a, a } obj; ]測試文件中的注釋明確說明了這些取舍的意圖ESLint already catches properties keyed with different-formatted number literals, we havent implemented it yet.——0x1與1、012與10等數字字面量在語義上指向同一鍵但 Rome 當前版本尚未實現字面量規(guī)范化比較故暫不告警This particular simple computed property case with just a string literal would be easy to catch, but we dont want to open Pandoras static analysis box so we have to draw a line somewhere——對計算屬性[a]這類靜態(tài)可解的情況規(guī)則刻意劃出邊界、不做求值避免引入不可控的靜態(tài)分析復雜度var { a, a } obj屬于解構模式而非對象字面量超出本規(guī)則范圍。這些用例同時是邊界行為的可驗證依據運行cargo test -p rome_js_analyze即可通過 spec_tests.rs 復現上述全部診斷快照。源碼級實現原理該規(guī)則的實現位于 no_duplicate_object_keys.rs整體可拆解為四個部分。1. 規(guī)則聲明declare_rule 宏declare_rule! { pub(crate) NoDuplicateObjectKeys { version: 11.0.0, name: noDuplicateObjectKeys, recommended: true, } }宏內的文檔注釋與 website 上的規(guī)則文檔 內容保持一致文檔由規(guī)則注釋生成/同步。規(guī)則隨后注冊進 suspicious.rs 的declare_category!中。2. 成員類型的統(tǒng)一抽象規(guī)則把對象字面量成員抽象為MemberDefinition枚舉覆蓋五種成員形態(tài)源碼 L63-L70enum MemberDefinition { Getter(JsGetterObjectMember), Setter(JsSetterObjectMember), Method(JsMethodObjectMember), Property(JsPropertyObjectMember), ShorthandProperty(JsShorthandPropertyObjectMember), }其name()方法負責提取每個成員的鍵名TokenText普通屬性、方法、getter、setter 都通過as_js_literal_member_name()?.name()取字面量成員名簡寫屬性則直接取value_token()的文本。TryFromAnyJsObjectMember轉換中JsSpread展開運算符與JsBogusMember會被排除——它們不是單一屬性聲明這正是({ a: 1, ...a })被視為合法用例的原因。3. 沖突檢測倒序遍歷 HashMaprun()是分析核心源碼 L215-L251let mut defined_properties: HashMapTokenText, DefinedProperty HashMap::new(); for member_definition in node.members().into_iter().flatten() .filter_map(|member| MemberDefinition::try_from(member).ok()) // 注意從最后一個成員向第一個遍歷 // 這樣被高亮為問題的是“被覆蓋”的屬性而不是“生效”的那個 .rev() { // 嘗試把當前成員并入已有定義沖突則產生 signal match defined_properties.remove(member_name) { ... } }要點有兩個倒序迭代先處理最后一個成員再向前回溯。這樣在a: 1, a: 2中a: 2先進入HashMap隨后a: 1因與已有定義沖突而成為被報告的對象——與診斷中高亮被覆蓋者的設計完全一致DefinedProperty狀態(tài)機每個鍵名在哈希表中維護一個已確認生效的定義狀態(tài)共四態(tài)enum DefinedProperty { Get(TextRange), // 只有 getter Set(TextRange), // 只有 setter GetSet(TextRange, TextRange), // getter setter 配對 Value(TextRange), // 普通值/方法/簡寫屬性 }extend_with()只允許一種合并操作源碼 L186-L207已存在Set時遇到Getter或已存在Get時遇到Setter合并為GetSet其余任何組合都會產生PropertyConflict。這從類型層面精確落實了getter 配 setter 合法、其余一律沖突的規(guī)則語義。4. 診斷與自動修復diagnostic()根據DefinedProperty的具體形態(tài)Get/Set/Value/GetSet生成不同的詳情文案Overwritten with this getter/setter/value.。對于GetSet狀態(tài)還需通過TextRange的順序判斷沖突成員是 getter 還是 setter以指向正確的覆蓋來源action()自動修復通過batch.remove_js_object_member(member_definition.node())刪除被覆蓋成員。值得注意的是其Applicability被標記為MaybeIncorrect可能不正確源碼注釋給出了原因The property initialization could contain side effects——被刪除的a: 1若帶有副作用如a: fn()刪除后副作用也隨之消失因此 Rome 不會將其標記為絕對安全的修復IDE 或 CI 中應用該修復時需要人工確認。在配置中啟用、調整與禁用由于該規(guī)則recommended: true使用默認rome.json倉庫根目錄即有一份示例 rome.json時它已隨 recommended 組自動生效無需顯式聲明。需要單獨控制時可在rome.json的linter.rules下配置底層對應的配置字段定義在 crates/rome_service/src/configuration/linter/rules.rs#L3425no_duplicate_object_keys: OptionRuleConfigurationJSON 解析邏輯見 crates/rome_service/src/configuration/parse/json/rules.rs#L3672-L3682{ linter: { rules: { suspicious: { noDuplicateObjectKeys: error } } } }將該規(guī)則設為off即可關閉也可以使用// rome-ignore lint/suspicious/noDuplicateObjectKeys: 原因行內注釋對單行做豁免。關于禁用規(guī)則的完整方法與規(guī)則級options如error/warn/off取值與 recommended 分組行為參見 linter/index.mdx 中的 Disable a lint rule 與 Rule options 兩節(jié)這也是原文檔 Related links 所指向的內容。使用建議與局限說明保持 recommended 默認開啟重復鍵名幾乎沒有正當使用場景getter/setter 配對除外屬于純收益規(guī)則建議在 CI 中直接以error級別攔截留意自動修復的副作用當被覆蓋屬性的初始化表達式含函數調用等副作用時Rome 會將修復標記為MaybeIncorrect應用前應人工核對已知檢測盲區(qū)不同格式的數字字面量0x1vs1、1_0vs10與字符串字面量計算屬性[a]在當前版本基于倉庫源碼尚未被檢測若項目對此類寫法敏感可輔以人工 review 或等待上游增強作用范圍規(guī)則只針對對象字面量JsObjectExpression解構模式var { a, a } obj不在其職責內。小結noDuplicateObjectKeys是一個小而精的靜態(tài)分析規(guī)則它把 JavaScript 中同名屬性靜默覆蓋這一運行時陷阱提前到編碼期暴露通過倒序遍歷與Get/Set/GetSet/Value狀態(tài)機精確區(qū)分gettersetter 合法配對與真正的重復聲明并附帶帶有副作用提示的自動修復。理解其實現no_duplicate_object_keys.rs與測試邊界invalid.jsonc / valid.jsonc既能幫你用好這一規(guī)則也能為閱讀 Rome 其它 suspicious 類規(guī)則的源碼提供參考范式。【免費下載鏈接】toolsUnified developer tools for JavaScript, TypeScript, and the web項目地址: https://gitcode.com/gh_mirrors/to/tools創(chuàng)作聲明:本文部分內容由AI輔助生成(AIGC),僅供參考