feat(rome_js_analyze): add noExtraNonNullAssertion rule
MODIFY
crates/rome_diagnostics_categories/src/categories.rs
crates/rome_diagnostics_categories/src/categories.rs
@@ -85,6 +85,7 @@ define_dategories! {
"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/noPrecisionLoss": "https://docs.rome.tools/lint/rules/noPrecisionLoss",

MODIFY
crates/rome_js_analyze/src/analyzers/nursery.rs
crates/rome_js_analyze/src/analyzers/nursery.rs
@@ -9,6 +9,7 @@ mod no_distracting_elements;
mod no_dupe_keys;
mod no_empty_interface;
mod no_explicit_any;
+mod no_extra_non_null_assertion;
mod no_header_scope;
mod no_invalid_constructor_super;
mod no_precision_loss;
@@ -18,4 +19,4 @@ mod no_unsafe_finally;
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_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_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 :: 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_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 :: use_flat_map :: UseFlatMap , self :: use_numeric_literals :: UseNumericLiterals , self :: use_valid_for_direction :: UseValidForDirection ,] } }

ADD
crates/rome_js_analyze/src/analyzers/nursery/no_extra_non_null_assertion.rs
+use rome_analyze::context::RuleContext;
+use rome_analyze::{declare_rule, ActionCategory, Ast, Rule, RuleDiagnostic};
+use rome_console::markup;
+use rome_diagnostics::Applicability;
+use rome_js_syntax::{
+ JsAnyAssignment, JsAnyExpression, TsNonNullAssertionAssignment, TsNonNullAssertionExpression,
+};
+use rome_rowan::{declare_node_union, AstNode, BatchMutationExt};
+
+use crate::JsRuleAction;
+
+declare_rule! {
+ /// Prevents the wrong usage of the non-null assertion operator (`!`) in TypeScript files.
+ ///
+ /// > The `!` non-null assertion operator in TypeScript is used to assert that a value's type does not include `null` or `undefined`. Using the operator any more than once on a single value does nothing.
+ ///
+ /// Source: https://typescript-eslint.io/rules/no-extra-non-null-assertion
+ ///
+ /// ## Examples
+ ///
+ /// ### Invalid
+ /// ```ts,expect_diagnostic
+ /// const bar = foo!!.bar;
+ /// ```
+ ///
+ /// ```ts,expect_diagnostic
+ /// function fn(bar?: { n: number }) {
+ /// return bar!?.n;
+ /// }
+ /// ```
+ ///
+ /// ```ts,expect_diagnostic
+ /// function fn(bar?: { n: number }) {
+ /// return ((bar!))?.();
+ /// }
+ /// ```
+ ///
+ /// ### Valid
+ /// ```ts
+ /// const bar = foo!.bar;
+ ///
+ /// obj?.string!.trim();
+ ///
+ /// function fn(key: string | null) {
+ /// const obj = {};
+ /// return obj?.[key!];
+ /// }
+ /// ```
+ ///
+ pub(crate) NoExtraNonNullAssertion {
+ version: "11.0.0",
+ name: "noExtraNonNullAssertion",
+ recommended: false,
+ }
+}
+
+declare_node_union! {
+ pub(crate) TsAnyNonNullAssertion = TsNonNullAssertionAssignment | TsNonNullAssertionExpression
+}
+
+impl Rule for NoExtraNonNullAssertion {
+ type Query = Ast<TsAnyNonNullAssertion>;
+ type State = ();
+ type Signals = Option<Self::State>;
+ type Options = ();
+
+ fn run(ctx: &RuleContext<Self>) -> Self::Signals {
+ let node = ctx.query();
+
+ match node {
+ TsAnyNonNullAssertion::TsNonNullAssertionAssignment(_) => {
+ let parent = node.parent::<JsAnyAssignment>()?;
+
+ // Cases considered as invalid:
+ // - TsNonNullAssertionAssignment > TsNonNullAssertionAssignment
+ if matches!(parent, JsAnyAssignment::TsNonNullAssertionAssignment(_)) {
+ return Some(());
+ }
+ }
+ TsAnyNonNullAssertion::TsNonNullAssertionExpression(_) => {
+ let parent = node.parent::<JsAnyExpression>()?;
+
+ // Cases considered as invalid:
+ // - TsNonNullAssertionAssignment > TsNonNullAssertionExpression
+ // - TsNonNullAssertionExpression > TsNonNullAssertionExpression
+ // - JsCallExpression[optional] > TsNonNullAssertionExpression
+ // - JsStaticMemberExpression[optional] > TsNonNullAssertionExpression
+ let has_extra_non_assertion = match parent.omit_parentheses() {
+ JsAnyExpression::JsAssignmentExpression(expr) => expr
+ .left()
+ .ok()?
+ .as_js_any_assignment()?
+ .as_ts_non_null_assertion_assignment()
+ .is_some(),
+ JsAnyExpression::TsNonNullAssertionExpression(_) => true,
+ JsAnyExpression::JsStaticMemberExpression(expr) => expr.is_optional(),
+ JsAnyExpression::JsCallExpression(expr) => expr.is_optional(),
+ _ => false,
+ };
+
+ if has_extra_non_assertion {
+ return Some(());
+ }
+ }
+ }
+
+ None
+ }
+
+ fn diagnostic(ctx: &RuleContext<Self>, _: &Self::State) -> Option<RuleDiagnostic> {
+ let diagnostic = RuleDiagnostic::new(
+ rule_category!(),
+ ctx.query().range(),
+ "Forbidden extra non-null assertion.",
+ );
+
+ Some(diagnostic)
+ }
+
+ fn action(ctx: &RuleContext<Self>, _: &Self::State) -> Option<JsRuleAction> {
+ let mut mutation = ctx.root().begin();
+ let node = ctx.query();
+
+ let excl_token = match node {
+ TsAnyNonNullAssertion::TsNonNullAssertionAssignment(assignment) => {
+ assignment.excl_token().ok()?
+ }
+ TsAnyNonNullAssertion::TsNonNullAssertionExpression(expression) => {
+ expression.excl_token().ok()?
+ }
+ };
+
+ mutation.remove_token(excl_token);
+
+ Some(JsRuleAction {
+ category: ActionCategory::QuickFix,
+ applicability: Applicability::Always,
+ message: markup! { "Remove extra non-null assertion." }.to_owned(),
+ mutation,
+ })
+ }
+}

ADD
crates/rome_js_analyze/tests/specs/nursery/noExtraNonNullAssertion/invalid.ts
+// @ts-expect-error
+const bar = case1!!.bar;
+
+function case2(bar: number | undefined) {
+ const bar1: number = bar!!!;
+}
+
+function case3(bar?: { n: number }) {
+ return bar!?.n;
+}
+
+function case4(bar?: { n: number }) {
+ return bar!?.();
+}
+
+const bar2 = (case5!)!.bar;
+
+function case6(bar?: { n: number }) {
+ return (bar!)?.n;
+}
+
+function case7(bar?: { n: number }) {
+ return (bar)!?.n;
+}
+
+function case8(bar?: { n: number }) {
+ return ((bar!))?.();
+}
+
+class Case9 {
+ method() {
+ this.property!!;
+ }
+}
+
+case10!!.prop = null;
+
+case11!?.[computedField];
+
+case12!?.[a.b!!];
+
+case13!!! = null
+
+case14!! = null
+
+if (case15!!) {}
+
+if (!case16!!) {}

ADD
crates/rome_js_analyze/tests/specs/nursery/noExtraNonNullAssertion/valid.ts
+const bar = case1!.bar;
+
+function case2(bar: number | undefined) {
+ const bar1: number = bar!;
+}
+
+function case3(bar?: { n: number }) {
+ return bar?.n;
+}
+
+checksCounter?.case4!.trim();
+
+function case5(key: string | null) {
+ const obj = {};
+ return obj?.[key!];
+}

ADD
crates/rome_js_analyze/tests/specs/nursery/noExtraNonNullAssertion/valid.ts.snap
+---
+source: crates/rome_js_analyze/tests/spec_tests.rs
+assertion_line: 73
+expression: valid.ts
+---
+# Input
+```js
+const bar = case1!.bar;
+
+function case2(bar: number | undefined) {
+ const bar1: number = bar!;
+}
+
+function case3(bar?: { n: number }) {
+ return bar?.n;
+}
+
+checksCounter?.case4!.trim();
+
+function case5(key: string | null) {
+ const obj = {};
+ return obj?.[key!];
+}
+
+```
+
+

MODIFY
crates/rome_service/src/configuration/linter/rules.rs
crates/rome_service/src/configuration/linter/rules.rs
@@ -747,6 +747,8 @@ struct NurserySchema {
no_empty_interface: Option<RuleConfiguration>,
#[doc = "Disallow the any type usage"]
no_explicit_any: Option<RuleConfiguration>,
+ #[doc = "Prevents the wrong usage of the non-null assertion operator (!) in TypeScript files."]
+ no_extra_non_null_assertion: Option<RuleConfiguration>,
#[doc = "Check that the scope attribute is only used on th elements."]
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."]
@@ -776,7 +778,7 @@ struct NurserySchema {
}
impl Nursery {
const CATEGORY_NAME: &'static str = "nursery";
- pub(crate) const CATEGORY_RULES: [&'static str; 22] = [
+ pub(crate) const CATEGORY_RULES: [&'static str; 23] = [
"noAccessKey",
"noBannedTypes",
"noConditionalAssignment",
@@ -786,6 +788,7 @@ impl Nursery {
"noDupeKeys",
"noEmptyInterface",
"noExplicitAny",
+ "noExtraNonNullAssertion",
"noHeaderScope",
"noInvalidConstructorSuper",
"noPrecisionLoss",

MODIFY
editors/vscode/configuration_schema.json
editors/vscode/configuration_schema.json
}
]
},
+ "noExtraNonNullAssertion": {
+ "description": "Prevents the wrong usage of the non-null assertion operator (!) in TypeScript files.",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/RuleConfiguration"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"noHeaderScope": {
"description": "Check that the scope attribute is only used on th elements.",
"anyOf": [

MODIFY
npm/backend-jsonrpc/src/workspace.ts
npm/backend-jsonrpc/src/workspace.ts
@@ -383,6 +383,10 @@ export interface Nursery {
* Disallow the any type usage
*/
noExplicitAny?: RuleConfiguration;
+ /**
+ * Prevents the wrong usage of the non-null assertion operator (!) in TypeScript files.
+ */
+ noExtraNonNullAssertion?: RuleConfiguration;
/**
* Check that the scope attribute is only used on th elements.
*/
@@ -653,6 +657,7 @@ export type Category =
| "lint/nursery/noDupeKeys"
| "lint/nursery/noEmptyInterface"
| "lint/nursery/noExplicitAny"
+ | "lint/nursery/noExtraNonNullAssertion"
| "lint/nursery/noHeaderScope"
| "lint/nursery/noInvalidConstructorSuper"
| "lint/nursery/noPrecisionLoss"

MODIFY
npm/rome/configuration_schema.json
npm/rome/configuration_schema.json
}
]
},
+ "noExtraNonNullAssertion": {
+ "description": "Prevents the wrong usage of the non-null assertion operator (!) in TypeScript files.",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/RuleConfiguration"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"noHeaderScope": {
"description": "Check that the scope attribute is only used on th elements.",
"anyOf": [

MODIFY
website/src/pages/lint/rules/index.mdx
website/src/pages/lint/rules/index.mdx
@@ -498,6 +498,12 @@ Disallow the declaration of empty interfaces.
Disallow the <code>any</code> type usage
</section>
<section class="rule">
+<h3 data-toc-exclude id="noExtraNonNullAssertion">
+ <a href="/lint/rules/noExtraNonNullAssertion">noExtraNonNullAssertion</a>
+</h3>
+Prevents the wrong usage of the non-null assertion operator (<code>!</code>) in TypeScript files.
+</section>
+<section class="rule">
<h3 data-toc-exclude id="noHeaderScope">
<a href="/lint/rules/noHeaderScope">noHeaderScope</a>
</h3>

feat(rome_js_analyze): add noExtraNonNullAssertion rule (#3797)