From 2c49a0a5e858196875a8df44d8799cac15be16ba Mon Sep 17 00:00:00 2001 From: Victorien Elvinger Date: Thu, 1 Dec 2022 19:15:08 +0100 Subject: [PATCH] feat(rome_js_analyze): noSwitchDeclarations --- .../src/categories.rs | 1 + .../rome_js_analyze/src/analyzers/nursery.rs | 3 +- .../nursery/no_switch_declarations.rs | 149 +++++++++ .../noSwitchDeclarations/invalid.jsonc | 10 + .../noSwitchDeclarations/invalid.jsonc.snap | 222 ++++++++++++++ .../nursery/noSwitchDeclarations/invalid.ts | 37 +++ .../noSwitchDeclarations/invalid.ts.snap | 286 ++++++++++++++++++ .../nursery/noSwitchDeclarations/valid.jsonc | 6 + .../noSwitchDeclarations/valid.jsonc.snap | 26 ++ crates/rome_js_syntax/src/stmt_ext.rs | 28 +- crates/rome_rowan/src/syntax/token.rs | 16 + .../src/configuration/linter/rules.rs | 131 ++++---- .../src/configuration/parse/json/rules.rs | 19 ++ editors/vscode/configuration_schema.json | 7 + npm/backend-jsonrpc/src/workspace.ts | 5 + npm/rome/configuration_schema.json | 7 + website/src/pages/lint/rules/index.mdx | 6 + .../pages/lint/rules/noSwitchDeclarations.md | 185 +++++++++++ 18 files changed, 1084 insertions(+), 60 deletions(-) create mode 100644 crates/rome_js_analyze/src/analyzers/nursery/no_switch_declarations.rs create mode 100644 crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/invalid.jsonc create mode 100644 crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/invalid.jsonc.snap create mode 100644 crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/invalid.ts create mode 100644 crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/invalid.ts.snap create mode 100644 crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/valid.jsonc create mode 100644 crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/valid.jsonc.snap create mode 100644 website/src/pages/lint/rules/noSwitchDeclarations.md diff --git a/crates/rome_diagnostics_categories/src/categories.rs b/crates/rome_diagnostics_categories/src/categories.rs index 2d9c8042462..95f01416c10 100644 --- a/crates/rome_diagnostics_categories/src/categories.rs +++ b/crates/rome_diagnostics_categories/src/categories.rs @@ -71,6 +71,7 @@ define_dategories! { "lint/nursery/noSelfCompare": "https://docs.rome.tools/lint/rules/noSelfCompare", "lint/nursery/noSetterReturn": "https://docs.rome.tools/lint/rules/noSetterReturn", "lint/nursery/noStringCaseMismatch": "https://docs.rome.tools/lint/rules/noStringCaseMismatch", + "lint/nursery/noSwitchDeclarations": "https://docs.rome.tools/lint/rules/noSwitchDeclarations", "lint/nursery/noUnreachableSuper": "https://rome.tools/docs/lint/rules/noUnreachableSuper", "lint/nursery/noUnsafeFinally": "https://docs.rome.tools/lint/rules/noUnsafeFinally", "lint/nursery/noUnusedLabels": "https://docs.rome.tools/lint/rules/noUnusedLabels", diff --git a/crates/rome_js_analyze/src/analyzers/nursery.rs b/crates/rome_js_analyze/src/analyzers/nursery.rs index 259026926cd..bf21aed3974 100644 --- a/crates/rome_js_analyze/src/analyzers/nursery.rs +++ b/crates/rome_js_analyze/src/analyzers/nursery.rs @@ -30,6 +30,7 @@ mod no_redundant_use_strict; mod no_self_compare; mod no_setter_return; mod no_string_case_mismatch; +mod no_switch_declarations; mod no_unreachable_super; mod no_unsafe_finally; mod no_unused_labels; @@ -45,4 +46,4 @@ mod use_is_nan; mod use_media_caption; mod use_numeric_literals; mod use_yield; -declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_access_key :: NoAccessKey , self :: no_assign_in_expressions :: NoAssignInExpressions , self :: no_banned_types :: NoBannedTypes , self :: no_comma_operator :: NoCommaOperator , self :: no_confusing_labels :: NoConfusingLabels , self :: no_const_enum :: NoConstEnum , self :: no_constructor_return :: NoConstructorReturn , self :: no_distracting_elements :: NoDistractingElements , self :: no_duplicate_case :: NoDuplicateCase , self :: no_duplicate_class_members :: NoDuplicateClassMembers , self :: no_duplicate_jsx_props :: NoDuplicateJsxProps , self :: no_duplicate_object_keys :: NoDuplicateObjectKeys , self :: no_empty_interface :: NoEmptyInterface , self :: no_extra_labels :: NoExtraLabels , self :: no_extra_non_null_assertion :: NoExtraNonNullAssertion , self :: no_extra_semicolons :: NoExtraSemicolons , self :: no_global_object_calls :: NoGlobalObjectCalls , self :: no_header_scope :: NoHeaderScope , self :: no_inferrable_types :: NoInferrableTypes , self :: no_inner_declarations :: NoInnerDeclarations , self :: no_invalid_constructor_super :: NoInvalidConstructorSuper , self :: no_non_null_assertion :: NoNonNullAssertion , self :: no_precision_loss :: NoPrecisionLoss , self :: no_prototype_builtins :: NoPrototypeBuiltins , self :: no_redundant_alt :: NoRedundantAlt , self :: no_redundant_use_strict :: NoRedundantUseStrict , self :: no_self_compare :: NoSelfCompare , self :: no_setter_return :: NoSetterReturn , self :: no_string_case_mismatch :: NoStringCaseMismatch , self :: no_unreachable_super :: NoUnreachableSuper , self :: no_unsafe_finally :: NoUnsafeFinally , self :: no_unused_labels :: NoUnusedLabels , self :: no_useless_rename :: NoUselessRename , self :: no_useless_switch_case :: NoUselessSwitchCase , self :: no_void_type_return :: NoVoidTypeReturn , self :: no_with :: NoWith , self :: use_default_parameter_last :: UseDefaultParameterLast , self :: use_default_switch_clause_last :: UseDefaultSwitchClauseLast , self :: use_enum_initializers :: UseEnumInitializers , self :: use_exponentiation_operator :: UseExponentiationOperator , self :: use_is_nan :: UseIsNan , self :: use_media_caption :: UseMediaCaption , self :: use_numeric_literals :: UseNumericLiterals , self :: use_yield :: UseYield ,] } } +declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_access_key :: NoAccessKey , self :: no_assign_in_expressions :: NoAssignInExpressions , self :: no_banned_types :: NoBannedTypes , self :: no_comma_operator :: NoCommaOperator , self :: no_confusing_labels :: NoConfusingLabels , self :: no_const_enum :: NoConstEnum , self :: no_constructor_return :: NoConstructorReturn , self :: no_distracting_elements :: NoDistractingElements , self :: no_duplicate_case :: NoDuplicateCase , self :: no_duplicate_class_members :: NoDuplicateClassMembers , self :: no_duplicate_jsx_props :: NoDuplicateJsxProps , self :: no_duplicate_object_keys :: NoDuplicateObjectKeys , self :: no_empty_interface :: NoEmptyInterface , self :: no_extra_labels :: NoExtraLabels , self :: no_extra_non_null_assertion :: NoExtraNonNullAssertion , self :: no_extra_semicolons :: NoExtraSemicolons , self :: no_global_object_calls :: NoGlobalObjectCalls , self :: no_header_scope :: NoHeaderScope , self :: no_inferrable_types :: NoInferrableTypes , self :: no_inner_declarations :: NoInnerDeclarations , self :: no_invalid_constructor_super :: NoInvalidConstructorSuper , self :: no_non_null_assertion :: NoNonNullAssertion , self :: no_precision_loss :: NoPrecisionLoss , self :: no_prototype_builtins :: NoPrototypeBuiltins , self :: no_redundant_alt :: NoRedundantAlt , self :: no_redundant_use_strict :: NoRedundantUseStrict , self :: no_self_compare :: NoSelfCompare , self :: no_setter_return :: NoSetterReturn , self :: no_string_case_mismatch :: NoStringCaseMismatch , self :: no_switch_declarations :: NoSwitchDeclarations , self :: no_unreachable_super :: NoUnreachableSuper , self :: no_unsafe_finally :: NoUnsafeFinally , self :: no_unused_labels :: NoUnusedLabels , self :: no_useless_rename :: NoUselessRename , self :: no_useless_switch_case :: NoUselessSwitchCase , self :: no_void_type_return :: NoVoidTypeReturn , self :: no_with :: NoWith , self :: use_default_parameter_last :: UseDefaultParameterLast , self :: use_default_switch_clause_last :: UseDefaultSwitchClauseLast , self :: use_enum_initializers :: UseEnumInitializers , self :: use_exponentiation_operator :: UseExponentiationOperator , self :: use_is_nan :: UseIsNan , self :: use_media_caption :: UseMediaCaption , self :: use_numeric_literals :: UseNumericLiterals , self :: use_yield :: UseYield ,] } } diff --git a/crates/rome_js_analyze/src/analyzers/nursery/no_switch_declarations.rs b/crates/rome_js_analyze/src/analyzers/nursery/no_switch_declarations.rs new file mode 100644 index 00000000000..ce1f9bc8bd5 --- /dev/null +++ b/crates/rome_js_analyze/src/analyzers/nursery/no_switch_declarations.rs @@ -0,0 +1,149 @@ +use std::iter; + +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_factory::make; +use rome_js_syntax::{ + AnyJsDeclaration, AnyJsStatement, AnyJsSwitchClause, JsSyntaxNode, JsVariableStatement, + TriviaPieceKind, T, +}; +use rome_rowan::{AstNode, BatchMutationExt}; + +use crate::JsRuleAction; + +declare_rule! { + /// Disallow lexical declarations in `switch` clauses. + /// + /// Lexical declarations in `switch` clauses are accessible in the entire `switch`. + /// However, it only gets initialized when it is assigned, which will only happen if the `switch` clause where it is defined is reached. + /// + /// To ensure that the lexical declarations only apply to the current `switch` clause wrap your declarations in a block. + /// + /// Source: https://eslint.org/docs/latest/rules/no-case-declarations + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```js,expect_diagnostic + /// switch (foo) { + /// case 0: + /// const x = 1; + /// break; + /// case 2: + /// x; // `x` can be used while it is not initialized + /// break; + /// } + /// ``` + /// + /// ```js,expect_diagnostic + /// switch (foo) { + /// case 0: + /// function f() {} + /// break; + /// case 2: + /// f(); // `f` can be called here + /// break; + /// } + /// ``` + /// + /// ```js,expect_diagnostic + /// switch (foo) { + /// case 0: + /// class A {} + /// break; + /// default: + /// new A(); // `A` can be instantiated here + /// break; + /// } + /// ``` + /// + /// ### Valid + /// + /// ```js + /// switch (foo) { + /// case 0: { + /// const x = 1; + /// break; + /// } + /// case 1: + /// // `x` is not visible here + /// break; + /// } + /// ``` + /// + pub(crate) NoSwitchDeclarations { + version: "12.0.0", + name: "noSwitchDeclarations", + recommended: true, + } +} + +fn declaration_cast(node: JsSyntaxNode) -> Option { + if JsVariableStatement::can_cast(node.kind()) { + Some(AnyJsDeclaration::JsVariableDeclaration( + JsVariableStatement::cast(node)?.declaration().ok()?, + )) + } else { + AnyJsDeclaration::cast(node) + } +} + +impl Rule for NoSwitchDeclarations { + type Query = Ast; + type State = AnyJsDeclaration; + type Signals = Vec; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let switch_clause = ctx.query(); + switch_clause + .consequent() + .syntax() + .children() + .filter_map(declaration_cast) + .collect() + } + + fn diagnostic(ctx: &RuleContext, decl: &Self::State) -> Option { + let switch_clause = ctx.query(); + Some(RuleDiagnostic::new( + rule_category!(), + decl.range(), + markup! { + "Other switch clauses can erroneously access this ""declaration"".\nWrap the declaration in a block to restrict its access to the switch clause." + }, + ).detail(switch_clause.range(), markup! { + "The declaration is defined in this ""switch clause"":" + })) + } + + fn action(ctx: &RuleContext, _: &Self::State) -> Option { + let switch_clause = ctx.query(); + let clause_token = switch_clause.clause_token().ok()?; + let colon_token = switch_clause.colon_token().ok()?; + let consequent = switch_clause.consequent(); + let new_colon_token = colon_token.with_trailing_trivia(iter::empty()); + let new_consequent = make::js_statement_list(Some(AnyJsStatement::JsBlockStatement( + make::js_block_statement( + make::token(T!['{']) + .with_leading_trivia(Some((TriviaPieceKind::Whitespace, " "))) + .with_trailing_trivia_pieces(colon_token.trailing_trivia().pieces()), + consequent.to_owned(), + make::token(T!['}']).with_leading_trivia_pieces(clause_token.indentation_trivia()), + ), + ))); + let mut mutation = ctx.root().begin(); + mutation.replace_token_discard_trivia(colon_token, new_colon_token); + mutation.replace_node_discard_trivia(consequent, new_consequent); + Some(JsRuleAction { + category: ActionCategory::QuickFix, + applicability: Applicability::MaybeIncorrect, + message: markup! { "Wrap the ""declaration"" in a block." } + .to_owned(), + mutation, + }) + } +} diff --git a/crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/invalid.jsonc b/crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/invalid.jsonc new file mode 100644 index 00000000000..e10682fde64 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/invalid.jsonc @@ -0,0 +1,10 @@ +[ + "switch (a) { case 1: let x = 1; break; }", + "switch (a) { default: let x = 2; break; }", + "switch (a) { case 1: const x = 1; break; }", + "switch (a) { default: const x = 2; break; }", + "switch (a) { case 1: function f() {} break; }", + "switch (a) { default: function f() {} break; }", + "switch (a) { case 1: class C {} break; }", + "switch (a) { default: class C {} break; }" +] diff --git a/crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/invalid.jsonc.snap b/crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/invalid.jsonc.snap new file mode 100644 index 00000000000..f762df22a27 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/invalid.jsonc.snap @@ -0,0 +1,222 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +assertion_line: 92 +expression: invalid.jsonc +--- +# Input +```js +switch (a) { case 1: let x = 1; break; } +``` + +# Diagnostics +``` +invalid.jsonc:1:22 lint/nursery/noSwitchDeclarations FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Other switch clauses can erroneously access this declaration. + Wrap the declaration in a block to restrict its access to the switch clause. + + > 1 │ switch (a) { case 1: let x = 1; break; } + │ ^^^^^^^^^ + + i The declaration is defined in this switch clause: + + > 1 │ switch (a) { case 1: let x = 1; break; } + │ ^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Suggested fix: Wrap the declaration in a block. + + 1 │ switch·(a)·{·case·1:·{·let·x·=·1;·break;·}} + │ ++ + + +``` + +# Input +```js +switch (a) { default: let x = 2; break; } +``` + +# Diagnostics +``` +invalid.jsonc:1:23 lint/nursery/noSwitchDeclarations FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Other switch clauses can erroneously access this declaration. + Wrap the declaration in a block to restrict its access to the switch clause. + + > 1 │ switch (a) { default: let x = 2; break; } + │ ^^^^^^^^^ + + i The declaration is defined in this switch clause: + + > 1 │ switch (a) { default: let x = 2; break; } + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Suggested fix: Wrap the declaration in a block. + + 1 │ switch·(a)·{·default:·{·let·x·=·2;·break;·}} + │ ++ + + +``` + +# Input +```js +switch (a) { case 1: const x = 1; break; } +``` + +# Diagnostics +``` +invalid.jsonc:1:22 lint/nursery/noSwitchDeclarations FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Other switch clauses can erroneously access this declaration. + Wrap the declaration in a block to restrict its access to the switch clause. + + > 1 │ switch (a) { case 1: const x = 1; break; } + │ ^^^^^^^^^^^ + + i The declaration is defined in this switch clause: + + > 1 │ switch (a) { case 1: const x = 1; break; } + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Suggested fix: Wrap the declaration in a block. + + 1 │ switch·(a)·{·case·1:·{·const·x·=·1;·break;·}} + │ ++ + + +``` + +# Input +```js +switch (a) { default: const x = 2; break; } +``` + +# Diagnostics +``` +invalid.jsonc:1:23 lint/nursery/noSwitchDeclarations FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Other switch clauses can erroneously access this declaration. + Wrap the declaration in a block to restrict its access to the switch clause. + + > 1 │ switch (a) { default: const x = 2; break; } + │ ^^^^^^^^^^^ + + i The declaration is defined in this switch clause: + + > 1 │ switch (a) { default: const x = 2; break; } + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Suggested fix: Wrap the declaration in a block. + + 1 │ switch·(a)·{·default:·{·const·x·=·2;·break;·}} + │ ++ + + +``` + +# Input +```js +switch (a) { case 1: function f() {} break; } +``` + +# Diagnostics +``` +invalid.jsonc:1:22 lint/nursery/noSwitchDeclarations FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Other switch clauses can erroneously access this declaration. + Wrap the declaration in a block to restrict its access to the switch clause. + + > 1 │ switch (a) { case 1: function f() {} break; } + │ ^^^^^^^^^^^^^^^ + + i The declaration is defined in this switch clause: + + > 1 │ switch (a) { case 1: function f() {} break; } + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Suggested fix: Wrap the declaration in a block. + + 1 │ switch·(a)·{·case·1:·{·function·f()·{}·break;·}} + │ ++ + + +``` + +# Input +```js +switch (a) { default: function f() {} break; } +``` + +# Diagnostics +``` +invalid.jsonc:1:23 lint/nursery/noSwitchDeclarations FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Other switch clauses can erroneously access this declaration. + Wrap the declaration in a block to restrict its access to the switch clause. + + > 1 │ switch (a) { default: function f() {} break; } + │ ^^^^^^^^^^^^^^^ + + i The declaration is defined in this switch clause: + + > 1 │ switch (a) { default: function f() {} break; } + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Suggested fix: Wrap the declaration in a block. + + 1 │ switch·(a)·{·default:·{·function·f()·{}·break;·}} + │ ++ + + +``` + +# Input +```js +switch (a) { case 1: class C {} break; } +``` + +# Diagnostics +``` +invalid.jsonc:1:22 lint/nursery/noSwitchDeclarations FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Other switch clauses can erroneously access this declaration. + Wrap the declaration in a block to restrict its access to the switch clause. + + > 1 │ switch (a) { case 1: class C {} break; } + │ ^^^^^^^^^^ + + i The declaration is defined in this switch clause: + + > 1 │ switch (a) { case 1: class C {} break; } + │ ^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Suggested fix: Wrap the declaration in a block. + + 1 │ switch·(a)·{·case·1:·{·class·C·{}·break;·}} + │ ++ + + +``` + +# Input +```js +switch (a) { default: class C {} break; } +``` + +# Diagnostics +``` +invalid.jsonc:1:23 lint/nursery/noSwitchDeclarations FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Other switch clauses can erroneously access this declaration. + Wrap the declaration in a block to restrict its access to the switch clause. + + > 1 │ switch (a) { default: class C {} break; } + │ ^^^^^^^^^^ + + i The declaration is defined in this switch clause: + + > 1 │ switch (a) { default: class C {} break; } + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Suggested fix: Wrap the declaration in a block. + + 1 │ switch·(a)·{·default:·{·class·C·{}·break;·}} + │ ++ + + +``` + + diff --git a/crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/invalid.ts b/crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/invalid.ts new file mode 100644 index 00000000000..c18036d4ec6 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/invalid.ts @@ -0,0 +1,37 @@ +switch (1) { + case 1: + enum E {} + break; +} + +switch (1) { + default: + enum E {} + break; +} + +switch (1) { + case 1: + interface I {} + break; +} + +switch (1) { + default: + interface I {} + { + } + break; +} + +switch (1) { + case 1: + type N = number; + break; +} + +switch (1) { + default: + type N = number; + break; +} diff --git a/crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/invalid.ts.snap b/crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/invalid.ts.snap new file mode 100644 index 00000000000..822352ad59c --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/invalid.ts.snap @@ -0,0 +1,286 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +assertion_line: 92 +expression: invalid.ts +--- +# Input +```js +switch (1) { + case 1: + enum E {} + break; +} + +switch (1) { + default: + enum E {} + break; +} + +switch (1) { + case 1: + interface I {} + break; +} + +switch (1) { + default: + interface I {} + { + } + break; +} + +switch (1) { + case 1: + type N = number; + break; +} + +switch (1) { + default: + type N = number; + break; +} + +``` + +# Diagnostics +``` +invalid.ts:3:3 lint/nursery/noSwitchDeclarations FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Other switch clauses can erroneously access this declaration. + Wrap the declaration in a block to restrict its access to the switch clause. + + 1 │ switch (1) { + 2 │ case 1: + > 3 │ enum E {} + │ ^^^^^^^^^ + 4 │ break; + 5 │ } + + i The declaration is defined in this switch clause: + + 1 │ switch (1) { + > 2 │ case 1: + │ ^^^^^^^ + > 3 │ enum E {} + > 4 │ break; + │ ^^^^^^ + 5 │ } + 6 │ + + i Suggested fix: Wrap the declaration in a block. + + 1 1 │ switch (1) { + 2 │ - → case·1: + 2 │ + → case·1:·{ + 3 3 │ enum E {} + 4 4 │ break; + 5 │ + → } + 5 6 │ } + 6 7 │ + + +``` + +``` +invalid.ts:9:3 lint/nursery/noSwitchDeclarations FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Other switch clauses can erroneously access this declaration. + Wrap the declaration in a block to restrict its access to the switch clause. + + 7 │ switch (1) { + 8 │ default: + > 9 │ enum E {} + │ ^^^^^^^^^ + 10 │ break; + 11 │ } + + i The declaration is defined in this switch clause: + + 7 │ switch (1) { + > 8 │ default: + │ ^^^^^^^^ + > 9 │ enum E {} + > 10 │ break; + │ ^^^^^^ + 11 │ } + 12 │ + + i Suggested fix: Wrap the declaration in a block. + + 6 6 │ + 7 7 │ switch (1) { + 8 │ - → default: + 8 │ + → default:·{ + 9 9 │ enum E {} + 10 10 │ break; + 11 │ + → } + 11 12 │ } + 12 13 │ + + +``` + +``` +invalid.ts:15:3 lint/nursery/noSwitchDeclarations FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Other switch clauses can erroneously access this declaration. + Wrap the declaration in a block to restrict its access to the switch clause. + + 13 │ switch (1) { + 14 │ case 1: + > 15 │ interface I {} + │ ^^^^^^^^^^^^^^ + 16 │ break; + 17 │ } + + i The declaration is defined in this switch clause: + + 13 │ switch (1) { + > 14 │ case 1: + │ ^^^^^^^ + > 15 │ interface I {} + > 16 │ break; + │ ^^^^^^ + 17 │ } + 18 │ + + i Suggested fix: Wrap the declaration in a block. + + 12 12 │ + 13 13 │ switch (1) { + 14 │ - → case·1: + 14 │ + → case·1:·{ + 15 15 │ interface I {} + 16 16 │ break; + 17 │ + → } + 17 18 │ } + 18 19 │ + + +``` + +``` +invalid.ts:21:3 lint/nursery/noSwitchDeclarations FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Other switch clauses can erroneously access this declaration. + Wrap the declaration in a block to restrict its access to the switch clause. + + 19 │ switch (1) { + 20 │ default: + > 21 │ interface I {} + │ ^^^^^^^^^^^^^^ + 22 │ { + 23 │ } + + i The declaration is defined in this switch clause: + + 19 │ switch (1) { + > 20 │ default: + │ ^^^^^^^^ + > 21 │ interface I {} + > 22 │ { + > 23 │ } + > 24 │ break; + │ ^^^^^^ + 25 │ } + 26 │ + + i Suggested fix: Wrap the declaration in a block. + + 18 18 │ + 19 19 │ switch (1) { + 20 │ - → default: + 20 │ + → default:·{ + 21 21 │ interface I {} + 22 22 │ { + 23 23 │ } + 24 24 │ break; + 25 │ + → } + 25 26 │ } + 26 27 │ + + +``` + +``` +invalid.ts:29:3 lint/nursery/noSwitchDeclarations FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Other switch clauses can erroneously access this declaration. + Wrap the declaration in a block to restrict its access to the switch clause. + + 27 │ switch (1) { + 28 │ case 1: + > 29 │ type N = number; + │ ^^^^^^^^^^^^^^^^ + 30 │ break; + 31 │ } + + i The declaration is defined in this switch clause: + + 27 │ switch (1) { + > 28 │ case 1: + │ ^^^^^^^ + > 29 │ type N = number; + > 30 │ break; + │ ^^^^^^ + 31 │ } + 32 │ + + i Suggested fix: Wrap the declaration in a block. + + 26 26 │ + 27 27 │ switch (1) { + 28 │ - → case·1: + 28 │ + → case·1:·{ + 29 29 │ type N = number; + 30 30 │ break; + 31 │ + → } + 31 32 │ } + 32 33 │ + + +``` + +``` +invalid.ts:35:3 lint/nursery/noSwitchDeclarations FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Other switch clauses can erroneously access this declaration. + Wrap the declaration in a block to restrict its access to the switch clause. + + 33 │ switch (1) { + 34 │ default: + > 35 │ type N = number; + │ ^^^^^^^^^^^^^^^^ + 36 │ break; + 37 │ } + + i The declaration is defined in this switch clause: + + 33 │ switch (1) { + > 34 │ default: + │ ^^^^^^^^ + > 35 │ type N = number; + > 36 │ break; + │ ^^^^^^ + 37 │ } + 38 │ + + i Suggested fix: Wrap the declaration in a block. + + 32 32 │ + 33 33 │ switch (1) { + 34 │ - → default: + 34 │ + → default:·{ + 35 35 │ type N = number; + 36 36 │ break; + 37 │ + → } + 37 38 │ } + 38 39 │ + + +``` + + diff --git a/crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/valid.jsonc b/crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/valid.jsonc new file mode 100644 index 00000000000..9a73663a521 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/valid.jsonc @@ -0,0 +1,6 @@ +[ + "switch (a) { case 1: { let x = 1; break; } default: { let x = 2; break; } }", + "switch (a) { case 1: { const x = 1; break; } default: { const x = 2; break; } }", + "switch (a) { case 1: { function f() {} break; } default: { function f() {} break; } }", + "switch (a) { case 1: { class C {} break; } default: { class C {} break; } }" +] diff --git a/crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/valid.jsonc.snap b/crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/valid.jsonc.snap new file mode 100644 index 00000000000..f238fcf04eb --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noSwitchDeclarations/valid.jsonc.snap @@ -0,0 +1,26 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +assertion_line: 92 +expression: valid.jsonc +--- +# Input +```js +switch (a) { case 1: { let x = 1; break; } default: { let x = 2; break; } } +``` + +# Input +```js +switch (a) { case 1: { const x = 1; break; } default: { const x = 2; break; } } +``` + +# Input +```js +switch (a) { case 1: { function f() {} break; } default: { function f() {} break; } } +``` + +# Input +```js +switch (a) { case 1: { class C {} break; } default: { class C {} break; } } +``` + + diff --git a/crates/rome_js_syntax/src/stmt_ext.rs b/crates/rome_js_syntax/src/stmt_ext.rs index 95b3bf48820..e165b41e9f1 100644 --- a/crates/rome_js_syntax/src/stmt_ext.rs +++ b/crates/rome_js_syntax/src/stmt_ext.rs @@ -1,11 +1,35 @@ //! Extended AST node definitions for statements which are unique and special enough to generate code for manually use crate::{ - AnyJsArrayAssignmentPatternElement, AnyJsAssignmentPattern, JsForVariableDeclaration, - JsVariableDeclaration, T, + AnyJsArrayAssignmentPatternElement, AnyJsAssignmentPattern, AnyJsSwitchClause, + JsForVariableDeclaration, JsStatementList, JsSyntaxToken as SyntaxToken, JsVariableDeclaration, + T, }; use rome_rowan::SyntaxResult; +impl AnyJsSwitchClause { + pub fn clause_token(&self) -> SyntaxResult { + match &self { + AnyJsSwitchClause::JsCaseClause(item) => item.case_token(), + AnyJsSwitchClause::JsDefaultClause(item) => item.default_token(), + } + } + + pub fn colon_token(&self) -> SyntaxResult { + match &self { + AnyJsSwitchClause::JsCaseClause(item) => item.colon_token(), + AnyJsSwitchClause::JsDefaultClause(item) => item.colon_token(), + } + } + + pub fn consequent(&self) -> JsStatementList { + match &self { + AnyJsSwitchClause::JsCaseClause(item) => item.consequent(), + AnyJsSwitchClause::JsDefaultClause(item) => item.consequent(), + } + } +} + #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum JsVariableKind { Const, diff --git a/crates/rome_rowan/src/syntax/token.rs b/crates/rome_rowan/src/syntax/token.rs index 45df9f66d7b..3d2f133bf08 100644 --- a/crates/rome_rowan/src/syntax/token.rs +++ b/crates/rome_rowan/src/syntax/token.rs @@ -188,6 +188,22 @@ impl SyntaxToken { } } + /// Return the leading whitespace and newline until the first leading newline or token. + pub fn indentation_trivia(&self) -> Vec> { + let leading_trivia = self.leading_trivia().pieces(); + let skip_count = leading_trivia.len() + - leading_trivia + .rev() + .position(|x| x.is_newline()) + .map(|pos| pos + 1) + .unwrap_or(0); + self.leading_trivia() + .pieces() + .skip(skip_count) + .filter(|x| x.is_newline() || x.is_whitespace()) + .collect::>() + } + /// Return a new version of this token with its leading trivia replaced with `trivia` #[must_use = "syntax elements are immutable, the result of update methods must be propagated to have any effect"] pub fn with_leading_trivia<'a, I>(&self, trivia: I) -> Self diff --git a/crates/rome_service/src/configuration/linter/rules.rs b/crates/rome_service/src/configuration/linter/rules.rs index 4e8ea21fe6c..a42dc5954fc 100644 --- a/crates/rome_service/src/configuration/linter/rules.rs +++ b/crates/rome_service/src/configuration/linter/rules.rs @@ -1029,6 +1029,9 @@ pub struct Nursery { #[doc = "Disallow comparison of expressions modifying the string case with non-compliant value."] #[serde(skip_serializing_if = "Option::is_none")] pub no_string_case_mismatch: Option, + #[doc = "Disallow lexical declarations in switch clauses."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_switch_declarations: Option, #[doc = "Ensures the super() constructor is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass"] #[serde(skip_serializing_if = "Option::is_none")] pub no_unreachable_super: Option, @@ -1107,7 +1110,7 @@ pub struct Nursery { } impl Nursery { const GROUP_NAME: &'static str = "nursery"; - pub(crate) const GROUP_RULES: [&'static str; 57] = [ + pub(crate) const GROUP_RULES: [&'static str; 58] = [ "noAccessKey", "noAssignInExpressions", "noBannedTypes", @@ -1140,6 +1143,7 @@ impl Nursery { "noSelfCompare", "noSetterReturn", "noStringCaseMismatch", + "noSwitchDeclarations", "noUnreachableSuper", "noUnsafeFinally", "noUnusedLabels", @@ -1166,7 +1170,7 @@ impl Nursery { "useValidLang", "useYield", ]; - const RECOMMENDED_RULES: [&'static str; 47] = [ + const RECOMMENDED_RULES: [&'static str; 48] = [ "noAssignInExpressions", "noBannedTypes", "noClassAssign", @@ -1193,6 +1197,7 @@ impl Nursery { "noSelfCompare", "noSetterReturn", "noStringCaseMismatch", + "noSwitchDeclarations", "noUnreachableSuper", "noUnsafeFinally", "noUnusedLabels", @@ -1215,7 +1220,7 @@ impl Nursery { "useValidLang", "useYield", ]; - const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 47] = [ + const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 48] = [ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3]), @@ -1250,19 +1255,20 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[46]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[52]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[53]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[54]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[55]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[56]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[57]), ]; pub(crate) fn is_recommended(&self) -> bool { !matches!(self.recommended, Some(false)) } pub(crate) fn get_enabled_rules(&self) -> IndexSet { @@ -1427,131 +1433,136 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.no_unreachable_super.as_ref() { + if let Some(rule) = self.no_switch_declarations.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } - if let Some(rule) = self.no_unsafe_finally.as_ref() { + if let Some(rule) = self.no_unreachable_super.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } - if let Some(rule) = self.no_unused_labels.as_ref() { + if let Some(rule) = self.no_unsafe_finally.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } } - if let Some(rule) = self.no_useless_rename.as_ref() { + if let Some(rule) = self.no_unused_labels.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); } } - if let Some(rule) = self.no_useless_switch_case.as_ref() { + if let Some(rule) = self.no_useless_rename.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36])); } } - if let Some(rule) = self.no_var.as_ref() { + if let Some(rule) = self.no_useless_switch_case.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37])); } } - if let Some(rule) = self.no_void_type_return.as_ref() { + if let Some(rule) = self.no_var.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38])); } } - if let Some(rule) = self.no_with.as_ref() { + if let Some(rule) = self.no_void_type_return.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); } } - if let Some(rule) = self.use_aria_prop_types.as_ref() { + if let Some(rule) = self.no_with.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40])); } } - if let Some(rule) = self.use_aria_props_for_role.as_ref() { + if let Some(rule) = self.use_aria_prop_types.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41])); } } - if let Some(rule) = self.use_camel_case.as_ref() { + if let Some(rule) = self.use_aria_props_for_role.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42])); } } - if let Some(rule) = self.use_const.as_ref() { + if let Some(rule) = self.use_camel_case.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43])); } } - if let Some(rule) = self.use_default_parameter_last.as_ref() { + if let Some(rule) = self.use_const.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44])); } } - if let Some(rule) = self.use_default_switch_clause_last.as_ref() { + if let Some(rule) = self.use_default_parameter_last.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45])); } } - if let Some(rule) = self.use_enum_initializers.as_ref() { + if let Some(rule) = self.use_default_switch_clause_last.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[46])); } } - if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { + if let Some(rule) = self.use_enum_initializers.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47])); } } - if let Some(rule) = self.use_exponentiation_operator.as_ref() { + if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48])); } } - if let Some(rule) = self.use_hook_at_top_level.as_ref() { + if let Some(rule) = self.use_exponentiation_operator.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[49])); } } - if let Some(rule) = self.use_iframe_title.as_ref() { + if let Some(rule) = self.use_hook_at_top_level.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50])); } } - if let Some(rule) = self.use_is_nan.as_ref() { + if let Some(rule) = self.use_iframe_title.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51])); } } - if let Some(rule) = self.use_media_caption.as_ref() { + if let Some(rule) = self.use_is_nan.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[52])); } } - if let Some(rule) = self.use_numeric_literals.as_ref() { + if let Some(rule) = self.use_media_caption.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[53])); } } - if let Some(rule) = self.use_valid_aria_props.as_ref() { + if let Some(rule) = self.use_numeric_literals.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[54])); } } - if let Some(rule) = self.use_valid_lang.as_ref() { + if let Some(rule) = self.use_valid_aria_props.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[55])); } } - if let Some(rule) = self.use_yield.as_ref() { + if let Some(rule) = self.use_valid_lang.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[56])); } } + if let Some(rule) = self.use_yield.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[57])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> IndexSet { @@ -1716,131 +1727,136 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.no_unreachable_super.as_ref() { + if let Some(rule) = self.no_switch_declarations.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } - if let Some(rule) = self.no_unsafe_finally.as_ref() { + if let Some(rule) = self.no_unreachable_super.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } - if let Some(rule) = self.no_unused_labels.as_ref() { + if let Some(rule) = self.no_unsafe_finally.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } } - if let Some(rule) = self.no_useless_rename.as_ref() { + if let Some(rule) = self.no_unused_labels.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); } } - if let Some(rule) = self.no_useless_switch_case.as_ref() { + if let Some(rule) = self.no_useless_rename.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36])); } } - if let Some(rule) = self.no_var.as_ref() { + if let Some(rule) = self.no_useless_switch_case.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37])); } } - if let Some(rule) = self.no_void_type_return.as_ref() { + if let Some(rule) = self.no_var.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38])); } } - if let Some(rule) = self.no_with.as_ref() { + if let Some(rule) = self.no_void_type_return.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); } } - if let Some(rule) = self.use_aria_prop_types.as_ref() { + if let Some(rule) = self.no_with.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40])); } } - if let Some(rule) = self.use_aria_props_for_role.as_ref() { + if let Some(rule) = self.use_aria_prop_types.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41])); } } - if let Some(rule) = self.use_camel_case.as_ref() { + if let Some(rule) = self.use_aria_props_for_role.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42])); } } - if let Some(rule) = self.use_const.as_ref() { + if let Some(rule) = self.use_camel_case.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43])); } } - if let Some(rule) = self.use_default_parameter_last.as_ref() { + if let Some(rule) = self.use_const.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44])); } } - if let Some(rule) = self.use_default_switch_clause_last.as_ref() { + if let Some(rule) = self.use_default_parameter_last.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45])); } } - if let Some(rule) = self.use_enum_initializers.as_ref() { + if let Some(rule) = self.use_default_switch_clause_last.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[46])); } } - if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { + if let Some(rule) = self.use_enum_initializers.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47])); } } - if let Some(rule) = self.use_exponentiation_operator.as_ref() { + if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48])); } } - if let Some(rule) = self.use_hook_at_top_level.as_ref() { + if let Some(rule) = self.use_exponentiation_operator.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[49])); } } - if let Some(rule) = self.use_iframe_title.as_ref() { + if let Some(rule) = self.use_hook_at_top_level.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50])); } } - if let Some(rule) = self.use_is_nan.as_ref() { + if let Some(rule) = self.use_iframe_title.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51])); } } - if let Some(rule) = self.use_media_caption.as_ref() { + if let Some(rule) = self.use_is_nan.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[52])); } } - if let Some(rule) = self.use_numeric_literals.as_ref() { + if let Some(rule) = self.use_media_caption.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[53])); } } - if let Some(rule) = self.use_valid_aria_props.as_ref() { + if let Some(rule) = self.use_numeric_literals.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[54])); } } - if let Some(rule) = self.use_valid_lang.as_ref() { + if let Some(rule) = self.use_valid_aria_props.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[55])); } } - if let Some(rule) = self.use_yield.as_ref() { + if let Some(rule) = self.use_valid_lang.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[56])); } } + if let Some(rule) = self.use_yield.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[57])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -1849,7 +1865,7 @@ impl Nursery { pub(crate) fn is_recommended_rule(rule_name: &str) -> bool { Self::RECOMMENDED_RULES.contains(&rule_name) } - pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 47] { + pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 48] { Self::RECOMMENDED_RULES_AS_FILTERS } pub(crate) fn get_rule_configuration(&self, rule_name: &str) -> Option<&RuleConfiguration> { @@ -1888,6 +1904,7 @@ impl Nursery { "noSelfCompare" => self.no_self_compare.as_ref(), "noSetterReturn" => self.no_setter_return.as_ref(), "noStringCaseMismatch" => self.no_string_case_mismatch.as_ref(), + "noSwitchDeclarations" => self.no_switch_declarations.as_ref(), "noUnreachableSuper" => self.no_unreachable_super.as_ref(), "noUnsafeFinally" => self.no_unsafe_finally.as_ref(), "noUnusedLabels" => self.no_unused_labels.as_ref(), diff --git a/crates/rome_service/src/configuration/parse/json/rules.rs b/crates/rome_service/src/configuration/parse/json/rules.rs index abe83471fa0..51ddc846d76 100644 --- a/crates/rome_service/src/configuration/parse/json/rules.rs +++ b/crates/rome_service/src/configuration/parse/json/rules.rs @@ -735,6 +735,7 @@ impl VisitNode for Nursery { "noSelfCompare", "noSetterReturn", "noStringCaseMismatch", + "noSwitchDeclarations", "noUnreachableSuper", "noUnsafeFinally", "noUnusedLabels", @@ -1352,6 +1353,24 @@ impl VisitNode for Nursery { )); } }, + "noSwitchDeclarations" => match value { + AnyJsonValue::JsonStringValue(_) => { + let mut configuration = RuleConfiguration::default(); + self.map_to_known_string(&value, name_text, &mut configuration, diagnostics)?; + self.no_switch_declarations = Some(configuration); + } + AnyJsonValue::JsonObjectValue(_) => { + let mut configuration = RuleConfiguration::default(); + self.map_to_object(&value, name_text, &mut configuration, diagnostics)?; + self.no_switch_declarations = Some(configuration); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_incorrect_type( + "object or string", + value.range(), + )); + } + }, "noUnreachableSuper" => match value { AnyJsonValue::JsonStringValue(_) => { let mut configuration = RuleConfiguration::default(); diff --git a/editors/vscode/configuration_schema.json b/editors/vscode/configuration_schema.json index 610c8b07c16..0162bf2df58 100644 --- a/editors/vscode/configuration_schema.json +++ b/editors/vscode/configuration_schema.json @@ -613,6 +613,13 @@ { "type": "null" } ] }, + "noSwitchDeclarations": { + "description": "Disallow lexical declarations in switch clauses.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noUnreachableSuper": { "description": "Ensures the super() constructor is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass", "anyOf": [ diff --git a/npm/backend-jsonrpc/src/workspace.ts b/npm/backend-jsonrpc/src/workspace.ts index 8d793af73a7..af355a1ff42 100644 --- a/npm/backend-jsonrpc/src/workspace.ts +++ b/npm/backend-jsonrpc/src/workspace.ts @@ -412,6 +412,10 @@ export interface Nursery { * Disallow comparison of expressions modifying the string case with non-compliant value. */ noStringCaseMismatch?: RuleConfiguration; + /** + * Disallow lexical declarations in switch clauses. + */ + noSwitchDeclarations?: RuleConfiguration; /** * Ensures the super() constructor is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass */ @@ -814,6 +818,7 @@ export type Category = | "lint/nursery/noSelfCompare" | "lint/nursery/noSetterReturn" | "lint/nursery/noStringCaseMismatch" + | "lint/nursery/noSwitchDeclarations" | "lint/nursery/noUnreachableSuper" | "lint/nursery/noUnsafeFinally" | "lint/nursery/noUnusedLabels" diff --git a/npm/rome/configuration_schema.json b/npm/rome/configuration_schema.json index 610c8b07c16..0162bf2df58 100644 --- a/npm/rome/configuration_schema.json +++ b/npm/rome/configuration_schema.json @@ -613,6 +613,13 @@ { "type": "null" } ] }, + "noSwitchDeclarations": { + "description": "Disallow lexical declarations in switch clauses.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noUnreachableSuper": { "description": "Ensures the super() constructor is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass", "anyOf": [ diff --git a/website/src/pages/lint/rules/index.mdx b/website/src/pages/lint/rules/index.mdx index 0c2632e94ca..75adeaef800 100644 --- a/website/src/pages/lint/rules/index.mdx +++ b/website/src/pages/lint/rules/index.mdx @@ -678,6 +678,12 @@ Disallow returning a value from a setter Disallow comparison of expressions modifying the string case with non-compliant value.
+

+ noSwitchDeclarations +

+Disallow lexical declarations in switch clauses. +
+

noUnreachableSuper

diff --git a/website/src/pages/lint/rules/noSwitchDeclarations.md b/website/src/pages/lint/rules/noSwitchDeclarations.md new file mode 100644 index 00000000000..6786c9720cd --- /dev/null +++ b/website/src/pages/lint/rules/noSwitchDeclarations.md @@ -0,0 +1,185 @@ +--- +title: Lint Rule noSwitchDeclarations +parent: lint/rules/index +--- + +# noSwitchDeclarations (since v12.0.0) + +Disallow lexical declarations in `switch` clauses. + +Lexical declarations in `switch` clauses are accessible in the entire `switch`. +However, it only gets initialized when it is assigned, which will only happen if the `switch` clause where it is defined is reached. + +To ensure that the lexical declarations only apply to the current `switch` clause wrap your declarations in a block. + +Source: https://eslint.org/docs/latest/rules/no-case-declarations + +## Examples + +### Invalid + +```jsx +switch (foo) { + case 0: + const x = 1; + break; + case 2: + x; // `x` can be used while it is not initialized + break; +} +``` + +
nursery/noSwitchDeclarations.js:3:9 lint/nursery/noSwitchDeclarations  FIXABLE  ━━━━━━━━━━━━━━━━━━━━
+
+   Other switch clauses can erroneously access this declaration.
+    Wrap the declaration in a block to restrict its access to the switch clause.
+  
+    1 │ switch (foo) {
+    2 │     case 0:
+  > 3 │         const x = 1;
+           ^^^^^^^^^^^
+    4 │         break;
+    5 │     case 2:
+  
+   The declaration is defined in this switch clause:
+  
+    1 │ switch (foo) {
+  > 2 │     case 0:
+       ^^^^^^^
+  > 3 │         const x = 1;
+  > 4 │         break;
+           ^^^^^^
+    5 │     case 2:
+    6 │         x; // `x` can be used while it is not initialized
+  
+   Suggested fix: Wrap the declaration in a block.
+  
+    1  1  switch (foo) {
+    2   - ····case·0:
+       2+ ····case·0:·{
+    3  3          const x = 1;
+    4  4          break;
+    5   - ····case·2:
+       5+ ····}
+       6+ ····case·2:
+    6  7          x; // `x` can be used while it is not initialized
+    7  8          break;
+  
+
+ +```jsx +switch (foo) { + case 0: + function f() {} + break; + case 2: + f(); // `f` can be called here + break; +} +``` + +
nursery/noSwitchDeclarations.js:3:9 lint/nursery/noSwitchDeclarations  FIXABLE  ━━━━━━━━━━━━━━━━━━━━
+
+   Other switch clauses can erroneously access this declaration.
+    Wrap the declaration in a block to restrict its access to the switch clause.
+  
+    1 │ switch (foo) {
+    2 │     case 0:
+  > 3 │         function f() {}
+           ^^^^^^^^^^^^^^^
+    4 │         break;
+    5 │     case 2:
+  
+   The declaration is defined in this switch clause:
+  
+    1 │ switch (foo) {
+  > 2 │     case 0:
+       ^^^^^^^
+  > 3 │         function f() {}
+  > 4 │         break;
+           ^^^^^^
+    5 │     case 2:
+    6 │         f(); // `f` can be called here
+  
+   Suggested fix: Wrap the declaration in a block.
+  
+    1  1  switch (foo) {
+    2   - ····case·0:
+       2+ ····case·0:·{
+    3  3          function f() {}
+    4  4          break;
+    5   - ····case·2:
+       5+ ····}
+       6+ ····case·2:
+    6  7          f(); // `f` can be called here
+    7  8          break;
+  
+
+ +```jsx +switch (foo) { + case 0: + class A {} + break; + default: + new A(); // `A` can be instantiated here + break; +} +``` + +
nursery/noSwitchDeclarations.js:3:9 lint/nursery/noSwitchDeclarations  FIXABLE  ━━━━━━━━━━━━━━━━━━━━
+
+   Other switch clauses can erroneously access this declaration.
+    Wrap the declaration in a block to restrict its access to the switch clause.
+  
+    1 │ switch (foo) {
+    2 │     case 0:
+  > 3 │         class A {}
+           ^^^^^^^^^^
+    4 │         break;
+    5 │     default:
+  
+   The declaration is defined in this switch clause:
+  
+    1 │ switch (foo) {
+  > 2 │     case 0:
+       ^^^^^^^
+  > 3 │         class A {}
+  > 4 │         break;
+           ^^^^^^
+    5 │     default:
+    6 │         new A(); // `A` can be instantiated here
+  
+   Suggested fix: Wrap the declaration in a block.
+  
+    1  1  switch (foo) {
+    2   - ····case·0:
+       2+ ····case·0:·{
+    3  3          class A {}
+    4  4          break;
+    5   - ····default:
+       5+ ····}
+       6+ ····default:
+    6  7          new A(); // `A` can be instantiated here
+    7  8          break;
+  
+
+ +### Valid + +```jsx +switch (foo) { + case 0: { + const x = 1; + break; + } + case 1: + // `x` is not visible here + break; +} +``` + +## Related links + +- [Disable a rule](/linter/#disable-a-lint-rule) +- [Rule options](/linter/#rule-options)