-
-
Notifications
You must be signed in to change notification settings - Fork 472
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(linter): @typescript-eslint/prefer-literal-enum-member (#3134)
Related issue: #2180 original implementation - code: https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/src/rules/prefer-literal-enum-member.ts - test: https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/tests/rules/prefer-literal-enum-member.test.ts - doc: https://typescript-eslint.io/rules/prefer-literal-enum-member/
- Loading branch information
Showing
3 changed files
with
613 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
347 changes: 347 additions & 0 deletions
347
crates/oxc_linter/src/rules/typescript/prefer_literal_enum_member.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
Oops, something went wrong.