diff --git a/CHANGELOG.md b/CHANGELOG.md index f4b628cfc36..ebd1b9e0702 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ #### New rules - [`noUselessConstructor`](https://docs.rome.tools/lint/rules/noUselessConstructor/) +- [`useLiteralEnumMembers`](https://docs.rome.tools/lint/rules/useLiteralEnumMembers/) #### Other changes - Add new command `rome migrate` the transform the configuration file `rome.json` diff --git a/crates/rome_diagnostics_categories/src/categories.rs b/crates/rome_diagnostics_categories/src/categories.rs index 44074723eef..74caaa684d1 100644 --- a/crates/rome_diagnostics_categories/src/categories.rs +++ b/crates/rome_diagnostics_categories/src/categories.rs @@ -106,6 +106,7 @@ define_categories! { "lint/nursery/noForEach": "https://docs.rome.tools/lint/rules/noForEach", "lint/nursery/useLiteralKeys": "https://docs.rome.tools/lint/rules/useLiteralKeys", "lint/nursery/noUselessConstructor": "https://docs.rome.tools/lint/rules/noUselessConstructor", +"lint/nursery/useLiteralEnumMembers": "https://docs.rome.tools/lint/rules/useLiteralEnumMembers", // Insert new nursery rule here "lint/nursery/noRedeclare": "https://docs.rome.tools/lint/rules/noRedeclare", "lint/nursery/useNamespaceKeyword": "https://docs.rome.tools/lint/rules/useNamespaceKeyword", diff --git a/crates/rome_js_analyze/src/analyzers/nursery.rs b/crates/rome_js_analyze/src/analyzers/nursery.rs index 679b3553ea2..3fd53b12263 100644 --- a/crates/rome_js_analyze/src/analyzers/nursery.rs +++ b/crates/rome_js_analyze/src/analyzers/nursery.rs @@ -33,8 +33,9 @@ mod no_useless_rename; mod no_useless_switch_case; mod no_with; mod use_is_nan; +mod use_literal_enum_members; mod use_literal_keys; mod use_media_caption; mod use_namespace_keyword; mod use_yield; -declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_assign_in_expressions :: NoAssignInExpressions , self :: no_banned_types :: NoBannedTypes , self :: no_comma_operator :: NoCommaOperator , self :: no_confusing_arrow :: NoConfusingArrow , self :: no_confusing_labels :: NoConfusingLabels , self :: no_duplicate_case :: NoDuplicateCase , self :: no_duplicate_class_members :: NoDuplicateClassMembers , self :: no_duplicate_jsx_props :: NoDuplicateJsxProps , self :: no_extra_labels :: NoExtraLabels , self :: no_extra_semicolons :: NoExtraSemicolons , self :: no_for_each :: NoForEach , self :: no_global_object_calls :: NoGlobalObjectCalls , self :: no_inferrable_types :: NoInferrableTypes , self :: no_inner_declarations :: NoInnerDeclarations , self :: no_invalid_constructor_super :: NoInvalidConstructorSuper , self :: no_namespace :: NoNamespace , self :: no_parameter_properties :: NoParameterProperties , self :: no_prototype_builtins :: NoPrototypeBuiltins , self :: no_redundant_alt :: NoRedundantAlt , self :: no_self_assign :: NoSelfAssign , self :: no_self_compare :: NoSelfCompare , self :: no_svg_without_title :: NoSvgWithoutTitle , self :: no_switch_declarations :: NoSwitchDeclarations , self :: no_unreachable_super :: NoUnreachableSuper , self :: no_unsafe_optional_chaining :: NoUnsafeOptionalChaining , self :: no_unused_labels :: NoUnusedLabels , self :: no_useless_catch :: NoUselessCatch , self :: no_useless_constructor :: NoUselessConstructor , self :: no_useless_rename :: NoUselessRename , self :: no_useless_switch_case :: NoUselessSwitchCase , self :: no_with :: NoWith , self :: use_is_nan :: UseIsNan , self :: use_literal_keys :: UseLiteralKeys , self :: use_media_caption :: UseMediaCaption , self :: use_namespace_keyword :: UseNamespaceKeyword , self :: use_yield :: UseYield ,] } } +declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_assign_in_expressions :: NoAssignInExpressions , self :: no_banned_types :: NoBannedTypes , self :: no_comma_operator :: NoCommaOperator , self :: no_confusing_arrow :: NoConfusingArrow , self :: no_confusing_labels :: NoConfusingLabels , self :: no_duplicate_case :: NoDuplicateCase , self :: no_duplicate_class_members :: NoDuplicateClassMembers , self :: no_duplicate_jsx_props :: NoDuplicateJsxProps , self :: no_extra_labels :: NoExtraLabels , self :: no_extra_semicolons :: NoExtraSemicolons , self :: no_for_each :: NoForEach , self :: no_global_object_calls :: NoGlobalObjectCalls , self :: no_inferrable_types :: NoInferrableTypes , self :: no_inner_declarations :: NoInnerDeclarations , self :: no_invalid_constructor_super :: NoInvalidConstructorSuper , self :: no_namespace :: NoNamespace , self :: no_parameter_properties :: NoParameterProperties , self :: no_prototype_builtins :: NoPrototypeBuiltins , self :: no_redundant_alt :: NoRedundantAlt , self :: no_self_assign :: NoSelfAssign , self :: no_self_compare :: NoSelfCompare , self :: no_svg_without_title :: NoSvgWithoutTitle , self :: no_switch_declarations :: NoSwitchDeclarations , self :: no_unreachable_super :: NoUnreachableSuper , self :: no_unsafe_optional_chaining :: NoUnsafeOptionalChaining , self :: no_unused_labels :: NoUnusedLabels , self :: no_useless_catch :: NoUselessCatch , self :: no_useless_constructor :: NoUselessConstructor , self :: no_useless_rename :: NoUselessRename , self :: no_useless_switch_case :: NoUselessSwitchCase , self :: no_with :: NoWith , self :: use_is_nan :: UseIsNan , self :: use_literal_enum_members :: UseLiteralEnumMembers , self :: use_literal_keys :: UseLiteralKeys , self :: use_media_caption :: UseMediaCaption , self :: use_namespace_keyword :: UseNamespaceKeyword , self :: use_yield :: UseYield ,] } } diff --git a/crates/rome_js_analyze/src/analyzers/nursery/use_literal_enum_members.rs b/crates/rome_js_analyze/src/analyzers/nursery/use_literal_enum_members.rs new file mode 100644 index 00000000000..e411e58dcba --- /dev/null +++ b/crates/rome_js_analyze/src/analyzers/nursery/use_literal_enum_members.rs @@ -0,0 +1,139 @@ +use rome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic}; +use rome_console::markup; +use rome_js_syntax::{ + AnyJsExpression, JsBinaryExpression, JsSyntaxKind, JsUnaryExpression, JsUnaryOperator, + TsEnumMember, +}; +use rome_rowan::AstNode; + +declare_rule! { + /// Require all enum members to be literal values. + /// + /// Usually, an enum member is initialized with a literal number or a literal string. + /// However, _TypeScript_ allows the value of an enum member to be many different kinds of expressions. + /// Using a computed enum member is often error-prone and confusing. + /// This rule requires the initialization of enum members with literal values. + /// It allows bitwise expressions for supporting [enum flags](https://stackoverflow.com/questions/39359740/what-are-enum-flags-in-typescript/39359953#39359953). + /// + /// In contrast to the equivalent _ESLint_ rule, this rule allows arbitrary bitwise constant expressions. + /// + /// Source: https://typescript-eslint.io/rules/prefer-literal-enum-member/ + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```ts,expect_diagnostic + /// const x = 2; + /// enum Computed { + /// A, + /// B = x, + /// } + /// ``` + /// + /// ```ts,expect_diagnostic + /// const x = 2; + /// enum Invalid { + /// A, + /// B = 2**3, + /// } + /// ``` + /// + /// ## Valid + /// + /// ```ts + /// enum Direction { + /// Left, + /// Right, + /// } + /// ``` + /// + /// ```ts + /// enum Order { + /// Less = -1, + /// Equal = 0, + /// Greater = 1, + /// } + /// ``` + /// + /// ```ts + /// enum State { + /// Open = "Open", + /// Close = "Close", + /// } + /// ``` + /// + /// ```ts + /// enum FileAccess { + /// None = 0, + /// Read = 1, + /// Write = 1 << 1, + /// All = 1 | (1 << 1) + /// } + /// ``` + pub(crate) UseLiteralEnumMembers { + version: "next", + name: "useLiteralEnumMembers", + recommended: true, + } +} + +impl Rule for UseLiteralEnumMembers { + type Query = Ast; + type State = (); + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let enum_member = ctx.query(); + let Some(initializer) = enum_member.initializer() else { + // no initializer => sequentially assigned literal integer + return None; + }; + let expr = initializer.expression().ok()?.omit_parentheses(); + if expr.as_any_js_literal_expression().is_some() || is_bitwise_constant_expression(&expr) { + return None; + } else if let Some(expr) = expr.as_js_unary_expression() { + if expr.is_signed_numeric_literal().ok()? { + return None; + } + } else if let Some(expr) = expr.as_js_template_expression() { + if expr.is_constant() { + return None; + } + } + Some(()) + } + + fn diagnostic(ctx: &RuleContext, _: &Self::State) -> Option { + let enum_member = ctx.query(); + Some(RuleDiagnostic::new( + rule_category!(), + enum_member.initializer()?.expression().ok()?.range(), + markup! { + "The enum member should be initialized with a literal value such as a number or a string." + }, + )) + } +} + +/// Returns true if `expr` is an expression that only includes literal numbers and bitwise operations. +fn is_bitwise_constant_expression(expr: &AnyJsExpression) -> bool { + for node in expr.syntax().descendants() { + if let Some(exp) = JsUnaryExpression::cast_ref(&node) { + if exp.operator() != Ok(JsUnaryOperator::BitwiseNot) { + return false; + } + } else if let Some(exp) = JsBinaryExpression::cast_ref(&node) { + if !exp.is_binary_operator() { + return false; + } + } else if !matches!( + node.kind(), + JsSyntaxKind::JS_NUMBER_LITERAL_EXPRESSION | JsSyntaxKind::JS_PARENTHESIZED_EXPRESSION + ) { + return false; + } + } + true +} diff --git a/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/invalid.ts b/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/invalid.ts new file mode 100644 index 00000000000..7d250761ed7 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/invalid.ts @@ -0,0 +1,82 @@ +enum InvalidObject { + A = {}, +} + + +enum InvalidArray { + A = [], +} + + +enum InvalidTemplateLiteral { + A = `foo ${0}`, +} + + +enum InvalidConstructor { + A = new Set(), +} + + +enum InvalidExpression { + A = 2 + 2, +} + +enum InvalidExpression { + A = delete 2, + B = -a, + C = void 2, + D = ~2, + E = !0, +} + + +const variable = 'Test'; +enum InvalidVariable { + A = 'TestStr', + B = 2, + C, + V = variable, +} + + +enum InvalidEnumMember { + A = 'TestStr', + B = A, +} + + +const Valid = { A: 2 }; +enum InvalidObjectMember { + A = 'TestStr', + B = Valid.A, +} + + +enum Valid { + A, +} +enum InvalidEnumMember { + A = 'TestStr', + B = Valid.A, +} + + +const obj = { a: 1 }; +enum InvalidSpread { + A = 'TestStr', + B = { ...a }, +} + + +const x = 1; +enum Foo { + A = x << 0, + B = x >> 0, + C = x >>> 0, + D = x | 0, + E = x & 0, + F = x ^ 0, + G = ~x, +} + diff --git a/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/invalid.ts.snap b/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/invalid.ts.snap new file mode 100644 index 00000000000..487acaaef38 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/invalid.ts.snap @@ -0,0 +1,403 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +assertion_line: 96 +expression: invalid.ts +--- +# Input +```js +enum InvalidObject { + A = {}, +} + + +enum InvalidArray { + A = [], +} + + +enum InvalidTemplateLiteral { + A = `foo ${0}`, +} + + +enum InvalidConstructor { + A = new Set(), +} + + +enum InvalidExpression { + A = 2 + 2, +} + +enum InvalidExpression { + A = delete 2, + B = -a, + C = void 2, + D = ~2, + E = !0, +} + + +const variable = 'Test'; +enum InvalidVariable { + A = 'TestStr', + B = 2, + C, + V = variable, +} + + +enum InvalidEnumMember { + A = 'TestStr', + B = A, +} + + +const Valid = { A: 2 }; +enum InvalidObjectMember { + A = 'TestStr', + B = Valid.A, +} + + +enum Valid { + A, +} +enum InvalidEnumMember { + A = 'TestStr', + B = Valid.A, +} + + +const obj = { a: 1 }; +enum InvalidSpread { + A = 'TestStr', + B = { ...a }, +} + + +const x = 1; +enum Foo { + A = x << 0, + B = x >> 0, + C = x >>> 0, + D = x | 0, + E = x & 0, + F = x ^ 0, + G = ~x, +} + + +``` + +# Diagnostics +``` +invalid.ts:2:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The enum member should be initialized with a literal value such as a number or a string. + + 1 │ enum InvalidObject { + > 2 │ A = {}, + │ ^^ + 3 │ } + 4 │ + + +``` + +``` +invalid.ts:7:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The enum member should be initialized with a literal value such as a number or a string. + + 6 │ enum InvalidArray { + > 7 │ A = [], + │ ^^ + 8 │ } + 9 │ + + +``` + +``` +invalid.ts:12:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The enum member should be initialized with a literal value such as a number or a string. + + 11 │ enum InvalidTemplateLiteral { + > 12 │ A = `foo ${0}`, + │ ^^^^^^^^^^ + 13 │ } + 14 │ + + +``` + +``` +invalid.ts:17:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The enum member should be initialized with a literal value such as a number or a string. + + 16 │ enum InvalidConstructor { + > 17 │ A = new Set(), + │ ^^^^^^^^^ + 18 │ } + 19 │ + + +``` + +``` +invalid.ts:22:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The enum member should be initialized with a literal value such as a number or a string. + + 21 │ enum InvalidExpression { + > 22 │ A = 2 + 2, + │ ^^^^^ + 23 │ } + 24 │ + + +``` + +``` +invalid.ts:26:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The enum member should be initialized with a literal value such as a number or a string. + + 25 │ enum InvalidExpression { + > 26 │ A = delete 2, + │ ^^^^^^^^ + 27 │ B = -a, + 28 │ C = void 2, + + +``` + +``` +invalid.ts:27:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The enum member should be initialized with a literal value such as a number or a string. + + 25 │ enum InvalidExpression { + 26 │ A = delete 2, + > 27 │ B = -a, + │ ^^ + 28 │ C = void 2, + 29 │ D = ~2, + + +``` + +``` +invalid.ts:28:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The enum member should be initialized with a literal value such as a number or a string. + + 26 │ A = delete 2, + 27 │ B = -a, + > 28 │ C = void 2, + │ ^^^^^^ + 29 │ D = ~2, + 30 │ E = !0, + + +``` + +``` +invalid.ts:30:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The enum member should be initialized with a literal value such as a number or a string. + + 28 │ C = void 2, + 29 │ D = ~2, + > 30 │ E = !0, + │ ^^ + 31 │ } + 32 │ + + +``` + +``` +invalid.ts:39:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The enum member should be initialized with a literal value such as a number or a string. + + 37 │ B = 2, + 38 │ C, + > 39 │ V = variable, + │ ^^^^^^^^ + 40 │ } + 41 │ + + +``` + +``` +invalid.ts:45:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The enum member should be initialized with a literal value such as a number or a string. + + 43 │ enum InvalidEnumMember { + 44 │ A = 'TestStr', + > 45 │ B = A, + │ ^ + 46 │ } + 47 │ + + +``` + +``` +invalid.ts:52:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The enum member should be initialized with a literal value such as a number or a string. + + 50 │ enum InvalidObjectMember { + 51 │ A = 'TestStr', + > 52 │ B = Valid.A, + │ ^^^^^^^ + 53 │ } + 54 │ + + +``` + +``` +invalid.ts:61:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The enum member should be initialized with a literal value such as a number or a string. + + 59 │ enum InvalidEnumMember { + 60 │ A = 'TestStr', + > 61 │ B = Valid.A, + │ ^^^^^^^ + 62 │ } + 63 │ + + +``` + +``` +invalid.ts:68:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The enum member should be initialized with a literal value such as a number or a string. + + 66 │ enum InvalidSpread { + 67 │ A = 'TestStr', + > 68 │ B = { ...a }, + │ ^^^^^^^^ + 69 │ } + 70 │ + + +``` + +``` +invalid.ts:74:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The enum member should be initialized with a literal value such as a number or a string. + + 72 │ const x = 1; + 73 │ enum Foo { + > 74 │ A = x << 0, + │ ^^^^^^ + 75 │ B = x >> 0, + 76 │ C = x >>> 0, + + +``` + +``` +invalid.ts:75:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The enum member should be initialized with a literal value such as a number or a string. + + 73 │ enum Foo { + 74 │ A = x << 0, + > 75 │ B = x >> 0, + │ ^^^^^^ + 76 │ C = x >>> 0, + 77 │ D = x | 0, + + +``` + +``` +invalid.ts:76:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The enum member should be initialized with a literal value such as a number or a string. + + 74 │ A = x << 0, + 75 │ B = x >> 0, + > 76 │ C = x >>> 0, + │ ^^^^^^^ + 77 │ D = x | 0, + 78 │ E = x & 0, + + +``` + +``` +invalid.ts:77:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The enum member should be initialized with a literal value such as a number or a string. + + 75 │ B = x >> 0, + 76 │ C = x >>> 0, + > 77 │ D = x | 0, + │ ^^^^^ + 78 │ E = x & 0, + 79 │ F = x ^ 0, + + +``` + +``` +invalid.ts:78:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The enum member should be initialized with a literal value such as a number or a string. + + 76 │ C = x >>> 0, + 77 │ D = x | 0, + > 78 │ E = x & 0, + │ ^^^^^ + 79 │ F = x ^ 0, + 80 │ G = ~x, + + +``` + +``` +invalid.ts:79:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The enum member should be initialized with a literal value such as a number or a string. + + 77 │ D = x | 0, + 78 │ E = x & 0, + > 79 │ F = x ^ 0, + │ ^^^^^ + 80 │ G = ~x, + 81 │ } + + +``` + +``` +invalid.ts:80:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The enum member should be initialized with a literal value such as a number or a string. + + 78 │ E = x & 0, + 79 │ F = x ^ 0, + > 80 │ G = ~x, + │ ^^ + 81 │ } + 82 │ + + +``` + + diff --git a/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/valid.ts b/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/valid.ts new file mode 100644 index 00000000000..5e890e5f283 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/valid.ts @@ -0,0 +1,79 @@ + +enum ValidRegex { + A = /test/ +} + + +enum ValidString { + A = 'test', +} + + +enum ValidLiteral { + A = `test`, +} + + +enum ValidNumber { + A = 42, +} + + +enum ValidNumber { + A = -42, +} + + +enum ValidNumber { + A = +42, +} + + +enum ValidNull { + A = null, +} + + +enum ValidPlain { + A, +} + + +enum ValidQuotedKey { + 'a', +} + + +enum ValidQuotedKeyWithAssignment { + 'a' = 1, +} + + +enum ValidKeyWithComputedSyntaxButNoComputedKey { + ['a'], +} + + +enum Foo { + A = 1 << 0, + B = 1 >> 0, + C = 1 >>> 0, + D = 1 | 0, + E = 1 & 0, + F = 1 ^ 0, + G = ~1, +} + + +enum FileAccess { + None = 0, + Read = 1, + Write = 1 << 1, + All = (1 | (1 << 1)) // ESlint rejects this +} + + +enum Parenthesis { + Left = ((("Left"))), + Right = (((1))), +} diff --git a/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/valid.ts.snap b/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/valid.ts.snap new file mode 100644 index 00000000000..260b1f8eedb --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/valid.ts.snap @@ -0,0 +1,90 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +assertion_line: 96 +expression: valid.ts +--- +# Input +```js + +enum ValidRegex { + A = /test/ +} + + +enum ValidString { + A = 'test', +} + + +enum ValidLiteral { + A = `test`, +} + + +enum ValidNumber { + A = 42, +} + + +enum ValidNumber { + A = -42, +} + + +enum ValidNumber { + A = +42, +} + + +enum ValidNull { + A = null, +} + + +enum ValidPlain { + A, +} + + +enum ValidQuotedKey { + 'a', +} + + +enum ValidQuotedKeyWithAssignment { + 'a' = 1, +} + + +enum ValidKeyWithComputedSyntaxButNoComputedKey { + ['a'], +} + + +enum Foo { + A = 1 << 0, + B = 1 >> 0, + C = 1 >>> 0, + D = 1 | 0, + E = 1 & 0, + F = 1 ^ 0, + G = ~1, +} + + +enum FileAccess { + None = 0, + Read = 1, + Write = 1 << 1, + All = (1 | (1 << 1)) // ESlint rejects this +} + + +enum Parenthesis { + Left = ((("Left"))), + Right = (((1))), +} + +``` + + diff --git a/crates/rome_js_syntax/src/expr_ext.rs b/crates/rome_js_syntax/src/expr_ext.rs index 02cf5dc9350..fea902b09b3 100644 --- a/crates/rome_js_syntax/src/expr_ext.rs +++ b/crates/rome_js_syntax/src/expr_ext.rs @@ -7,7 +7,8 @@ use crate::{ JsComputedMemberExpression, JsLiteralMemberName, JsLogicalExpression, JsNewExpression, JsNumberLiteralExpression, JsObjectExpression, JsPostUpdateExpression, JsReferenceIdentifier, JsRegexLiteralExpression, JsStaticMemberExpression, JsStringLiteralExpression, JsSyntaxKind, - JsSyntaxToken, JsTemplateExpression, JsUnaryExpression, OperatorPrecedence, T, + JsSyntaxToken, JsTemplateChunkElement, JsTemplateExpression, JsUnaryExpression, + OperatorPrecedence, T, }; use crate::{JsPreUpdateExpression, JsSyntaxKind::*}; use core::iter; @@ -515,6 +516,70 @@ impl JsStringLiteralExpression { } impl JsTemplateExpression { + /// Returns true if `self` is a template expression without a tag and without template elements. + /// + /// # Examples + /// + /// ``` + /// use rome_js_factory::make; + /// use rome_js_syntax::{AnyJsExpression, AnyJsTemplateElement, JsSyntaxKind, JsSyntaxToken}; + /// use std::iter; + /// + /// let tick = make::token(JsSyntaxKind::BACKTICK); + /// let empty_str = make::js_template_expression( + /// tick.clone(), + /// make::js_template_element_list(iter::empty()), + /// tick.clone(), + /// ).build(); + /// + /// let chunk = AnyJsTemplateElement::JsTemplateChunkElement( + /// make::js_template_chunk_element( + /// JsSyntaxToken::new_detached(JsSyntaxKind::TEMPLATE_CHUNK, "text", [], []) + /// ) + /// ); + /// let constant_str = make::js_template_expression( + /// tick.clone(), + /// make::js_template_element_list(iter::once(chunk.clone())), + /// tick.clone(), + /// ).build(); + /// + /// let constant_str2 = make::js_template_expression( + /// tick.clone(), + /// make::js_template_element_list([chunk.clone(), chunk]), + /// tick.clone(), + /// ).build(); + /// + /// let template_elt = AnyJsTemplateElement::JsTemplateElement( + /// make::js_template_element( + /// JsSyntaxToken::new_detached(JsSyntaxKind::DOLLAR_CURLY, "${", [], []), + /// AnyJsExpression::JsIdentifierExpression( + /// make::js_identifier_expression( + /// make::js_reference_identifier(make::ident("var")), + /// ), + /// ), + /// make::token(JsSyntaxKind::R_CURLY), + /// ) + /// ); + /// let template_str = make::js_template_expression( + /// tick.clone(), + /// make::js_template_element_list(iter::once(template_elt)), + /// tick, + /// ).build(); + /// + /// assert!(empty_str.is_constant()); + /// assert!(constant_str.is_constant()); + /// assert!(constant_str2.is_constant()); + /// assert!(!template_str.is_constant()); + /// ``` + /// + pub fn is_constant(&self) -> bool { + self.tag().is_none() + && self + .elements() + .into_iter() + .all(|e| JsTemplateChunkElement::can_cast(e.syntax().kind())) + } + /// The string chunks of the template. aka: /// `foo ${bar} foo` breaks down into: /// `QUASIS ELEMENT{EXPR} QUASIS` diff --git a/crates/rome_service/src/configuration/linter/rules.rs b/crates/rome_service/src/configuration/linter/rules.rs index cdb73960b32..a4ec4e3e4ed 100644 --- a/crates/rome_service/src/configuration/linter/rules.rs +++ b/crates/rome_service/src/configuration/linter/rules.rs @@ -1331,6 +1331,9 @@ pub struct Nursery { #[doc = "Require calls to isNaN() when checking for NaN."] #[serde(skip_serializing_if = "Option::is_none")] pub use_is_nan: Option, + #[doc = "Require all enum members to be literal values."] + #[serde(skip_serializing_if = "Option::is_none")] + pub use_literal_enum_members: Option, #[doc = "Enforce the usage of a literal access to properties over computed property access."] #[serde(skip_serializing_if = "Option::is_none")] pub use_literal_keys: Option, @@ -1352,7 +1355,7 @@ pub struct Nursery { } impl Nursery { const GROUP_NAME: &'static str = "nursery"; - pub(crate) const GROUP_RULES: [&'static str; 54] = [ + pub(crate) const GROUP_RULES: [&'static str; 55] = [ "noAriaUnsupportedElements", "noAssignInExpressions", "noBannedTypes", @@ -1401,6 +1404,7 @@ impl Nursery { "useHookAtTopLevel", "useIframeTitle", "useIsNan", + "useLiteralEnumMembers", "useLiteralKeys", "useMediaCaption", "useNamespaceKeyword", @@ -1408,7 +1412,7 @@ impl Nursery { "useValidLang", "useYield", ]; - const RECOMMENDED_RULES: [&'static str; 43] = [ + const RECOMMENDED_RULES: [&'static str; 44] = [ "noAriaUnsupportedElements", "noAssignInExpressions", "noBannedTypes", @@ -1447,13 +1451,14 @@ impl Nursery { "useExhaustiveDependencies", "useIframeTitle", "useIsNan", + "useLiteralEnumMembers", "useLiteralKeys", "useMediaCaption", "useValidAriaProps", "useValidLang", "useYield", ]; - const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 43] = [ + const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 44] = [ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), @@ -1494,11 +1499,12 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[49]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50]), 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]), ]; - const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 54] = [ + const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 55] = [ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), @@ -1553,6 +1559,7 @@ impl Nursery { 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]), ]; pub(crate) fn is_recommended(&self) -> bool { !matches!(self.recommended, Some(false)) } pub(crate) const fn is_not_recommended(&self) -> bool { @@ -1802,36 +1809,41 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47])); } } - if let Some(rule) = self.use_literal_keys.as_ref() { + if let Some(rule) = self.use_literal_enum_members.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48])); } } - if let Some(rule) = self.use_media_caption.as_ref() { + if let Some(rule) = self.use_literal_keys.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[49])); } } - if let Some(rule) = self.use_namespace_keyword.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[50])); } } - if let Some(rule) = self.use_valid_aria_props.as_ref() { + if let Some(rule) = self.use_namespace_keyword.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51])); } } - 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[52])); } } - 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[53])); } } + if let Some(rule) = self.use_yield.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[54])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> IndexSet { @@ -2076,36 +2088,41 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47])); } } - if let Some(rule) = self.use_literal_keys.as_ref() { + if let Some(rule) = self.use_literal_enum_members.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48])); } } - if let Some(rule) = self.use_media_caption.as_ref() { + if let Some(rule) = self.use_literal_keys.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[49])); } } - if let Some(rule) = self.use_namespace_keyword.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[50])); } } - if let Some(rule) = self.use_valid_aria_props.as_ref() { + if let Some(rule) = self.use_namespace_keyword.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51])); } } - 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[52])); } } - 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[53])); } } + if let Some(rule) = self.use_yield.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[54])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -2114,10 +2131,10 @@ 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>; 43] { + pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 44] { Self::RECOMMENDED_RULES_AS_FILTERS } - pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 54] { Self::ALL_RULES_AS_FILTERS } + pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 55] { Self::ALL_RULES_AS_FILTERS } #[doc = r" Select preset rules"] pub(crate) fn collect_preset_rules( &self, @@ -2187,6 +2204,7 @@ impl Nursery { "useHookAtTopLevel" => self.use_hook_at_top_level.as_ref(), "useIframeTitle" => self.use_iframe_title.as_ref(), "useIsNan" => self.use_is_nan.as_ref(), + "useLiteralEnumMembers" => self.use_literal_enum_members.as_ref(), "useLiteralKeys" => self.use_literal_keys.as_ref(), "useMediaCaption" => self.use_media_caption.as_ref(), "useNamespaceKeyword" => self.use_namespace_keyword.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 57f338d686c..c4304ea5150 100644 --- a/crates/rome_service/src/configuration/parse/json/rules.rs +++ b/crates/rome_service/src/configuration/parse/json/rules.rs @@ -959,6 +959,7 @@ impl VisitNode for Nursery { "useHookAtTopLevel", "useIframeTitle", "useIsNan", + "useLiteralEnumMembers", "useLiteralKeys", "useMediaCaption", "useNamespaceKeyword", @@ -1848,6 +1849,24 @@ impl VisitNode for Nursery { )); } }, + "useLiteralEnumMembers" => match value { + AnyJsonValue::JsonStringValue(_) => { + let mut configuration = RuleConfiguration::default(); + self.map_to_known_string(&value, name_text, &mut configuration, diagnostics)?; + self.use_literal_enum_members = Some(configuration); + } + AnyJsonValue::JsonObjectValue(_) => { + let mut configuration = RuleConfiguration::default(); + self.map_to_object(&value, name_text, &mut configuration, diagnostics)?; + self.use_literal_enum_members = Some(configuration); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_incorrect_type( + "object or string", + value.range(), + )); + } + }, "useLiteralKeys" => match value { AnyJsonValue::JsonStringValue(_) => { let mut configuration = RuleConfiguration::default(); diff --git a/editors/vscode/configuration_schema.json b/editors/vscode/configuration_schema.json index 5fb4035e49e..5a4b1593960 100644 --- a/editors/vscode/configuration_schema.json +++ b/editors/vscode/configuration_schema.json @@ -821,6 +821,13 @@ { "type": "null" } ] }, + "useLiteralEnumMembers": { + "description": "Require all enum members to be literal values.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "useLiteralKeys": { "description": "Enforce the usage of a literal access to properties over computed property access.", "anyOf": [ diff --git a/npm/backend-jsonrpc/src/workspace.ts b/npm/backend-jsonrpc/src/workspace.ts index 8192728c009..66748387728 100644 --- a/npm/backend-jsonrpc/src/workspace.ts +++ b/npm/backend-jsonrpc/src/workspace.ts @@ -584,6 +584,10 @@ export interface Nursery { * Require calls to isNaN() when checking for NaN. */ useIsNan?: RuleConfiguration; + /** + * Require all enum members to be literal values. + */ + useLiteralEnumMembers?: RuleConfiguration; /** * Enforce the usage of a literal access to properties over computed property access. */ @@ -1011,6 +1015,7 @@ export type Category = | "lint/nursery/noForEach" | "lint/nursery/useLiteralKeys" | "lint/nursery/noUselessConstructor" + | "lint/nursery/useLiteralEnumMembers" | "lint/nursery/noRedeclare" | "lint/nursery/useNamespaceKeyword" | "lint/nursery/noRedundantRoles" diff --git a/npm/rome/configuration_schema.json b/npm/rome/configuration_schema.json index 5fb4035e49e..5a4b1593960 100644 --- a/npm/rome/configuration_schema.json +++ b/npm/rome/configuration_schema.json @@ -821,6 +821,13 @@ { "type": "null" } ] }, + "useLiteralEnumMembers": { + "description": "Require all enum members to be literal values.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "useLiteralKeys": { "description": "Enforce the usage of a literal access to properties over computed property access.", "anyOf": [ diff --git a/website/src/pages/lint/rules/index.mdx b/website/src/pages/lint/rules/index.mdx index 0db17a55ca2..cf5d5849680 100644 --- a/website/src/pages/lint/rules/index.mdx +++ b/website/src/pages/lint/rules/index.mdx @@ -927,6 +927,12 @@ Enforces the usage of the attribute title for the element ifr Require calls to isNaN() when checking for NaN.
+

+ useLiteralEnumMembers +

+Require all enum members to be literal values. +
+

useLiteralKeys

diff --git a/website/src/pages/lint/rules/useLiteralEnumMembers.md b/website/src/pages/lint/rules/useLiteralEnumMembers.md new file mode 100644 index 00000000000..4570e2c433e --- /dev/null +++ b/website/src/pages/lint/rules/useLiteralEnumMembers.md @@ -0,0 +1,102 @@ +--- +title: Lint Rule useLiteralEnumMembers +parent: lint/rules/index +--- + +# useLiteralEnumMembers (since vnext) + +Require all enum members to be literal values. + +Usually, an enum member is initialized with a literal number or a literal string. +However, _TypeScript_ allows the value of an enum member to be many different kinds of expressions. +Using a computed enum member is often error-prone and confusing. +This rule requires the initialization of enum members with literal values. +It allows bitwise expressions for supporting [enum flags](https://stackoverflow.com/questions/39359740/what-are-enum-flags-in-typescript/39359953#39359953). + +In contrast to the equivalent _ESLint_ rule, this rule allows arbitrary bitwise constant expressions. + +Source: https://typescript-eslint.io/rules/prefer-literal-enum-member/ + +## Examples + +### Invalid + +```ts +const x = 2; +enum Computed { + A, + B = x, +} +``` + +
nursery/useLiteralEnumMembers.js:4:9 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   The enum member should be initialized with a literal value such as a number or a string.
+  
+    2 │ enum Computed {
+    3 │     A,
+  > 4 │     B = x,
+           ^
+    5 │ }
+    6 │ 
+  
+
+ +```ts +const x = 2; +enum Invalid { + A, + B = 2**3, +} +``` + +
nursery/useLiteralEnumMembers.js:4:9 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   The enum member should be initialized with a literal value such as a number or a string.
+  
+    2 │ enum Invalid {
+    3 │     A,
+  > 4 │     B = 2**3,
+           ^^^^
+    5 │ }
+    6 │ 
+  
+
+ +## Valid + +```ts +enum Direction { + Left, + Right, +} +``` + +```ts +enum Order { + Less = -1, + Equal = 0, + Greater = 1, +} +``` + +```ts +enum State { + Open = "Open", + Close = "Close", +} +``` + +```ts +enum FileAccess { + None = 0, + Read = 1, + Write = 1 << 1, + All = 1 | (1 << 1) +} +``` + +## Related links + +- [Disable a rule](/linter/#disable-a-lint-rule) +- [Rule options](/linter/#rule-options)