Rome noNonNullAssertion 规则深度解析:禁止 TypeScript 非空断言运算符 `!`

Rome noNonNullAssertion 规则深度解析:禁止 TypeScript 非空断言运算符 !

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

本指南以 Rome(JavaScript、TypeScript 与 Web 的统一开发工具链)官方 lint 规则文档 noNonNullAssertion.md 为核心,系统讲解 TypeScript 非空断言运算符 ! 为何被禁止、规则如何检测与自动修复,并结合 crates/rome_js_analyze 下的源码实现与测试用例(no_non_null_assertion.rsinvalid.ts.snap)展开原理级剖析。读完本文,你将掌握该规则的全部行为边界、修复策略、配置方法,以及与 noExtraNonNullAssertion 的协作关系,能在实际项目中安全落地这条推荐规则。

规则速览

属性
规则名称noNonNullAssertion
所属分组lint/style(代码风格组)
诊断分类lint/style/noNonNullAssertion(见 categories.rs
引入版本v11.0.0
是否推荐是(recommended,Rome 官方推荐启用)
适用语言TypeScript / TSX(针对 ! 后置运算符)
是否可自动修复是(有限场景,见下文"快速修复")

no_non_null_assertion.rs 的规则声明中,可以看到上述元数据被显式定义:

pub(crate) NoNonNullAssertion {
    version: "11.0.0",
    name: "noNonNullAssertion",
    recommended: true,
}

规则动机:为什么禁用 ! 运算符

TypeScript 的 ! 非空断言运算符(non-null assertion operator)用来向类型系统断言"某个表达式是非空值(not nullundefined)"。但正如规则文档所言:

Using assertions to tell the type system new information is often a sign that code is not fully type-safe.

也就是说,用断言向类型系统"撒谎"往往是代码并未完全类型安全的信号! 只是编译期告诉编译器"相信我,这里不为空",它不产生任何运行时检查——如果运行时值实际为 null/undefined,程序依然会崩溃,类型系统无法提供任何保护。

规则给出的建议是:与其用 ! 掩盖可空性,不如调整程序逻辑结构,让 TypeScript 真正理解"值在何时可能为空"。

因此该规则的基本判定非常简单:只要检测到 TypeScript 非空断言表达式(TsNonNullAssertionExpression)或非空断言赋值(TsNonNullAssertionAssignment),就报告诊断

无效代码示例(完整场景)

规则文档给出了两组无效示例,均可在 tests/specs/style/noNonNullAssertion/invalid.ts 测试用例中找到对应或扩展版本。

示例一:可选属性上的断言

interface Example {
  property?: string;
}
declare const example: Example;
const includesBaz = foo.property!.includes('baz');

这里 property 是可选的(可能为 undefined),直接使用 ! 断言后调用 .includes()。Rome 报告:

style/noNonNullAssertion.js:5:21 lint/style/noNonNullAssertion  FIXABLE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

✖ Forbidden non-null assertion.

   3 │ }
   4 │ declare const example: Example;
>  5 │ const includesBaz = foo.property!.includes('baz');
     │                    ^^^^^^^^^^^^^
   6 │

注意诊断信息包含两点关键内容:

  1. FIXABLE 标记:该位置可自动修复;
  2. Suggested fix 提示Replace with optional chain operator ?.——建议用可选链 ?. 替换。正如规则提示文案所说,?. 包含运行时检查,比仅编译期的 ! 更安全

示例二:赋值目标上的断言

(b!! as number) = "test";

这是一个把非空断言用在赋值左侧(assignment target)的极端场景,同时出现了双重断言 !!。Rome 报告:

style/noNonNullAssertion.js:1:2 lint/style/noNonNullAssertion
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

✖ Forbidden non-null assertion.

> 1 │ (b!! as number) = "test";
    │  ^^^
   2 │

请注意:这条诊断没有 FIXABLE 标记。从源码 no_non_null_assertion.rsaction 方法可以看出原因——当匹配到 TsNonNullAssertionAssignment(赋值场景)时,直接返回 None,不提供自动修复:

AnyTsNonNullAssertion::TsNonNullAssertionAssignment(_) => None,

因为赋值目标无法用可选链替换,Rome 不会给出可能有副作用的修复建议。

有效代码示例

规则文档中的有效示例展示了推荐的替代写法——使用可选链 + 兜底值:

interface Example {
  property?: string;
}

declare const example: Example;
const includesBaz = foo.property?.includes('baz') ?? false;

?.propertyundefined 时短路返回 undefined,配合 ?? false 给出默认值,整个表达式类型安全且运行时行为正确。

测试文件 valid.ts 进一步列出了 6 类不被该规则告警的代码:

x;
x.y;
x.y.z;
x?.y.z;
x?.y?.z;
!x;

可见:普通成员访问、可选链访问、以及对表达式取逻辑非!x,这里 ! 是逻辑非运算符而非非空断言)都是合法的。

源码实现:规则如何检测与报告

从源码结构看,该规则的检测链路分三步(对应 no_non_null_assertion.rs 中的 Rule trait 实现):

1. 查询节点类型(Query

规则通过 declare_node_union! 宏将两类语法节点合并查询:

declare_node_union! {
    pub(crate) AnyTsNonNullAssertion = TsNonNullAssertionExpression | TsNonNullAssertionAssignment
}

即同时覆盖表达式中的断言(如 x!)与赋值目标中的断言(如 (b! as number) = "test")。

2. 去重过滤(run

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
    match ctx.query() {
        AnyTsNonNullAssertion::TsNonNullAssertionExpression(node) => node
            .parent::<TsNonNullAssertionExpression>()
            .map_or(Some(()), |_| None),
        AnyTsNonNullAssertion::TsNonNullAssertionAssignment(node) => node
            .parent::<TsNonNullAssertionAssignment>()
            .map_or(Some(()), |_| None),
    }
}

这里的关键逻辑是:如果当前断言节点的父节点仍是同类断言节点,则跳过报告。这避免了 x!!! 这类连续断言被重复报告三次——测试快照中 x!!! 只产生一条诊断(覆盖 ^^^ 整个范围)正是这个机制的结果。

3. 生成诊断(diagnostic

诊断消息固定为 Forbidden non-null assertion.,定位到整个断言表达式的范围(ctx.query().range()):

Some(RuleDiagnostic::new(
    rule_category!(),
    ctx.query().range(),
    markup! { "Forbidden non-null assertion." },
))

快速修复:可选链替换策略

action 方法是该规则最有价值的部分。它以 Applicability::MaybeIncorrect(可能不正确)的等级提供快速修复,因为 ?. 会引入运行时检查、改变语义,需要开发者确认。根据父节点类型分为四种情况(源码注释与代码一一对应):

场景 1:静态成员访问(JsStaticMemberExpression

// object!.prop --> object?.prop
// object!?.prop --> object?.prop
  • foo!.barfoo?.bar
  • 若父级已是可选链 foo!?.bar,则仅移除 !foo?.bar

场景 2:计算成员访问(JsComputedMemberExpression

// object!["prop"] --> object?["prop"]
// object!?["prop"] --> object?.["prop"]
  • foo!["bar"]foo?.["bar"]
  • 若父级已是可选 foo!?.["bar"],仅移除 !foo?.["bar"]

场景 3:函数调用(JsCallExpression

// f!() --> f?.()
// f!?() --> f?.()
  • run!()run?.()
  • 若已是可选调用 run!?.(),仅移除 !run?.()

场景 4:其余上下文

_ => {
    // unsupported
    return None;
}

不属于以上三种父节点的场景(如赋值目标、类型断言内部等)不提供修复,只报告诊断。

此外,修复逻辑中还有一个细节:在移除断言标记前会先循环剥掉重复的断言:

let mut expr = node.expression();
while let Ok(AnyJsExpression::TsNonNullAssertionExpression(assertion)) = expr {
    expr = assertion.expression()
}

x!!.y 会直接修复为 x?.y(而不是 x!?.y),这在测试快照 invalid.ts.snap 中有明确验证。

测试用例全景:19 个无效场景

仓库的规格测试文件 invalid.ts 覆盖了该规则的完整行为矩阵,快照 invalid.ts.snap 记录了每条诊断与修复的精确输出:

输入是否可修复修复结果
x!;否(孤立表达式)
x!.y;x?.y;
x.y!;
!x!.y;!x?.y;
x!.y?.z;x?.y?.z;
x![y];x?.[y];
x![y]?.z;x?.[y]?.z;
x.y.z!();x.y.z?.();
x.y?.z!();x.y?.z?.();
x!!!;
x!!.y;x?.y;
x.y!!;
x.y.z!!();x.y.z?.();
x!?.[y].z;x?.[y].z;
x!?.y.z;x?.y.z;
x!!!?.y.z;x?.y.z;
x.y.z!?.();x.y.z?.();
x.y.z!!!?.();x.y.z?.();
(b! as number) = "test";
(b!! as number) = "test";

这些用例清晰地展示了规则的两个设计取向:尽可能给出安全的可选链修复;同时对于无法安全转换的孤立断言、赋值目标等场景保持只报告、不修复

与 noExtraNonNullAssertion 的分工

值得注意,Rome 在 suspicious 组还有一条相邻规则 noExtraNonNullAssertionno_extra_non_null_assertion.rs,同为 v11.0.0、推荐启用),二者都基于 AnyTsNonNullAssertion 节点工作,但职责不同:

  • noNonNullAssertion(style 组):禁止一切非空断言,主张用可选链等类型安全写法替代;
  • noExtraNonNullAssertion(suspicious 组):只针对重复/多余的断言(如 foo!!.barbar!?.n 中的冗余 !),修复方式是直接删除多余的 !applicability: Always,恒定安全)。

两条规则组合后,foo!!.bar 会同时收到两类诊断;若你不想完全禁止 !、只想清理冗余用法,可关闭 noNonNullAssertion 而保留 noExtraNonNullAssertion

配置与使用方式

推荐规则(默认行为)

由于 recommended: true,当启用 Rome 的推荐规则集时,noNonNullAssertion 自动生效。配置结构在 linter/rules.rs 中体现为 no_non_null_assertion: Option<RuleConfiguration> 字段,其类型为 linter/mod.rs 中定义的枚举:

#[serde(rename_all = "camelCase", deny_unknown_fields, untagged)]
pub enum RuleConfiguration {
    Plain(RulePlainConfiguration),
    WithOptions(RuleWithOptions),
}

即每条规则支持两种配置形态:纯级别配置,或带选项配置。

在 rome.json 中配置

在项目根目录的 rome.json(仓库根目录即有一份示例)中,可按如下方式调整该规则:

{
  "linter": {
    "enabled": true,
    "rules": {
      "style": {
        "noNonNullAssertion": "off",        // 关闭该规则
        "noNonNullAssertion": "warn",       // 降级为警告
        "noNonNullAssertion": "error",      // 显式设为错误(默认级别)
        "noNonNullAssertion": {             // 带选项的配置形态
          "level": "warn"
        }
      }
    }
  }
}

按行/代码块局部禁用

若确需在个别位置保留 !,可参考 linter.mdx 中"Disable a lint rule"一节使用行内抑制注释:

// rome-ignore lint/style/noNonNullAssertion: 此处已由上游保证非空
const value = map.get(key)!.toString();

关于规则的完整开启/关闭/选项机制,参见 linter.mdx 中的 "Disable a lint rule" 与 "Rule options" 两节。

最佳实践小结

  1. 默认信任推荐配置noNonNullAssertion 是 Rome 推荐规则,建议保持启用,让 CI 拦截新增的 ! 用法。
  2. 优先使用可选链 + 兜底值obj?.prop ?? defaultValue 在运行时安全性与类型安全性上都优于 obj!.prop
  3. 无法替换时用抑制注释并说明理由:对确属"上游已保证非空"的场景(如 map 查值后立即使用),使用 rome-ignore 注释并写明原因,保留审计痕迹。
  4. 善用自动修复:对 x!.yf!()x![i] 等常见形态,Rome 会直接给出 ?. 修复;对无法安全转换的赋值场景,规则只报告不修复,需人工评估。
  5. 与 noExtraNonNullAssertion 协同:若团队暂时不接受全面禁用 !,可仅依赖 suspicious/noExtraNonNullAssertion 消除冗余断言这一"确定无争议"的问题。

延伸阅读

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

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

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值