feat(rome_js_analyze): implement `noNonNullAssertion` rule
MODIFY
crates/rome_diagnostics_categories/src/categories.rs
crates/rome_diagnostics_categories/src/categories.rs
@@ -74,23 +74,23 @@ define_dategories! {
"lint/security/noDangerouslySetInnerHtml": "https://docs.rome.tools/lint/rules/noDangerouslySetInnerHtml",
"lint/security/noDangerouslySetInnerHtmlWithChildren": "https://docs.rome.tools/lint/rules/noDangerouslySetInnerHtmlWithChildren",
-
// nursery
"lint/nursery/noAccessKey": "https://docs.rome.tools/lint/rules/noAccessKey",
"lint/nursery/noBannedTypes":"https://docs.rome.tools/lint/rules/noBannedTypes",
"lint/nursery/noConditionalAssignment": "https://docs.rome.tools/lint/rules/noConditionalAssignment",
"lint/nursery/noConstAssign": "https://docs.rome.tools/lint/rules/noConstAssign",
"lint/nursery/noConstEnum": "https://docs.rome.tools/lint/rules/noConstEnum",
- "lint/nursery/noDistractingElements": "https://docs.rome.tools/lint/rules/noDistractingElements",
"lint/nursery/noConstructorReturn": "https://docs.rome.tools/lint/rules/noConstructorReturn",
- "lint/nursery/noSetterReturn": "https://docs.rome.tools/lint/rules/noSetterReturn",
+ "lint/nursery/noDistractingElements": "https://docs.rome.tools/lint/rules/noDistractingElements",
"lint/nursery/noDupeKeys":"https://docs.rome.tools/lint/rules/noDupeKeys",
"lint/nursery/noEmptyInterface": "https://docs.rome.tools/lint/rules/noEmptyInterface",
"lint/nursery/noExplicitAny": "https://docs.rome.tools/lint/rules/noExplicitAny",
"lint/nursery/noExtraNonNullAssertion":"https://docs.rome.tools/lint/rules/noExtraNonNullAssertion",
"lint/nursery/noHeaderScope": "https://docs.rome.tools/lint/rules/noHeaderScope",
"lint/nursery/noInvalidConstructorSuper": "https://docs.rome.tools/lint/rules/noInvalidConstructorSuper",
+ "lint/nursery/noNonNullAssertion": "https://docs.rome.tools/lint/rules/noNonNullAssertion",
"lint/nursery/noPrecisionLoss": "https://docs.rome.tools/lint/rules/noPrecisionLoss",
+ "lint/nursery/noSetterReturn": "https://docs.rome.tools/lint/rules/noSetterReturn",
"lint/nursery/noStringCaseMismatch": "https://docs.rome.tools/lint/rules/noStringCaseMismatch",
"lint/nursery/noUnsafeFinally": "https://docs.rome.tools/lint/rules/noUnsafeFinally",
"lint/nursery/noVar": "https://docs.rome.tools/lint/rules/noVar",

MODIFY
crates/rome_js_analyze/src/analyzers/nursery.rs
crates/rome_js_analyze/src/analyzers/nursery.rs
@@ -13,6 +13,7 @@ mod no_explicit_any;
mod no_extra_non_null_assertion;
mod no_header_scope;
mod no_invalid_constructor_super;
+mod no_non_null_assertion;
mod no_precision_loss;
mod no_setter_return;
mod no_string_case_mismatch;
@@ -22,4 +23,4 @@ mod use_default_switch_clause_last;
mod use_flat_map;
mod use_numeric_literals;
mod use_valid_for_direction;
-declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_access_key :: NoAccessKey , self :: no_banned_types :: NoBannedTypes , self :: no_conditional_assignment :: NoConditionalAssignment , self :: no_const_enum :: NoConstEnum , self :: no_constructor_return :: NoConstructorReturn , self :: no_distracting_elements :: NoDistractingElements , self :: no_dupe_keys :: NoDupeKeys , self :: no_empty_interface :: NoEmptyInterface , self :: no_explicit_any :: NoExplicitAny , self :: no_extra_non_null_assertion :: NoExtraNonNullAssertion , self :: no_header_scope :: NoHeaderScope , self :: no_invalid_constructor_super :: NoInvalidConstructorSuper , self :: no_precision_loss :: NoPrecisionLoss , self :: no_setter_return :: NoSetterReturn , self :: no_string_case_mismatch :: NoStringCaseMismatch , self :: no_unsafe_finally :: NoUnsafeFinally , self :: no_void_type_return :: NoVoidTypeReturn , self :: use_default_switch_clause_last :: UseDefaultSwitchClauseLast , self :: use_flat_map :: UseFlatMap , self :: use_numeric_literals :: UseNumericLiterals , self :: use_valid_for_direction :: UseValidForDirection ,] } }
+declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_access_key :: NoAccessKey , self :: no_banned_types :: NoBannedTypes , self :: no_conditional_assignment :: NoConditionalAssignment , self :: no_const_enum :: NoConstEnum , self :: no_constructor_return :: NoConstructorReturn , self :: no_distracting_elements :: NoDistractingElements , self :: no_dupe_keys :: NoDupeKeys , self :: no_empty_interface :: NoEmptyInterface , self :: no_explicit_any :: NoExplicitAny , self :: no_extra_non_null_assertion :: NoExtraNonNullAssertion , self :: no_header_scope :: NoHeaderScope , self :: no_invalid_constructor_super :: NoInvalidConstructorSuper , self :: no_non_null_assertion :: NoNonNullAssertion , self :: no_precision_loss :: NoPrecisionLoss , self :: no_setter_return :: NoSetterReturn , self :: no_string_case_mismatch :: NoStringCaseMismatch , self :: no_unsafe_finally :: NoUnsafeFinally , self :: no_void_type_return :: NoVoidTypeReturn , self :: use_default_switch_clause_last :: UseDefaultSwitchClauseLast , self :: use_flat_map :: UseFlatMap , self :: use_numeric_literals :: UseNumericLiterals , self :: use_valid_for_direction :: UseValidForDirection ,] } }

ADD
crates/rome_js_analyze/src/analyzers/nursery/no_non_null_assertion.rs
+use crate::JsRuleAction;
+use rome_analyze::{context::RuleContext, declare_rule, ActionCategory, Ast, Rule, RuleDiagnostic};
+use rome_console::markup;
+use rome_diagnostics::Applicability;
+use rome_js_factory::make;
+use rome_js_syntax::{
+ AnyJsExpression, JsSyntaxKind, TsNonNullAssertionAssignment, TsNonNullAssertionExpression,
+};
+use rome_rowan::{declare_node_union, AstNode, BatchMutationExt};
+
+declare_rule! {
+ /// Disallow non-null assertions using the `!` postfix operator.
+ ///
+ /// TypeScript's `!` non-null assertion operator asserts to the type system that an expression is non-nullable, as
+ /// in not `null` or `undefined`. Using assertions to tell the type system new information is often a sign that
+ /// code is not fully type-safe. It's generally better to structure program logic so that TypeScript understands
+ /// when values may be nullable.
+ ///
+ /// ## Examples
+ ///
+ /// ### Invalid
+ ///
+ /// ```ts,expect_diagnostic
+ /// interface Example {
+ /// property?: string;
+ /// }
+ /// declare const example: Example;
+ /// const includesBaz = foo.property!.includes('baz');
+ /// ```
+ /// ```ts,expect_diagnostic
+ /// (b!! as number) = "test";
+ /// ```
+ ///
+ /// ### Valid
+ ///
+ /// ```ts
+ /// interface Example {
+ /// property?: string;
+ /// }
+ ///
+ /// declare const example: Example;
+ /// const includesBaz = foo.property?.includes('baz') ?? false;
+ /// ```
+ ///
+ pub(crate) NoNonNullAssertion {
+ version: "11.0.0",
+ name: "noNonNullAssertion",
+ recommended: false,
+ }
+}
+
+declare_node_union! {
+ pub(crate) AnyTsNonNullAssertion = TsNonNullAssertionExpression | TsNonNullAssertionAssignment
+}
+
+impl Rule for NoNonNullAssertion {
+ type Query = Ast<AnyTsNonNullAssertion>;
+ type State = ();
+ type Signals = Option<Self::State>;
+ type Options = ();
+
+ 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),
+ }
+ }
+
+ fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
+ Some(RuleDiagnostic::new(
+ rule_category!(),
+ ctx.query().range(),
+ markup! {
+ "Forbidden non-null assertion."
+ },
+ ))
+ }
+
+ fn action(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<JsRuleAction> {
+ let node = ctx.query();
+ match node {
+ AnyTsNonNullAssertion::TsNonNullAssertionAssignment(_) => None,
+ AnyTsNonNullAssertion::TsNonNullAssertionExpression(node) => {
+ if !can_replace_with_optional_chain(node) {
+ return None;
+ }
+ let mut mutation = ctx.root().begin();
+
+ let assertions =
+ std::iter::successors(node.expression().ok(), |expression| match expression {
+ AnyJsExpression::TsNonNullAssertionExpression(assertion) => {
+ assertion.expression().ok()
+ }
+ _ => None,
+ });
+
+ for assertion in assertions {
+ if let Some(non_null_expr) = assertion.as_ts_non_null_assertion_expression() {
+ mutation.remove_token(non_null_expr.excl_token().ok()?);
+ }
+ }
+
+ match node.parent::<AnyJsExpression>()? {
+ AnyJsExpression::JsComputedMemberExpression(parent) => {
+ if parent.is_optional() {
+ mutation.remove_token(node.excl_token().ok()?);
+ } else {
+ mutation.replace_token(
+ node.excl_token().ok()?,
+ make::token(JsSyntaxKind::QUESTIONDOT),
+ );
+ }
+ }
+ AnyJsExpression::JsCallExpression(parent) => {
+ if parent.is_optional() {
+ mutation.remove_token(node.excl_token().ok()?);
+ } else {
+ mutation.replace_token(
+ node.excl_token().ok()?,
+ make::token(JsSyntaxKind::QUESTIONDOT),
+ );
+ }
+ }
+ AnyJsExpression::JsStaticMemberExpression(parent) => {
+ if parent.is_optional() {
+ mutation.remove_token(node.excl_token().ok()?);
+ } else {
+ mutation.replace_token(
+ node.excl_token().ok()?,
+ make::token(JsSyntaxKind::QUESTION),
+ );
+ }
+ }
+ _ => {}
+ };
+
+ Some(JsRuleAction {
+ category: ActionCategory::QuickFix,
+ applicability: Applicability::MaybeIncorrect,
+ message: markup! { "Replace with optional chain operator "<Emphasis>"?."</Emphasis>" This operator includes runtime checks, so it is safer than the compile-only non-null assertion operator" }
+ .to_owned(),
+ mutation,
+ })
+ }
+ }
+ }
+}
+
+fn can_replace_with_optional_chain(node: &TsNonNullAssertionExpression) -> bool {
+ use AnyJsExpression::*;
+
+ matches!(
+ node.parent::<AnyJsExpression>(),
+ Some(JsStaticMemberExpression(_) | JsComputedMemberExpression(_) | JsCallExpression(_))
+ )
+}

ADD
crates/rome_js_analyze/tests/specs/nursery/noNonNullAssertion/invalid.ts
+x!;
+x!.y;
+x.y!;
+!x!.y;
+x!.y?.z;
+x![y];
+x![y]?.z;
+x.y.z!();
+x.y?.z!();
+x!!!;
+x!!.y;
+x.y!!;
+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";

ADD
crates/rome_js_analyze/tests/specs/nursery/noNonNullAssertion/valid.ts
+x;
+x.y;
+x.y.z;
+x?.y.z;
+x?.y?.z;
+!x;

ADD
crates/rome_js_analyze/tests/specs/nursery/noNonNullAssertion/valid.ts.snap
+---
+source: crates/rome_js_analyze/tests/spec_tests.rs
+expression: valid.ts
+---
+# Input
+```js
+x;
+x.y;
+x.y.z;
+x?.y.z;
+x?.y?.z;
+!x;
+
+```
+
+

MODIFY
crates/rome_service/src/configuration/linter/rules.rs
crates/rome_service/src/configuration/linter/rules.rs
@@ -755,6 +755,8 @@ struct NurserySchema {
no_header_scope: Option<RuleConfiguration>,
#[doc = "Prevents the incorrect use of super() inside classes. It also checks whether a call super() is missing from classes that extends other constructors."]
no_invalid_constructor_super: Option<RuleConfiguration>,
+ #[doc = "Disallow non-null assertions using the ! postfix operator."]
+ no_non_null_assertion: Option<RuleConfiguration>,
#[doc = "Disallow literal numbers that lose precision"]
no_precision_loss: Option<RuleConfiguration>,
#[doc = "Disallow returning a value from a setter"]
@@ -788,7 +790,7 @@ struct NurserySchema {
}
impl Nursery {
const CATEGORY_NAME: &'static str = "nursery";
- pub(crate) const CATEGORY_RULES: [&'static str; 28] = [
+ pub(crate) const CATEGORY_RULES: [&'static str; 29] = [
"noAccessKey",
"noBannedTypes",
"noConditionalAssignment",
@@ -802,6 +804,7 @@ impl Nursery {
"noExtraNonNullAssertion",
"noHeaderScope",
"noInvalidConstructorSuper",
+ "noNonNullAssertion",
"noPrecisionLoss",
"noSetterReturn",
"noStringCaseMismatch",

MODIFY
editors/vscode/configuration_schema.json
editors/vscode/configuration_schema.json
}
]
},
+ "noNonNullAssertion": {
+ "description": "Disallow non-null assertions using the ! postfix operator.",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/RuleConfiguration"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"noPrecisionLoss": {
"description": "Disallow literal numbers that lose precision",
"anyOf": [

MODIFY
npm/backend-jsonrpc/src/workspace.ts
npm/backend-jsonrpc/src/workspace.ts
@@ -399,6 +399,10 @@ export interface Nursery {
* Prevents the incorrect use of super() inside classes. It also checks whether a call super() is missing from classes that extends other constructors.
*/
noInvalidConstructorSuper?: RuleConfiguration;
+ /**
+ * Disallow non-null assertions using the ! postfix operator.
+ */
+ noNonNullAssertion?: RuleConfiguration;
/**
* Disallow literal numbers that lose precision
*/
@@ -672,16 +676,17 @@ export type Category =
| "lint/nursery/noConditionalAssignment"
| "lint/nursery/noConstAssign"
| "lint/nursery/noConstEnum"
- | "lint/nursery/noDistractingElements"
| "lint/nursery/noConstructorReturn"
- | "lint/nursery/noSetterReturn"
+ | "lint/nursery/noDistractingElements"
| "lint/nursery/noDupeKeys"
| "lint/nursery/noEmptyInterface"
| "lint/nursery/noExplicitAny"
| "lint/nursery/noExtraNonNullAssertion"
| "lint/nursery/noHeaderScope"
| "lint/nursery/noInvalidConstructorSuper"
+ | "lint/nursery/noNonNullAssertion"
| "lint/nursery/noPrecisionLoss"
+ | "lint/nursery/noSetterReturn"
| "lint/nursery/noStringCaseMismatch"
| "lint/nursery/noUnsafeFinally"
| "lint/nursery/noVar"

MODIFY
npm/rome/configuration_schema.json
npm/rome/configuration_schema.json
}
]
},
+ "noNonNullAssertion": {
+ "description": "Disallow non-null assertions using the ! postfix operator.",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/RuleConfiguration"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"noPrecisionLoss": {
"description": "Disallow literal numbers that lose precision",
"anyOf": [

MODIFY
website/src/pages/lint/rules/index.mdx
website/src/pages/lint/rules/index.mdx
@@ -523,6 +523,12 @@ Prevents the incorrect use of <code>super()</code> inside classes.
It also checks whether a call <code>super()</code> is missing from classes that extends other constructors.
</section>
<section class="rule">
+<h3 data-toc-exclude id="noNonNullAssertion">
+ <a href="/lint/rules/noNonNullAssertion">noNonNullAssertion</a>
+</h3>
+Disallow non-null assertions using the <code>!</code> postfix operator.
+</section>
+<section class="rule">
<h3 data-toc-exclude id="noPrecisionLoss">
<a href="/lint/rules/noPrecisionLoss">noPrecisionLoss</a>
</h3>

feat(rome_js_analyze): implement `noNonNullAssertion` rule (#3891)