diff --git a/CHANGELOG.md b/CHANGELOG.md index 798d56be795..26942134803 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -235,6 +235,31 @@ if no error diagnostics are emitted. var x = a => 1 ? 2 : 3; ``` +- Relax [`useLiteralEnumMembers`](https://docs.rome.tools/lint/rules/useLiteralEnumMembers/) + + Enum members that refers to previous enum members are now allowed. + This allows common pattern in enum flags like in the following example: + + ```ts + enum FileAccess { + None = 0, + Read = 1, + Write = 1 << 1, + All = Read | Write, + } + ``` + + Arbitrary numeric constant expressions are also allowed: + + ```ts + enum FileAccess { + None = 0, + Read = 2**0, + Write = 2**1, + All = Read | Write, + } + ``` + - Improve [useLiteralKeys](https://docs.rome.tools/lint/rules/useLiteralKeys/). Now, the rule suggests simplifying computed properties to string literal properties: 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 index 0475fc32a2e..0c5ce3f18cf 100644 --- 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 @@ -1,10 +1,11 @@ use rome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic}; use rome_console::markup; use rome_js_syntax::{ - AnyJsExpression, JsBinaryExpression, JsSyntaxKind, JsUnaryExpression, JsUnaryOperator, - TsEnumMember, + AnyJsExpression, AnyJsLiteralExpression, AnyJsMemberExpression, JsUnaryOperator, + TsEnumDeclaration, }; -use rome_rowan::AstNode; +use rome_rowan::{AstNode, TextRange}; +use rustc_hash::FxHashSet; declare_rule! { /// Require all enum members to be literal values. @@ -12,10 +13,9 @@ declare_rule! { /// 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. + /// This rule requires the initialization of enum members with constant expressions. + /// It allows numeric and bitwise expressions for supporting [enum flags](https://stackoverflow.com/questions/39359740/what-are-enum-flags-in-typescript/39359953#39359953). + /// It also allows referencing previous enum members. /// /// Source: https://typescript-eslint.io/rules/prefer-literal-enum-member/ /// @@ -31,14 +31,6 @@ declare_rule! { /// } /// ``` /// - /// ```ts,expect_diagnostic - /// const x = 2; - /// enum Invalid { - /// A, - /// B = 2**3, - /// } - /// ``` - /// /// ## Valid /// /// ```ts @@ -68,7 +60,7 @@ declare_rule! { /// None = 0, /// Read = 1, /// Write = 1 << 1, - /// All = 1 | (1 << 1) + /// All = Read | Write /// } /// ``` pub(crate) UseLiteralEnumMembers { @@ -79,37 +71,52 @@ declare_rule! { } impl Rule for UseLiteralEnumMembers { - type Query = Ast; - type State = (); - type Signals = Option; + type Query = Ast; + type State = TextRange; + type Signals = Vec; 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 enum_declaration = ctx.query(); + let mut result = Vec::new(); + let mut enum_member_names = FxHashSet::default(); + let Ok(enum_name) = enum_declaration.id() else { + return result; }; - 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; + let Some(enum_name) = enum_name.as_js_identifier_binding() + .and_then(|x| x.name_token().ok()) else { + return result; + }; + let enum_name = enum_name.text_trimmed(); + for enum_member in enum_declaration.members() { + let Ok(enum_member) = enum_member else { + continue; + }; + // no initializer => sequentially assigned literal integer + if let Some(initializer) = enum_member.initializer() { + if let Ok(initializer) = initializer.expression() { + let range = initializer.range(); + if !is_constant_enum_expression(initializer, enum_name, &enum_member_names) { + result.push(range); + } + } + }; + if let Ok(name) = enum_member.name() { + if let Some(name) = name.name() { + enum_member_names.insert(name.text().to_string()); + } } } - Some(()) + result } - fn diagnostic(ctx: &RuleContext, _: &Self::State) -> Option { - let enum_member = ctx.query(); + fn diagnostic( + _: &RuleContext, + initializer_range: &Self::State, + ) -> Option { Some(RuleDiagnostic::new( rule_category!(), - enum_member.initializer()?.expression().ok()?.range(), + initializer_range, markup! { "The enum member should be initialized with a literal value such as a number or a string." }, @@ -117,23 +124,95 @@ impl Rule for UseLiteralEnumMembers { } } -/// 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; +/// Returns true if `expr` is a constant enum expression. +/// A constant enum expression can contain numbers, string literals, and reference to +/// one of the enum member of `enum_member_names` of the enum name `enum_name`. +/// These values can be combined thanks to numeric, bitwise, and concatenation operations. +fn is_constant_enum_expression( + expr: AnyJsExpression, + enum_name: &str, + enum_member_names: &FxHashSet, +) -> bool { + (move || { + // stack that holds expressions to validate. + let mut stack = Vec::new(); + stack.push(expr); + while let Some(expr) = stack.pop() { + match expr.omit_parentheses() { + AnyJsExpression::AnyJsLiteralExpression(expr) => { + if !matches!( + expr, + AnyJsLiteralExpression::JsNumberLiteralExpression(_) + | AnyJsLiteralExpression::JsStringLiteralExpression(_) + ) { + return Some(false); + } + } + AnyJsExpression::JsTemplateExpression(expr) => { + if !expr.is_constant() { + return Some(false); + } + } + AnyJsExpression::JsUnaryExpression(expr) => { + if !matches!( + expr.operator(), + Ok(JsUnaryOperator::BitwiseNot + | JsUnaryOperator::Minus + | JsUnaryOperator::Plus) + ) { + return Some(false); + } + stack.push(expr.argument().ok()?) + } + AnyJsExpression::JsBinaryExpression(expr) => { + if !expr.is_binary_operation() && !expr.is_numeric_operation() { + return Some(false); + } + stack.push(expr.left().ok()?); + stack.push(expr.right().ok()?); + } + AnyJsExpression::JsIdentifierExpression(expr) => { + // Allow reference to previous member name + let name = expr.name().ok()?; + if !enum_member_names.contains(name.value_token().ok()?.text_trimmed()) { + return Some(false); + } + } + AnyJsExpression::JsStaticMemberExpression(expr) => { + if !is_enum_member_reference(expr.into(), enum_name, enum_member_names) { + return Some(false); + } + } + AnyJsExpression::JsComputedMemberExpression(expr) => { + if !is_enum_member_reference(expr.into(), enum_name, enum_member_names) { + return Some(false); + } + } + _ => { + return Some(false); + } } - } else if !matches!( - node.kind(), - JsSyntaxKind::JS_NUMBER_LITERAL_EXPRESSION | JsSyntaxKind::JS_PARENTHESIZED_EXPRESSION - ) { - return false; } - } - true + Some(true) + })() + .unwrap_or_default() +} + +// Return true if `expr` is a reference to one of the enum member `enum_member_names` +// of the enum named `enum_name`. +fn is_enum_member_reference( + expr: AnyJsMemberExpression, + enum_name: &str, + enum_member_names: &FxHashSet, +) -> bool { + (move || { + // Allow reference to previous member name namespaced by the enum name + let object = expr.object().ok()?.omit_parentheses(); + let object = object.as_js_identifier_expression()?; + Some( + object.name().ok()?.has_name(enum_name) + && enum_member_names.contains(expr.member_name()?.text()), + ) + })() + .unwrap_or_default() } diff --git a/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/invalid.ts b/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/invalid.ts index 7d250761ed7..8e733c16c85 100644 --- a/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/invalid.ts +++ b/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/invalid.ts @@ -1,74 +1,38 @@ -enum InvalidObject { +enum InvalidLiterals { A = {}, + B = [], + C = true, + D = 1n, } - -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, + D = !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, + A = Valid.A, } - -const obj = { a: 1 }; -enum InvalidSpread { - A = 'TestStr', - B = { ...a }, -} - - const x = 1; enum Foo { A = x << 0, @@ -80,3 +44,14 @@ enum Foo { G = ~x, } +enum InvalidRef { + A = A, + B = InvalidRef.B, + C = InvalidRef["C"], + D = E, + E = InvalidRef.F, + F = InvalidRef["G"], + G +} + +export {} \ No newline at end of file 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 index 487acaaef38..c6a04bcd0c6 100644 --- a/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/invalid.ts.snap +++ b/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/invalid.ts.snap @@ -1,81 +1,44 @@ --- source: crates/rome_js_analyze/tests/spec_tests.rs -assertion_line: 96 expression: invalid.ts --- # Input ```js -enum InvalidObject { +enum InvalidLiterals { A = {}, + B = [], + C = true, + D = 1n, } - -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, + D = !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, + A = Valid.A, } - -const obj = { a: 1 }; -enum InvalidSpread { - A = 'TestStr', - B = { ...a }, -} - - const x = 1; enum Foo { A = x << 0, @@ -87,7 +50,17 @@ enum Foo { G = ~x, } +enum InvalidRef { + A = A, + B = InvalidRef.B, + C = InvalidRef["C"], + D = E, + E = InvalidRef.F, + F = InvalidRef["G"], + G +} +export {} ``` # Diagnostics @@ -96,306 +69,367 @@ 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 { + 1 │ enum InvalidLiterals { > 2 │ A = {}, │ ^^ - 3 │ } - 4 │ + 3 │ B = [], + 4 │ C = true, ``` ``` -invalid.ts:7:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:3: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 = [], + 1 │ enum InvalidLiterals { + 2 │ A = {}, + > 3 │ B = [], │ ^^ - 8 │ } - 9 │ + 4 │ C = true, + 5 │ D = 1n, ``` ``` -invalid.ts:12:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:4: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 │ + 2 │ A = {}, + 3 │ B = [], + > 4 │ C = true, + │ ^^^^ + 5 │ D = 1n, + 6 │ } ``` ``` -invalid.ts:17:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:5: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 │ + 3 │ B = [], + 4 │ C = true, + > 5 │ D = 1n, + │ ^^ + 6 │ } + 7 │ ``` ``` -invalid.ts:22:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:9: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 │ + 8 │ enum InvalidTemplateLiteral { + > 9 │ A = `foo ${0}`, + │ ^^^^^^^^^^ + 10 │ } + 11 │ ``` ``` -invalid.ts:26:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:13: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, + 12 │ enum InvalidConstructor { + > 13 │ A = new Set(), + │ ^^^^^^^^^ + 14 │ } + 15 │ + + +``` + +``` +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 InvalidExpression { + > 17 │ A = delete 2, │ ^^^^^^^^ - 27 │ B = -a, - 28 │ C = void 2, + 18 │ B = -a, + 19 │ C = void 2, ``` ``` -invalid.ts:27:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:18: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, + 16 │ enum InvalidExpression { + 17 │ A = delete 2, + > 18 │ B = -a, │ ^^ - 28 │ C = void 2, - 29 │ D = ~2, + 19 │ C = void 2, + 20 │ D = !0, ``` ``` -invalid.ts:28:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:19: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, + 17 │ A = delete 2, + 18 │ B = -a, + > 19 │ C = void 2, │ ^^^^^^ - 29 │ D = ~2, - 30 │ E = !0, + 20 │ D = !0, + 21 │ } ``` ``` -invalid.ts:30:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:20: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, + 18 │ B = -a, + 19 │ C = void 2, + > 20 │ D = !0, │ ^^ - 31 │ } - 32 │ + 21 │ } + 22 │ ``` ``` -invalid.ts:39:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:26: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, + 24 │ enum InvalidVariable { + 25 │ A = 'TestStr', + > 26 │ V = variable, │ ^^^^^^^^ - 40 │ } - 41 │ + 27 │ } + 28 │ ``` ``` -invalid.ts:45:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:33: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 │ + 31 │ } + 32 │ enum InvalidEnumMember { + > 33 │ A = Valid.A, + │ ^^^^^^^ + 34 │ } + 35 │ ``` ``` -invalid.ts:52:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:38: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 │ + 36 │ const x = 1; + 37 │ enum Foo { + > 38 │ A = x << 0, + │ ^^^^^^ + 39 │ B = x >> 0, + 40 │ C = x >>> 0, + + +``` + +``` +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 │ enum Foo { + 38 │ A = x << 0, + > 39 │ B = x >> 0, + │ ^^^^^^ + 40 │ C = x >>> 0, + 41 │ D = x | 0, ``` ``` -invalid.ts:61:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:40: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, + 38 │ A = x << 0, + 39 │ B = x >> 0, + > 40 │ C = x >>> 0, │ ^^^^^^^ - 62 │ } - 63 │ + 41 │ D = x | 0, + 42 │ E = x & 0, ``` ``` -invalid.ts:68:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:41: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 │ + 39 │ B = x >> 0, + 40 │ C = x >>> 0, + > 41 │ D = x | 0, + │ ^^^^^ + 42 │ E = x & 0, + 43 │ F = x ^ 0, ``` ``` -invalid.ts:74:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:42: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, + 40 │ C = x >>> 0, + 41 │ D = x | 0, + > 42 │ E = x & 0, + │ ^^^^^ + 43 │ F = x ^ 0, + 44 │ G = ~x, ``` ``` -invalid.ts:75:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:43: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, + 41 │ D = x | 0, + 42 │ E = x & 0, + > 43 │ F = x ^ 0, + │ ^^^^^ + 44 │ G = ~x, + 45 │ } ``` ``` -invalid.ts:76:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:44: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, + 42 │ E = x & 0, + 43 │ F = x ^ 0, + > 44 │ G = ~x, + │ ^^ + 45 │ } + 46 │ ``` ``` -invalid.ts:77:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:48: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, + 47 │ enum InvalidRef { + > 48 │ A = A, + │ ^ + 49 │ B = InvalidRef.B, + 50 │ C = InvalidRef["C"], ``` ``` -invalid.ts:78:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:49: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, + 47 │ enum InvalidRef { + 48 │ A = A, + > 49 │ B = InvalidRef.B, + │ ^^^^^^^^^^^^ + 50 │ C = InvalidRef["C"], + 51 │ D = E, ``` ``` -invalid.ts:79:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:50: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 │ } + 48 │ A = A, + 49 │ B = InvalidRef.B, + > 50 │ C = InvalidRef["C"], + │ ^^^^^^^^^^^^^^^ + 51 │ D = E, + 52 │ E = InvalidRef.F, ``` ``` -invalid.ts:80:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:51: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 │ + 49 │ B = InvalidRef.B, + 50 │ C = InvalidRef["C"], + > 51 │ D = E, + │ ^ + 52 │ E = InvalidRef.F, + 53 │ F = InvalidRef["G"], + + +``` + +``` +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 │ C = InvalidRef["C"], + 51 │ D = E, + > 52 │ E = InvalidRef.F, + │ ^^^^^^^^^^^^ + 53 │ F = InvalidRef["G"], + 54 │ G + + +``` + +``` +invalid.ts:53:7 lint/nursery/useLiteralEnumMembers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The enum member should be initialized with a literal value such as a number or a string. + + 51 │ D = E, + 52 │ E = InvalidRef.F, + > 53 │ F = InvalidRef["G"], + │ ^^^^^^^^^^^^^^^ + 54 │ G + 55 │ } ``` diff --git a/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/valid.ts b/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/valid.ts index 5e890e5f283..bd9ffb90098 100644 --- a/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/valid.ts +++ b/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/valid.ts @@ -1,60 +1,27 @@ - -enum ValidRegex { - A = /test/ -} - - enum ValidString { A = 'test', + B = 'div' + 'ided', + C = `test2`, + D = `di` + `ided2`, + AA = A + ValidString.A, } - -enum ValidLiteral { - A = `test`, -} - - enum ValidNumber { - A = 42, -} - - -enum ValidNumber { - A = -42, -} - - -enum ValidNumber { - A = +42, -} - - -enum ValidNull { - A = null, -} - - -enum ValidPlain { A, + B = 42, + C = -42, + D = +42, + E = 2 + 2, + F = A + ValidNumber.B, } - enum ValidQuotedKey { - 'a', -} - - -enum ValidQuotedKeyWithAssignment { - 'a' = 1, -} - - -enum ValidKeyWithComputedSyntaxButNoComputedKey { - ['a'], + 'A', + 'B' = 1, + ['C'], } - -enum Foo { +enum ValidFlags { A = 1 << 0, B = 1 >> 0, C = 1 >>> 0, @@ -64,16 +31,30 @@ enum Foo { G = ~1, } - enum FileAccess { None = 0, Read = 1, Write = 1 << 1, - All = (1 | (1 << 1)) // ESlint rejects this + All = (1 | (1 << 1)), } +enum FileAccessWithRef { + None = 0, + Read = 1, + Write = FileAccessWithRef["Read"] << 1, + All = Read | FileAccessWithRef.Write, +} + +enum ValidRef { + "A", + "B", + C = A | B, +} -enum Parenthesis { - Left = ((("Left"))), - Right = (((1))), +enum ValidComputedRef { + ["A"], + ["B"], + C = A | B, } + +export {} \ No newline at end of file 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 index 260b1f8eedb..9e049407836 100644 --- a/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/valid.ts.snap +++ b/crates/rome_js_analyze/tests/specs/nursery/useLiteralEnumMembers/valid.ts.snap @@ -1,67 +1,33 @@ --- 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', + B = 'div' + 'ided', + C = `test2`, + D = `di` + `ided2`, + AA = A + ValidString.A, } - -enum ValidLiteral { - A = `test`, -} - - -enum ValidNumber { - A = 42, -} - - enum ValidNumber { - A = -42, -} - - -enum ValidNumber { - A = +42, -} - - -enum ValidNull { - A = null, -} - - -enum ValidPlain { A, + B = 42, + C = -42, + D = +42, + E = 2 + 2, + F = A + ValidNumber.B, } - enum ValidQuotedKey { - 'a', -} - - -enum ValidQuotedKeyWithAssignment { - 'a' = 1, -} - - -enum ValidKeyWithComputedSyntaxButNoComputedKey { - ['a'], + 'A', + 'B' = 1, + ['C'], } - -enum Foo { +enum ValidFlags { A = 1 << 0, B = 1 >> 0, C = 1 >>> 0, @@ -71,20 +37,33 @@ enum Foo { G = ~1, } - enum FileAccess { None = 0, Read = 1, Write = 1 << 1, - All = (1 | (1 << 1)) // ESlint rejects this + All = (1 | (1 << 1)), } +enum FileAccessWithRef { + None = 0, + Read = 1, + Write = FileAccessWithRef["Read"] << 1, + All = Read | FileAccessWithRef.Write, +} + +enum ValidRef { + "A", + "B", + C = A | B, +} -enum Parenthesis { - Left = ((("Left"))), - Right = (((1))), +enum ValidComputedRef { + ["A"], + ["B"], + C = A | B, } +export {} ``` diff --git a/crates/rome_js_syntax/src/expr_ext.rs b/crates/rome_js_syntax/src/expr_ext.rs index 73919dcd830..1afe3d34cc4 100644 --- a/crates/rome_js_syntax/src/expr_ext.rs +++ b/crates/rome_js_syntax/src/expr_ext.rs @@ -2,11 +2,11 @@ use crate::numbers::parse_js_number; use crate::static_value::{QuotedString, StaticStringValue, StaticValue}; use crate::{ - AnyJsCallArgument, AnyJsExpression, AnyJsLiteralExpression, AnyJsTemplateElement, - JsArrayExpression, JsArrayHole, JsAssignmentExpression, JsBinaryExpression, JsCallExpression, - JsComputedMemberAssignment, JsComputedMemberExpression, JsLiteralMemberName, - JsLogicalExpression, JsNewExpression, JsNumberLiteralExpression, JsObjectExpression, - JsPostUpdateExpression, JsReferenceIdentifier, JsRegexLiteralExpression, + AnyJsCallArgument, AnyJsExpression, AnyJsLiteralExpression, AnyJsObjectMemberName, + AnyJsTemplateElement, JsArrayExpression, JsArrayHole, JsAssignmentExpression, + JsBinaryExpression, JsCallExpression, JsComputedMemberAssignment, JsComputedMemberExpression, + JsLiteralMemberName, JsLogicalExpression, JsNewExpression, JsNumberLiteralExpression, + JsObjectExpression, JsPostUpdateExpression, JsReferenceIdentifier, JsRegexLiteralExpression, JsStaticMemberExpression, JsStringLiteralExpression, JsSyntaxKind, JsSyntaxToken, JsTemplateChunkElement, JsTemplateExpression, JsUnaryExpression, OperatorPrecedence, T, }; @@ -242,8 +242,16 @@ impl JsBinaryExpression { Ok(kind) } + /// Whether this is a numeric operation, such as `+`, `-`, `*`, `%`, `**`. + pub fn is_numeric_operation(&self) -> bool { + matches!( + self.operator_token().map(|t| t.kind()), + Ok(T![+] | T![-] | T![*] | T![/] | T![%] | T![**]) + ) + } + /// Whether this is a binary operation, such as `<<`, `>>`, `>>>`, `&`, `|`, `^`. - pub fn is_binary_operator(&self) -> bool { + pub fn is_binary_operation(&self) -> bool { matches!( self.operator_token().map(|t| t.kind()), Ok(T![<<] | T![>>] | T![>>>] | T![&] | T![|] | T![^]) @@ -980,6 +988,61 @@ impl AnyJsMemberExpression { } } +impl AnyJsObjectMemberName { + /// Returns the member name of the current node + /// if it is a literal member name or a computed member with a literal value. + /// + /// ## Examples + /// + /// ``` + /// use rome_js_syntax::{AnyJsObjectMemberName, AnyJsExpression, AnyJsLiteralExpression, T}; + /// use rome_js_factory::make; + /// + /// let name = make::js_literal_member_name(make::ident("a")); + /// let name = AnyJsObjectMemberName::JsLiteralMemberName(name); + /// assert_eq!(name.name().unwrap().text(), "a"); + /// + /// let quoted_name = make::js_literal_member_name(make::js_string_literal("a")); + /// let quoted_name = AnyJsObjectMemberName::JsLiteralMemberName(quoted_name); + /// assert_eq!(quoted_name.name().unwrap().text(), "a"); + /// + /// let number_name = make::js_literal_member_name(make::js_number_literal(42)); + /// let number_name = AnyJsObjectMemberName::JsLiteralMemberName(number_name); + /// assert_eq!(number_name.name().unwrap().text(), "42"); + /// + /// let string_literal = make::js_string_literal_expression(make::js_string_literal("a")); + /// let string_literal = AnyJsExpression::AnyJsLiteralExpression(AnyJsLiteralExpression::from(string_literal)); + /// let computed = make::js_computed_member_name(make::token(T!['[']), string_literal, make::token(T![']'])); + /// let computed = AnyJsObjectMemberName::JsComputedMemberName(computed); + /// assert_eq!(computed.name().unwrap().text(), "a"); + /// ``` + pub fn name(&self) -> Option { + let token = match self { + AnyJsObjectMemberName::JsComputedMemberName(expr) => { + let expr = expr.expression().ok()?; + match expr.omit_parentheses() { + AnyJsExpression::AnyJsLiteralExpression(expr) => expr.value_token().ok()?, + AnyJsExpression::JsTemplateExpression(expr) => { + if !expr.is_constant() { + return None; + } + let chunk = expr.elements().first()?; + let chunk = chunk.as_js_template_chunk_element()?; + chunk.template_chunk_token().ok()? + } + _ => return None, + } + } + AnyJsObjectMemberName::JsLiteralMemberName(expr) => expr.value().ok()?, + }; + Some(if token.kind() == JsSyntaxKind::JS_STRING_LITERAL { + StaticStringValue::Quoted(QuotedString::new(token)) + } else { + StaticStringValue::Unquoted(token) + }) + } +} + /// Check if `expr` refers to a name that is directly referenced or indirectly via `globalThis` or `window`. /// Returns the reference and the name. /// diff --git a/website/src/pages/lint/rules/useLiteralEnumMembers.md b/website/src/pages/lint/rules/useLiteralEnumMembers.md index 20f6b044529..5a75deee358 100644 --- a/website/src/pages/lint/rules/useLiteralEnumMembers.md +++ b/website/src/pages/lint/rules/useLiteralEnumMembers.md @@ -10,10 +10,9 @@ 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. +This rule requires the initialization of enum members with constant expressions. +It allows numeric and bitwise expressions for supporting [enum flags](https://stackoverflow.com/questions/39359740/what-are-enum-flags-in-typescript/39359953#39359953). +It also allows referencing previous enum members. Source: https://typescript-eslint.io/rules/prefer-literal-enum-member/ @@ -42,27 +41,6 @@ enum Computed { -```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 @@ -92,7 +70,7 @@ enum FileAccess { None = 0, Read = 1, Write = 1 << 1, - All = 1 | (1 << 1) + All = Read | Write } ```