Rome noNonNullAssertion 规则深度解析:禁止 TypeScript 非空断言运算符 !
本指南以 Rome(JavaScript、TypeScript 与 Web 的统一开发工具链)官方 lint 规则文档 noNonNullAssertion.md 为核心,系统讲解 TypeScript 非空断言运算符
!为何被禁止、规则如何检测与自动修复,并结合 crates/rome_js_analyze 下的源码实现与测试用例(no_non_null_assertion.rs、invalid.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 null 或 undefined)"。但正如规则文档所言:
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 │
注意诊断信息包含两点关键内容:
- FIXABLE 标记:该位置可自动修复;
- 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.rs 的 action 方法可以看出原因——当匹配到 TsNonNullAssertionAssignment(赋值场景)时,直接返回 None,不提供自动修复:
AnyTsNonNullAssertion::TsNonNullAssertionAssignment(_) => None,
因为赋值目标无法用可选链替换,Rome 不会给出可能有副作用的修复建议。
有效代码示例
规则文档中的有效示例展示了推荐的替代写法——使用可选链 + 兜底值:
interface Example {
property?: string;
}
declare const example: Example;
const includesBaz = foo.property?.includes('baz') ?? false;
?. 在 property 为 undefined 时短路返回 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!.bar→foo?.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 组还有一条相邻规则 noExtraNonNullAssertion(no_extra_non_null_assertion.rs,同为 v11.0.0、推荐启用),二者都基于 AnyTsNonNullAssertion 节点工作,但职责不同:
noNonNullAssertion(style 组):禁止一切非空断言,主张用可选链等类型安全写法替代;noExtraNonNullAssertion(suspicious 组):只针对重复/多余的断言(如foo!!.bar、bar!?.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" 两节。
最佳实践小结
- 默认信任推荐配置:
noNonNullAssertion是 Rome 推荐规则,建议保持启用,让 CI 拦截新增的!用法。 - 优先使用可选链 + 兜底值:
obj?.prop ?? defaultValue在运行时安全性与类型安全性上都优于obj!.prop。 - 无法替换时用抑制注释并说明理由:对确属"上游已保证非空"的场景(如 map 查值后立即使用),使用
rome-ignore注释并写明原因,保留审计痕迹。 - 善用自动修复:对
x!.y、f!()、x![i]等常见形态,Rome 会直接给出?.修复;对无法安全转换的赋值场景,规则只报告不修复,需人工评估。 - 与 noExtraNonNullAssertion 协同:若团队暂时不接受全面禁用
!,可仅依赖suspicious/noExtraNonNullAssertion消除冗余断言这一"确定无争议"的问题。
延伸阅读
- 规则源码实现:crates/rome_js_analyze/src/analyzers/style/no_non_null_assertion.rs
- 规则注册与分组:crates/rome_js_analyze/src/analyzers/style.rs
- 完整测试用例:tests/specs/style/noNonNullAssertion/invalid.ts 与 invalid.ts.snap、valid.ts
- 相邻规则:crates/rome_js_analyze/src/analyzers/suspicious/no_extra_non_null_assertion.rs
- 配置模型:crates/rome_service/src/configuration/linter/mod.rs 与 crates/rome_service/src/configuration/linter/rules.rs
- 诊断分类定义:crates/rome_diagnostics_categories/src/categories.rs
- 规则启停与选项指南:website/src/pages/linter.mdx
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



