From a9c6385dcf70b3a419affe3442049b7b1c873c90 Mon Sep 17 00:00:00 2001 From: kaykdm Date: Sun, 23 Jun 2024 21:23:50 +0900 Subject: [PATCH] feat(linter): implement @typescript-eslint/no-non-null-asserted-nullish-coalescing --- crates/oxc_linter/src/rules.rs | 2 + ...no_non_null_asserted_nullish_coalescing.rs | 178 ++++++++++++++++++ ..._non_null_asserted_nullish_coalescing.snap | 121 ++++++++++++ 3 files changed, 301 insertions(+) create mode 100644 crates/oxc_linter/src/rules/typescript/no_non_null_asserted_nullish_coalescing.rs create mode 100644 crates/oxc_linter/src/snapshots/no_non_null_asserted_nullish_coalescing.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index b89f807fb1514..7309e26ab009a 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -136,6 +136,7 @@ mod typescript { pub mod no_import_type_side_effects; pub mod no_misused_new; pub mod no_namespace; + pub mod no_non_null_asserted_nullish_coalescing; pub mod no_non_null_asserted_optional_chain; pub mod no_non_null_assertion; pub mod no_this_alias; @@ -542,6 +543,7 @@ oxc_macros::declare_all_lint_rules! { typescript::prefer_literal_enum_member, typescript::explicit_function_return_type, typescript::no_non_null_assertion, + typescript::no_non_null_asserted_nullish_coalescing, jest::expect_expect, jest::max_expects, jest::max_nested_describe, diff --git a/crates/oxc_linter/src/rules/typescript/no_non_null_asserted_nullish_coalescing.rs b/crates/oxc_linter/src/rules/typescript/no_non_null_asserted_nullish_coalescing.rs new file mode 100644 index 0000000000000..a091df707beca --- /dev/null +++ b/crates/oxc_linter/src/rules/typescript/no_non_null_asserted_nullish_coalescing.rs @@ -0,0 +1,178 @@ +use oxc_ast::{ast::Expression, AstKind}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_semantic::SymbolId; +use oxc_span::Span; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +#[derive(Debug, Default, Clone)] +pub struct NoNonNullAssertedNullishCoalescing; + +declare_oxc_lint!( + /// ### What it does + /// Disallow non-null assertions in the left operand of a nullish coalescing operator. + /// + /// ### Why is this bad? + /// The ?? nullish coalescing runtime operator allows providing a default value when dealing with null or undefined. Using a ! non-null assertion type operator in the left operand of a nullish coalescing operator is redundant, and likely a sign of programmer error or confusion over the two operators. + /// + /// ### Example + /// ```javascript + /// foo! ?? bar; + /// + /// let x: string; + /// x! ?? ''; + /// ``` + NoNonNullAssertedNullishCoalescing, + restriction, +); + +fn no_non_null_asserted_nullish_coalescing_diagnostic(span0: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("typescript-eslint(no-non-null-asserted-nullish-coalescing): 'Disallow non-null assertions in the left operand of a nullish coalescing operator") + .with_help("The nullish coalescing operator is designed to handle undefined and null - using a non-null assertion is not needed.") + .with_labels([span0.into()]) +} + +impl Rule for NoNonNullAssertedNullishCoalescing { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::LogicalExpression(expr) = node.kind() else { return }; + let Expression::TSNonNullExpression(ts_non_null_expr) = &expr.left else { return }; + if let Expression::Identifier(ident) = &ts_non_null_expr.expression { + if let Some(symbol_id) = ctx.scopes().get_binding(node.scope_id(), &ident.name) { + if !has_assignment_before_node(symbol_id, ctx, expr.span.end) { + return; + } + } + } + + ctx.diagnostic(no_non_null_asserted_nullish_coalescing_diagnostic(ts_non_null_expr.span)); + } +} +fn has_assignment_before_node( + symbol_id: SymbolId, + ctx: &LintContext, + parent_span_end: u32, +) -> bool { + let symbol_table = ctx.semantic().symbols(); + + for reference in symbol_table.get_resolved_references(symbol_id) { + if reference.is_write() && reference.span().end < parent_span_end { + return true; + } + } + + let declaration_id = symbol_table.get_declaration(symbol_id); + let AstKind::VariableDeclarator(decl) = ctx.nodes().kind(declaration_id) else { + return false; + }; + decl.definite || decl.init.is_some() +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + "foo ?? bar;", + "foo ?? bar!;", + "foo.bazz ?? bar;", + "foo.bazz ?? bar!;", + "foo!.bazz ?? bar;", + "foo!.bazz ?? bar!;", + "foo() ?? bar;", + "foo() ?? bar!;", + "(foo ?? bar)!;", + " + let x: string; + x! ?? ''; + ", + " + let x: string; + x ?? ''; + ", + " + let x!: string; + x ?? ''; + ", + " + let x: string; + foo(x); + x! ?? ''; + ", + " + let x: string; + x! ?? ''; + x = foo(); + ", + " + let x: string; + foo(x); + x! ?? ''; + x = foo(); + ", + " + let x = foo(); + x ?? ''; + ", + " + function foo() { + let x: string; + return x ?? ''; + } + ", + " + let x: string; + function foo() { + return x ?? ''; + } + ", + ]; + + let fail = vec![ + "foo! ?? bar;", + "foo! ?? bar!;", + "foo.bazz! ?? bar;", + "foo.bazz! ?? bar!;", + "foo!.bazz! ?? bar;", + "foo!.bazz! ?? bar!;", + "foo()! ?? bar;", + "foo()! ?? bar!;", + " + let x!: string; + x! ?? ''; + ", + " + let x: string; + x = foo(); + x! ?? ''; + ", + " + let x: string; + x = foo(); + x! ?? ''; + x = foo(); + ", + " + let x = foo(); + x! ?? ''; + ", + " + function foo() { + let x!: string; + return x! ?? ''; + } + ", + " + let x!: string; + function foo() { + return x! ?? ''; + } + ", + " + let x = foo(); + x ! ?? ''; + ", + ]; + + Tester::new(NoNonNullAssertedNullishCoalescing::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_non_null_asserted_nullish_coalescing.snap b/crates/oxc_linter/src/snapshots/no_non_null_asserted_nullish_coalescing.snap new file mode 100644 index 0000000000000..bf8bb3d46497e --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_non_null_asserted_nullish_coalescing.snap @@ -0,0 +1,121 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ typescript-eslint(no-non-null-asserted-nullish-coalescing): 'Disallow non-null assertions in the left operand of a nullish coalescing operator + ╭─[no_non_null_asserted_nullish_coalescing.tsx:1:1] + 1 │ foo! ?? bar; + · ──── + ╰──── + help: The nullish coalescing operator is designed to handle undefined and null - using a non-null assertion is not needed. + + ⚠ typescript-eslint(no-non-null-asserted-nullish-coalescing): 'Disallow non-null assertions in the left operand of a nullish coalescing operator + ╭─[no_non_null_asserted_nullish_coalescing.tsx:1:1] + 1 │ foo! ?? bar!; + · ──── + ╰──── + help: The nullish coalescing operator is designed to handle undefined and null - using a non-null assertion is not needed. + + ⚠ typescript-eslint(no-non-null-asserted-nullish-coalescing): 'Disallow non-null assertions in the left operand of a nullish coalescing operator + ╭─[no_non_null_asserted_nullish_coalescing.tsx:1:1] + 1 │ foo.bazz! ?? bar; + · ───────── + ╰──── + help: The nullish coalescing operator is designed to handle undefined and null - using a non-null assertion is not needed. + + ⚠ typescript-eslint(no-non-null-asserted-nullish-coalescing): 'Disallow non-null assertions in the left operand of a nullish coalescing operator + ╭─[no_non_null_asserted_nullish_coalescing.tsx:1:1] + 1 │ foo.bazz! ?? bar!; + · ───────── + ╰──── + help: The nullish coalescing operator is designed to handle undefined and null - using a non-null assertion is not needed. + + ⚠ typescript-eslint(no-non-null-asserted-nullish-coalescing): 'Disallow non-null assertions in the left operand of a nullish coalescing operator + ╭─[no_non_null_asserted_nullish_coalescing.tsx:1:1] + 1 │ foo!.bazz! ?? bar; + · ────────── + ╰──── + help: The nullish coalescing operator is designed to handle undefined and null - using a non-null assertion is not needed. + + ⚠ typescript-eslint(no-non-null-asserted-nullish-coalescing): 'Disallow non-null assertions in the left operand of a nullish coalescing operator + ╭─[no_non_null_asserted_nullish_coalescing.tsx:1:1] + 1 │ foo!.bazz! ?? bar!; + · ────────── + ╰──── + help: The nullish coalescing operator is designed to handle undefined and null - using a non-null assertion is not needed. + + ⚠ typescript-eslint(no-non-null-asserted-nullish-coalescing): 'Disallow non-null assertions in the left operand of a nullish coalescing operator + ╭─[no_non_null_asserted_nullish_coalescing.tsx:1:1] + 1 │ foo()! ?? bar; + · ────── + ╰──── + help: The nullish coalescing operator is designed to handle undefined and null - using a non-null assertion is not needed. + + ⚠ typescript-eslint(no-non-null-asserted-nullish-coalescing): 'Disallow non-null assertions in the left operand of a nullish coalescing operator + ╭─[no_non_null_asserted_nullish_coalescing.tsx:1:1] + 1 │ foo()! ?? bar!; + · ────── + ╰──── + help: The nullish coalescing operator is designed to handle undefined and null - using a non-null assertion is not needed. + + ⚠ typescript-eslint(no-non-null-asserted-nullish-coalescing): 'Disallow non-null assertions in the left operand of a nullish coalescing operator + ╭─[no_non_null_asserted_nullish_coalescing.tsx:3:10] + 2 │ let x!: string; + 3 │ x! ?? ''; + · ── + 4 │ + ╰──── + help: The nullish coalescing operator is designed to handle undefined and null - using a non-null assertion is not needed. + + ⚠ typescript-eslint(no-non-null-asserted-nullish-coalescing): 'Disallow non-null assertions in the left operand of a nullish coalescing operator + ╭─[no_non_null_asserted_nullish_coalescing.tsx:4:10] + 3 │ x = foo(); + 4 │ x! ?? ''; + · ── + 5 │ + ╰──── + help: The nullish coalescing operator is designed to handle undefined and null - using a non-null assertion is not needed. + + ⚠ typescript-eslint(no-non-null-asserted-nullish-coalescing): 'Disallow non-null assertions in the left operand of a nullish coalescing operator + ╭─[no_non_null_asserted_nullish_coalescing.tsx:4:10] + 3 │ x = foo(); + 4 │ x! ?? ''; + · ── + 5 │ x = foo(); + ╰──── + help: The nullish coalescing operator is designed to handle undefined and null - using a non-null assertion is not needed. + + ⚠ typescript-eslint(no-non-null-asserted-nullish-coalescing): 'Disallow non-null assertions in the left operand of a nullish coalescing operator + ╭─[no_non_null_asserted_nullish_coalescing.tsx:3:10] + 2 │ let x = foo(); + 3 │ x! ?? ''; + · ── + 4 │ + ╰──── + help: The nullish coalescing operator is designed to handle undefined and null - using a non-null assertion is not needed. + + ⚠ typescript-eslint(no-non-null-asserted-nullish-coalescing): 'Disallow non-null assertions in the left operand of a nullish coalescing operator + ╭─[no_non_null_asserted_nullish_coalescing.tsx:4:19] + 3 │ let x!: string; + 4 │ return x! ?? ''; + · ── + 5 │ } + ╰──── + help: The nullish coalescing operator is designed to handle undefined and null - using a non-null assertion is not needed. + + ⚠ typescript-eslint(no-non-null-asserted-nullish-coalescing): 'Disallow non-null assertions in the left operand of a nullish coalescing operator + ╭─[no_non_null_asserted_nullish_coalescing.tsx:4:19] + 3 │ function foo() { + 4 │ return x! ?? ''; + · ── + 5 │ } + ╰──── + help: The nullish coalescing operator is designed to handle undefined and null - using a non-null assertion is not needed. + + ⚠ typescript-eslint(no-non-null-asserted-nullish-coalescing): 'Disallow non-null assertions in the left operand of a nullish coalescing operator + ╭─[no_non_null_asserted_nullish_coalescing.tsx:3:10] + 2 │ let x = foo(); + 3 │ x ! ?? ''; + · ──── + 4 │ + ╰──── + help: The nullish coalescing operator is designed to handle undefined and null - using a non-null assertion is not needed.