Rome 的 noDuplicateObjectKeys 规则:对象字面量重复键名检测的完整实战指南
2026/9/20 22:53:04 网站建设 项目流程

Rome 的 noDuplicateObjectKeys 规则:对象字面量重复键名检测的完整实战指南

【免费下载链接】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校验规则展开:它用于阻止对象字面量中出现多个同名属性声明,属于suspicious(可疑)分类且被 Rome 官方推荐开启。阅读本文后,你将掌握该规则触发与放行的完整边界、诊断与自动修复的行为细节、其底层静态分析实现原理,以及如何在实际项目中配置与按需禁用。

规则概述

noDuplicateObjectKeysv11.0.0起加入 Rome,是一个被官方标记为recommended: true(推荐启用)的 lint 规则,完整规则标识为lint/suspicious/noDuplicateObjectKeys。其官方文档位于 website/src/pages/lint/rules/noDuplicateObjectKeys.md,对应的源码实现在 crates/rome_js_analyze/src/analyzers/suspicious/no_duplicate_object_keys.rs。

该规则解决的核心问题源于 JavaScript 对象语义本身:当同一个对象字面量中,某个属性名被定义了多次(getter 与 setter 配对这一合法例外除外),只有最后一次定义会真正进入对象,之前的定义会被静默忽略。这几乎总是一个笔误或合并冲突遗留,且不会产生任何运行时错误——因此需要静态检查在编码阶段将其拦截。

为什么重复键名是危险的

考虑以下代码:

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·}); │ ------

诊断的四个层次值得注意:

  1. 主消息明确指出"被覆盖的成员"(高亮的是a: 1而非a: 2),因为被覆盖者才是问题所在;
  2. 详情(Overwritten with this value)定位到真正生效的成员a: 2,让开发者一眼看到"谁赢了";
  3. note复述规则的设计动机(重复定义的覆盖语义);
  4. Suggested fix给出自动修复:删除被覆盖的那个属性。

场景二:setter 与普通属性冲突

const obj = { set a(v) {}, a: 2, }

此时诊断消息变为This setter named a is later overwritten by an object member with the same name.,自动修复为Remove this setter named a。源码中通过MemberDefinition::fmt(no_duplicate_object_keys.rs)根据成员类型动态生成 "getter / setter / method / property value / shorthand property" 等描述词,再拼接named <属性名>

更多无效组合(来自测试用例)

仓库的 invalid.jsonc 共收录了 13 个无效用例,覆盖全部冲突形态:

[ "({ 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 });" // getter+setter 之后又被普通属性覆盖 ]

特别值得注意的是({ a: 1, a: 2, a: 3 }):快照显示它会同时产生两条诊断a: 1a: 3覆盖、a: 2也被a: 3覆盖),而不是只报告一处——这正体现了"找出所有注定无效的声明"的设计意图。

有效示例:什么情况下允许同名

原文档给出了两类合法代码:

const obj = { a: 1, b: 2, }
const obj = { get a() { return 1; }, set a(v) {}, }

第二例是规则明确豁免的唯一场景:getter 与 setter 恰好各一个时,构成合法的存取器(accessor)配对,二者共同描述同一个属性,不应报警告。这一点在源码的DefinedProperty::extend_with中有精确体现(见下文原理章节)。

此外,valid.jsonc 中的 17 个用例进一步划定了规则的"检测边界":

[ // 数字字面量写法不同(当前版本未做规范化比较,视为合法) "({ 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 });", // 计算属性名称未知,无法静态判定 "({ 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 } });", // 解构模式不在本规则范围 "var { a, a } = obj;" ]

测试文件中的注释明确说明了这些取舍的意图:

  • "ESLint already catches properties keyed with different-formatted number literals, we haven't implemented it yet."——0x1101210等数字字面量在语义上指向同一键,但 Rome 当前版本尚未实现字面量规范化比较,故暂不告警;
  • "This particular simple computed property case with just a string literal would be easy to catch, but we don't want to open Pandora's static analysis box so we have to draw a line somewhere"——对计算属性['a']这类静态可解的情况,规则刻意划出边界、不做求值,避免引入不可控的静态分析复杂度;
  • var { a, a } = obj属于解构模式而非对象字面量,超出本规则范围。

这些用例同时是边界行为的可验证依据:运行cargo test -p rome_js_analyze即可通过 spec_tests.rs 复现上述全部诊断快照。

源码级实现原理

该规则的实现位于 no_duplicate_object_keys.rs,整体可拆解为四个部分。

1. 规则声明(declare_rule 宏)

declare_rule! { pub(crate) NoDuplicateObjectKeys { version: "11.0.0", name: "noDuplicateObjectKeys", recommended: true, } }

宏内的文档注释与 website 上的规则文档 内容保持一致,文档由规则注释生成/同步。规则随后注册进 suspicious.rs 的declare_category!中。

2. 成员类型的统一抽象

规则把对象字面量成员抽象为MemberDefinition枚举,覆盖五种成员形态(源码 L63-L70):

enum MemberDefinition { Getter(JsGetterObjectMember), Setter(JsSetterObjectMember), Method(JsMethodObjectMember), Property(JsPropertyObjectMember), ShorthandProperty(JsShorthandPropertyObjectMember), }

name()方法负责提取每个成员的键名(TokenText):普通属性、方法、getter、setter 都通过as_js_literal_member_name()?.name()取字面量成员名,简写属性则直接取value_token()的文本。TryFrom<AnyJsObjectMember>转换中,JsSpread(展开运算符)与JsBogusMember会被排除——它们不是"单一属性声明",这正是({ a: 1, ...a })被视为合法用例的原因。

3. 冲突检测:倒序遍历 + HashMap

run()是分析核心(源码 L215-L251):

let mut defined_properties: HashMap<TokenText, 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状态机:每个键名在哈希表中维护一个"已确认生效"的定义状态,共四态:
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 合法、其余一律冲突"的规则语义。

4. 诊断与自动修复

  • diagnostic():根据DefinedProperty的具体形态(Get/Set/Value/GetSet)生成不同的详情文案("Overwritten with this getter/setter/value.")。对于GetSet状态,还需通过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 中应用该修复时需要人工确认。

在配置中启用、调整与禁用

由于该规则recommended: true,使用默认rome.json(仓库根目录即有一份示例 rome.json)时它已随 recommended 组自动生效,无需显式声明。需要单独控制时,可在rome.jsonlinter.rules下配置,底层对应的配置字段定义在 crates/rome_service/src/configuration/linter/rules.rs#L3425(no_duplicate_object_keys: Option<RuleConfiguration>),JSON 解析逻辑见 crates/rome_service/src/configuration/parse/json/rules.rs#L3672-L3682:

{ "linter": { "rules": { "suspicious": { "noDuplicateObjectKeys": "error" } } } }

将该规则设为"off"即可关闭;也可以使用// rome-ignore lint/suspicious/noDuplicateObjectKeys: <原因>行内注释对单行做豁免。关于禁用规则的完整方法与规则级options(如"error"/"warn"/"off"取值与 recommended 分组行为),参见 linter/index.mdx 中的 "Disable a lint rule" 与 "Rule options" 两节,这也是原文档 Related links 所指向的内容。

使用建议与局限说明

  1. 保持 recommended 默认开启:重复键名几乎没有正当使用场景(getter/setter 配对除外),属于纯收益规则,建议在 CI 中直接以error级别拦截;
  2. 留意自动修复的副作用:当被覆盖属性的初始化表达式含函数调用等副作用时,Rome 会将修复标记为MaybeIncorrect,应用前应人工核对;
  3. 已知检测盲区:不同格式的数字字面量(0x1vs11_0vs10)与字符串字面量计算属性(['a'])在当前版本(基于仓库源码)尚未被检测,若项目对此类写法敏感,可辅以人工 review 或等待上游增强;
  4. 作用范围:规则只针对对象字面量(JsObjectExpression),解构模式var { a, a } = obj不在其职责内。

小结

noDuplicateObjectKeys是一个小而精的静态分析规则:它把 JavaScript 中"同名属性静默覆盖"这一运行时陷阱提前到编码期暴露,通过倒序遍历与Get/Set/GetSet/Value状态机精确区分"getter+setter 合法配对"与"真正的重复声明",并附带带有副作用提示的自动修复。理解其实现(no_duplicate_object_keys.rs)与测试边界(invalid.jsonc / valid.jsonc),既能帮你用好这一规则,也能为阅读 Rome 其它 suspicious 类规则的源码提供参考范式。

【免费下载链接】toolsUnified developer tools for JavaScript, TypeScript, and the web项目地址: https://gitcode.com/gh_mirrors/to/tools

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询