From a9e62299a065f514a70b898a538770a94f1f75ff Mon Sep 17 00:00:00 2001 From: kaykdm Date: Sat, 29 Jun 2024 22:06:34 +0900 Subject: [PATCH] feat(linter): implement @typescript-eslint/no-dynamic-delete --- crates/oxc_linter/src/rules.rs | 2 + .../src/rules/typescript/no_dynamic_delete.rs | 164 ++++++++++++++++++ .../src/snapshots/no_dynamic_delete.snap | 92 ++++++++++ 3 files changed, 258 insertions(+) create mode 100644 crates/oxc_linter/src/rules/typescript/no_dynamic_delete.rs create mode 100644 crates/oxc_linter/src/snapshots/no_dynamic_delete.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index b200941cf1d1c..a15199d586a9f 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -131,6 +131,7 @@ mod typescript { pub mod consistent_type_definitions; pub mod explicit_function_return_type; pub mod no_duplicate_enum_values; + pub mod no_dynamic_delete; pub mod no_empty_interface; pub mod no_explicit_any; pub mod no_extra_non_null_assertion; @@ -547,6 +548,7 @@ oxc_macros::declare_all_lint_rules! { typescript::explicit_function_return_type, typescript::no_non_null_assertion, typescript::no_non_null_asserted_nullish_coalescing, + typescript::no_dynamic_delete, jest::expect_expect, jest::max_expects, jest::max_nested_describe, diff --git a/crates/oxc_linter/src/rules/typescript/no_dynamic_delete.rs b/crates/oxc_linter/src/rules/typescript/no_dynamic_delete.rs new file mode 100644 index 0000000000000..03ada775f213a --- /dev/null +++ b/crates/oxc_linter/src/rules/typescript/no_dynamic_delete.rs @@ -0,0 +1,164 @@ +use oxc_ast::{ast::Expression, AstKind}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; +use oxc_syntax::operator::UnaryOperator; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +#[derive(Debug, Default, Clone)] +pub struct NoDynamicDelete; + +declare_oxc_lint!( + /// ### What it does + /// Disallow using the delete operator on computed key expressions. + /// + /// ### Why is this bad? + /// Deleting dynamically computed keys can be dangerous and in some cases not well optimized. + /// Using the delete operator on keys that aren't runtime constants could be a sign that you're using the wrong data structures. + /// Consider using a Map or Set if you’re using an object as a key-value collection. + /// + /// ### Example + /// ```javascript + /// const container: { [i: string]: 0 } = {}; + /// delete container['aa' + 'b']; + /// ``` + NoDynamicDelete, + restriction, +); + +fn no_dynamic_delete_diagnostic(span0: Span) -> OxcDiagnostic { + OxcDiagnostic::warn( + "typescript-eslint(no-dynamic-delete): Do not delete dynamically computed property keys.", + ) + .with_help("Disallow using the `delete` operator on computed key expressions") + .with_label(span0) +} + +impl Rule for NoDynamicDelete { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::UnaryExpression(expr) = node.kind() else { return }; + if !matches!(expr.operator, UnaryOperator::Delete) { + return; + } + + let Expression::ComputedMemberExpression(computed_expr) = &expr.argument else { return }; + let inner_expression = computed_expr.expression.get_inner_expression(); + if inner_expression.is_string_literal() || inner_expression.is_number_literal() { + return; + } + + if let Expression::UnaryExpression(unary_expr) = &inner_expression { + if unary_expr.operator == UnaryOperator::UnaryNegation + && unary_expr.argument.is_number_literal() + { + return; + } + } + ctx.diagnostic(no_dynamic_delete_diagnostic(expr.span)); + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + " + const container: { [i: string]: 0 } = {}; + delete container.aaa; + ", + " + const container: { [i: string]: 0 } = {}; + delete container.delete; + ", + " + const container: { [i: string]: 0 } = {}; + delete container[7]; + ", + " + const container: { [i: string]: 0 } = {}; + delete container[-7]; + ", + " + const container: { [i: string]: 0 } = {}; + delete container['-Infinity']; + ", + " + const container: { [i: string]: 0 } = {}; + delete container['+Infinity']; + ", + " + const value = 1; + delete value; + ", + " + const value = 1; + delete -value; + ", + " + const container: { [i: string]: 0 } = {}; + delete container['aaa']; + ", + " + const container: { [i: string]: 0 } = {}; + delete container['delete']; + ", + " + const container: { [i: string]: 0 } = {}; + delete container['NaN']; + ", + " + const container = {}; + delete container[('aaa')] + ", + ]; + + let fail = vec![ + " + const container: { [i: string]: 0 } = {}; + delete container['aa' + 'b']; + ", + " + const container: { [i: string]: 0 } = {}; + delete container[+7]; + ", + " + const container: { [i: string]: 0 } = {}; + delete container[-Infinity]; + ", + " + const container: { [i: string]: 0 } = {}; + delete container[+Infinity]; + ", + " + const container: { [i: string]: 0 } = {}; + delete container[NaN]; + ", + " + const container: { [i: string]: 0 } = {}; + const name = 'name'; + delete container[name]; + ", + " + const container: { [i: string]: 0 } = {}; + const getName = () => 'aaa'; + delete container[getName()]; + ", + " + const container: { [i: string]: 0 } = {}; + const name = { foo: { bar: 'bar' } }; + delete container[name.foo.bar]; + ", + " + const container: { [i: string]: 0 } = {}; + delete container[+'Infinity']; + ", + " + const container: { [i: string]: 0 } = {}; + delete container[typeof 1]; + ", + ]; + + Tester::new(NoDynamicDelete::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_dynamic_delete.snap b/crates/oxc_linter/src/snapshots/no_dynamic_delete.snap new file mode 100644 index 0000000000000..e6a4133c24e30 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_dynamic_delete.snap @@ -0,0 +1,92 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ typescript-eslint(no-dynamic-delete): Do not delete dynamically computed property keys. + ╭─[no_dynamic_delete.tsx:3:10] + 2 │ const container: { [i: string]: 0 } = {}; + 3 │ delete container['aa' + 'b']; + · ──────────────────────────── + 4 │ + ╰──── + help: Disallow using the `delete` operator on computed key expressions + + ⚠ typescript-eslint(no-dynamic-delete): Do not delete dynamically computed property keys. + ╭─[no_dynamic_delete.tsx:3:10] + 2 │ const container: { [i: string]: 0 } = {}; + 3 │ delete container[+7]; + · ──────────────────── + 4 │ + ╰──── + help: Disallow using the `delete` operator on computed key expressions + + ⚠ typescript-eslint(no-dynamic-delete): Do not delete dynamically computed property keys. + ╭─[no_dynamic_delete.tsx:3:10] + 2 │ const container: { [i: string]: 0 } = {}; + 3 │ delete container[-Infinity]; + · ─────────────────────────── + 4 │ + ╰──── + help: Disallow using the `delete` operator on computed key expressions + + ⚠ typescript-eslint(no-dynamic-delete): Do not delete dynamically computed property keys. + ╭─[no_dynamic_delete.tsx:3:10] + 2 │ const container: { [i: string]: 0 } = {}; + 3 │ delete container[+Infinity]; + · ─────────────────────────── + 4 │ + ╰──── + help: Disallow using the `delete` operator on computed key expressions + + ⚠ typescript-eslint(no-dynamic-delete): Do not delete dynamically computed property keys. + ╭─[no_dynamic_delete.tsx:3:10] + 2 │ const container: { [i: string]: 0 } = {}; + 3 │ delete container[NaN]; + · ───────────────────── + 4 │ + ╰──── + help: Disallow using the `delete` operator on computed key expressions + + ⚠ typescript-eslint(no-dynamic-delete): Do not delete dynamically computed property keys. + ╭─[no_dynamic_delete.tsx:4:10] + 3 │ const name = 'name'; + 4 │ delete container[name]; + · ────────────────────── + 5 │ + ╰──── + help: Disallow using the `delete` operator on computed key expressions + + ⚠ typescript-eslint(no-dynamic-delete): Do not delete dynamically computed property keys. + ╭─[no_dynamic_delete.tsx:4:10] + 3 │ const getName = () => 'aaa'; + 4 │ delete container[getName()]; + · ─────────────────────────── + 5 │ + ╰──── + help: Disallow using the `delete` operator on computed key expressions + + ⚠ typescript-eslint(no-dynamic-delete): Do not delete dynamically computed property keys. + ╭─[no_dynamic_delete.tsx:4:10] + 3 │ const name = { foo: { bar: 'bar' } }; + 4 │ delete container[name.foo.bar]; + · ────────────────────────────── + 5 │ + ╰──── + help: Disallow using the `delete` operator on computed key expressions + + ⚠ typescript-eslint(no-dynamic-delete): Do not delete dynamically computed property keys. + ╭─[no_dynamic_delete.tsx:3:10] + 2 │ const container: { [i: string]: 0 } = {}; + 3 │ delete container[+'Infinity']; + · ───────────────────────────── + 4 │ + ╰──── + help: Disallow using the `delete` operator on computed key expressions + + ⚠ typescript-eslint(no-dynamic-delete): Do not delete dynamically computed property keys. + ╭─[no_dynamic_delete.tsx:3:10] + 2 │ const container: { [i: string]: 0 } = {}; + 3 │ delete container[typeof 1]; + · ────────────────────────── + 4 │ + ╰──── + help: Disallow using the `delete` operator on computed key expressions