diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 5f55767765450..0ba5dae87cd21 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -141,6 +141,7 @@ mod typescript { pub mod prefer_enum_initializers; pub mod prefer_for_of; pub mod prefer_function_type; + pub mod prefer_literal_enum_member; pub mod prefer_ts_expect_error; pub mod triple_slash_reference; } @@ -492,6 +493,7 @@ oxc_macros::declare_all_lint_rules! { typescript::prefer_function_type, typescript::prefer_ts_expect_error, typescript::triple_slash_reference, + typescript::prefer_literal_enum_member, jest::expect_expect, jest::max_expects, jest::no_alias_methods, diff --git a/crates/oxc_linter/src/rules/typescript/prefer_literal_enum_member.rs b/crates/oxc_linter/src/rules/typescript/prefer_literal_enum_member.rs new file mode 100644 index 0000000000000..b534f66420eb6 --- /dev/null +++ b/crates/oxc_linter/src/rules/typescript/prefer_literal_enum_member.rs @@ -0,0 +1,347 @@ +use oxc_ast::{ast::Expression, AstKind}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; +use oxc_syntax::operator::{BinaryOperator, UnaryOperator}; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +#[derive(Debug, Error, Diagnostic)] +#[error("typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc).")] +#[diagnostic(severity(warning), help("Require all enum members to be literal values."))] +struct PreferLiteralEnumMemberDiagnostic(#[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct PreferLiteralEnumMember { + allow_bitwise_expressions: bool, +} + +declare_oxc_lint!( + /// ### What it does + /// Explicit enum value must only be a literal value (string, number, boolean, etc). + /// + /// ### Why is this bad? + /// TypeScript allows the value of an enum member to be many different kinds of valid JavaScript expressions. + /// However, because enums create their own scope whereby each enum member becomes a variable in that scope, developers are often surprised at the resultant values. + /// + /// ### Example + /// ```javascript + /// const imOutside = 2; + /// const b = 2; + /// enum Foo { + /// outer = imOutside, + /// a = 1, + /// b = a, + /// c = b, + /// } + /// ``` + PreferLiteralEnumMember, + correctness +); + +impl Rule for PreferLiteralEnumMember { + fn from_configuration(value: serde_json::Value) -> Self { + let options: Option<&serde_json::Value> = value.get(0); + + Self { + allow_bitwise_expressions: options + .and_then(|x| x.get("allowBitwiseExpressions")) + .and_then(serde_json::Value::as_bool) + .unwrap_or(false), + } + } + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::TSEnumMember(decl) = node.kind() else { return }; + let Some(initializer) = &decl.initializer else { return }; + if initializer.is_literal() { + return; + } + + if let Expression::TemplateLiteral(template) = initializer { + if template.expressions.len() == 0 { + return; + } + } + + if let Expression::UnaryExpression(unary_expr) = initializer { + if unary_expr.argument.is_literal() { + if matches!( + unary_expr.operator, + UnaryOperator::UnaryPlus | UnaryOperator::UnaryNegation, + ) { + return; + } + + if self.allow_bitwise_expressions + && matches!(unary_expr.operator, UnaryOperator::BitwiseNot) + { + return; + } + } + } + + if self.allow_bitwise_expressions { + if let Expression::BinaryExpression(binary_expr) = initializer { + if matches!( + binary_expr.operator, + BinaryOperator::BitwiseOR + | BinaryOperator::BitwiseAnd + | BinaryOperator::BitwiseXOR + | BinaryOperator::ShiftLeft + | BinaryOperator::ShiftRight + | BinaryOperator::ShiftRightZeroFill + ) && binary_expr.left.is_literal() + && binary_expr.right.is_literal() + { + return; + } + } + } + + ctx.diagnostic(PreferLiteralEnumMemberDiagnostic(decl.span)); + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ( + " + enum ValidRegex { + A = /test/, + } + ", + None, + ), + ( + " + enum ValidString { + A = 'test', + } + ", + None, + ), + ( + " + enum ValidLiteral { + A = `test`, + } + ", + None, + ), + ( + " + enum ValidNumber { + A = 42, + } + ", + None, + ), + ( + " + enum ValidNumber { + A = -42, + } + ", + None, + ), + ( + " + enum ValidNumber { + A = +42, + } + ", + None, + ), + ( + " + enum ValidNull { + A = null, + } + ", + None, + ), + ( + " + enum ValidPlain { + A, + } + ", + None, + ), + ( + " + enum ValidQuotedKey { + 'a', + } + ", + None, + ), + ( + " + enum ValidQuotedKeyWithAssignment { + 'a' = 1, + } + ", + None, + ), + ( + " + enum Foo { + A = 1 << 0, + B = 1 >> 0, + C = 1 >>> 0, + D = 1 | 0, + E = 1 & 0, + F = 1 ^ 0, + G = ~1, + } + ", + Some(serde_json::json!([{ "allowBitwiseExpressions": true }])), + ), + ]; + + let fail = vec![ + ( + " + enum InvalidObject { + A = {}, + } + ", + None, + ), + ( + " + enum InvalidArray { + A = [], + } + ", + None, + ), + ( + " + enum InvalidTemplateLiteral { + A = `foo ${0}`, + } + ", + None, + ), + ( + " + enum InvalidConstructor { + A = new Set(), + } + ", + None, + ), + ( + " + enum InvalidExpression { + A = 2 + 2, + } + ", + None, + ), + ( + " + enum InvalidExpression { + A = delete 2, + B = -a, + C = void 2, + D = ~2, + E = !0, + } + ", + None, + ), + ( + " + const variable = 'Test'; + enum InvalidVariable { + A = 'TestStr', + B = 2, + C, + V = variable, + } + ", + None, + ), + ( + " + enum InvalidEnumMember { + A = 'TestStr', + B = A, + } + ", + None, + ), + ( + " + const Valid = { A: 2 }; + enum InvalidObjectMember { + A = 'TestStr', + B = Valid.A, + } + ", + None, + ), + ( + " + enum Valid { + A, + } + enum InvalidEnumMember { + A = 'TestStr', + B = Valid.A, + } + ", + None, + ), + ( + " + const obj = { a: 1 }; + enum InvalidSpread { + A = 'TestStr', + B = { ...a }, + } + ", + None, + ), + ( + " + enum Foo { + A = 1 << 0, + B = 1 >> 0, + C = 1 >>> 0, + D = 1 | 0, + E = 1 & 0, + F = 1 ^ 0, + G = ~1, + } + ", + Some(serde_json::json!([{ "allowBitwiseExpressions": false }])), + ), + ( + " + 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, + } + ", + Some(serde_json::json!([{ "allowBitwiseExpressions": true }])), + ), + ]; + + Tester::new(PreferLiteralEnumMember::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/prefer_literal_enum_member.snap b/crates/oxc_linter/src/snapshots/prefer_literal_enum_member.snap new file mode 100644 index 0000000000000..a085f206b17fb --- /dev/null +++ b/crates/oxc_linter/src/snapshots/prefer_literal_enum_member.snap @@ -0,0 +1,264 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: prefer_literal_enum_member +--- + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:3:12] + 2 │ enum InvalidObject { + 3 │ A = {}, + · ────── + 4 │ } + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:3:12] + 2 │ enum InvalidArray { + 3 │ A = [], + · ────── + 4 │ } + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:3:12] + 2 │ enum InvalidTemplateLiteral { + 3 │ A = `foo ${0}`, + · ────────────── + 4 │ } + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:3:12] + 2 │ enum InvalidConstructor { + 3 │ A = new Set(), + · ───────────── + 4 │ } + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:3:12] + 2 │ enum InvalidExpression { + 3 │ A = 2 + 2, + · ───────── + 4 │ } + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:3:12] + 2 │ enum InvalidExpression { + 3 │ A = delete 2, + · ──────────── + 4 │ B = -a, + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:4:12] + 3 │ A = delete 2, + 4 │ B = -a, + · ────── + 5 │ C = void 2, + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:5:12] + 4 │ B = -a, + 5 │ C = void 2, + · ────────── + 6 │ D = ~2, + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:6:12] + 5 │ C = void 2, + 6 │ D = ~2, + · ────── + 7 │ E = !0, + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:7:12] + 6 │ D = ~2, + 7 │ E = !0, + · ────── + 8 │ } + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:7:12] + 6 │ C, + 7 │ V = variable, + · ──────────── + 8 │ } + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:4:12] + 3 │ A = 'TestStr', + 4 │ B = A, + · ───── + 5 │ } + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:5:12] + 4 │ A = 'TestStr', + 5 │ B = Valid.A, + · ─────────── + 6 │ } + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:7:12] + 6 │ A = 'TestStr', + 7 │ B = Valid.A, + · ─────────── + 8 │ } + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:5:12] + 4 │ A = 'TestStr', + 5 │ B = { ...a }, + · ──────────── + 6 │ } + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:3:12] + 2 │ enum Foo { + 3 │ A = 1 << 0, + · ────────── + 4 │ B = 1 >> 0, + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:4:12] + 3 │ A = 1 << 0, + 4 │ B = 1 >> 0, + · ────────── + 5 │ C = 1 >>> 0, + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:5:12] + 4 │ B = 1 >> 0, + 5 │ C = 1 >>> 0, + · ─────────── + 6 │ D = 1 | 0, + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:6:12] + 5 │ C = 1 >>> 0, + 6 │ D = 1 | 0, + · ───────── + 7 │ E = 1 & 0, + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:7:12] + 6 │ D = 1 | 0, + 7 │ E = 1 & 0, + · ───────── + 8 │ F = 1 ^ 0, + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:8:12] + 7 │ E = 1 & 0, + 8 │ F = 1 ^ 0, + · ───────── + 9 │ G = ~1, + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:9:12] + 8 │ F = 1 ^ 0, + 9 │ G = ~1, + · ────── + 10 │ } + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:4:12] + 3 │ enum Foo { + 4 │ A = x << 0, + · ────────── + 5 │ B = x >> 0, + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:5:12] + 4 │ A = x << 0, + 5 │ B = x >> 0, + · ────────── + 6 │ C = x >>> 0, + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:6:12] + 5 │ B = x >> 0, + 6 │ C = x >>> 0, + · ─────────── + 7 │ D = x | 0, + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:7:12] + 6 │ C = x >>> 0, + 7 │ D = x | 0, + · ───────── + 8 │ E = x & 0, + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:8:12] + 7 │ D = x | 0, + 8 │ E = x & 0, + · ───────── + 9 │ F = x ^ 0, + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:9:12] + 8 │ E = x & 0, + 9 │ F = x ^ 0, + · ───────── + 10 │ G = ~x, + ╰──── + help: Require all enum members to be literal values. + + ⚠ typescript-eslint(prefer-literal-enum-member): Explicit enum value must only be a literal value (string, number, boolean, etc). + ╭─[prefer_literal_enum_member.tsx:10:12] + 9 │ F = x ^ 0, + 10 │ G = ~x, + · ────── + 11 │ } + ╰──── + help: Require all enum members to be literal values.