Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(linter): @typescript-eslint/prefer-literal-enum-member #3134

Merged
merged 1 commit into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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,
Expand Down
347 changes: 347 additions & 0 deletions crates/oxc_linter/src/rules/typescript/prefer_literal_enum_member.rs
Original file line number Diff line number Diff line change
@@ -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();
}
Loading
Loading