feat(rome_js_analyze): `useEnumInitializers`
MODIFY
crates/rome_diagnostics_categories/src/categories.rs
crates/rome_diagnostics_categories/src/categories.rs
@@ -98,6 +98,7 @@ define_dategories! {
"lint/nursery/useCamelCase": "https://docs.rome.tools/lint/rules/useCamelCase",
"lint/nursery/useConst":"https://docs.rome.tools/lint/rules/useConst",
"lint/nursery/useDefaultSwitchClauseLast":"https://docs.rome.tools/lint/rules/useDefaultSwitchClauseLast",
+ "lint/nursery/useEnumInitializers":"https://docs.rome.tools/lint/rules/useEnumInitializers",
"lint/nursery/useExhaustiveDependencies": "https://docs.rome.tools/lint/rules/useExhaustiveDependencies",
"lint/nursery/useFlatMap": "https://docs.rome.tools/lint/rules/useFlatMap",
"lint/nursery/useNumericLiterals": "https://docs.rome.tools/lint/rules/useNumericLiterals",

MODIFY
crates/rome_js_analyze/src/analyzers/nursery.rs
crates/rome_js_analyze/src/analyzers/nursery.rs
@@ -21,7 +21,8 @@ mod no_string_case_mismatch;
mod no_unsafe_finally;
mod no_void_type_return;
mod use_default_switch_clause_last;
+mod use_enum_initializers;
mod use_flat_map;
mod use_numeric_literals;
mod use_valid_for_direction;
-declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_access_key :: NoAccessKey , self :: no_banned_types :: NoBannedTypes , self :: no_conditional_assignment :: NoConditionalAssignment , self :: no_const_enum :: NoConstEnum , self :: no_constructor_return :: NoConstructorReturn , self :: no_distracting_elements :: NoDistractingElements , self :: no_dupe_keys :: NoDupeKeys , self :: no_empty_interface :: NoEmptyInterface , self :: no_explicit_any :: NoExplicitAny , self :: no_extra_non_null_assertion :: NoExtraNonNullAssertion , self :: no_header_scope :: NoHeaderScope , self :: no_invalid_constructor_super :: NoInvalidConstructorSuper , self :: no_non_null_assertion :: NoNonNullAssertion , self :: no_precision_loss :: NoPrecisionLoss , self :: no_redundant_use_strict :: NoRedundantUseStrict , self :: no_setter_return :: NoSetterReturn , self :: no_string_case_mismatch :: NoStringCaseMismatch , self :: no_unsafe_finally :: NoUnsafeFinally , self :: no_void_type_return :: NoVoidTypeReturn , self :: use_default_switch_clause_last :: UseDefaultSwitchClauseLast , self :: use_flat_map :: UseFlatMap , self :: use_numeric_literals :: UseNumericLiterals , self :: use_valid_for_direction :: UseValidForDirection ,] } }
+declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_access_key :: NoAccessKey , self :: no_banned_types :: NoBannedTypes , self :: no_conditional_assignment :: NoConditionalAssignment , self :: no_const_enum :: NoConstEnum , self :: no_constructor_return :: NoConstructorReturn , self :: no_distracting_elements :: NoDistractingElements , self :: no_dupe_keys :: NoDupeKeys , self :: no_empty_interface :: NoEmptyInterface , self :: no_explicit_any :: NoExplicitAny , self :: no_extra_non_null_assertion :: NoExtraNonNullAssertion , self :: no_header_scope :: NoHeaderScope , self :: no_invalid_constructor_super :: NoInvalidConstructorSuper , self :: no_non_null_assertion :: NoNonNullAssertion , self :: no_precision_loss :: NoPrecisionLoss , self :: no_redundant_use_strict :: NoRedundantUseStrict , self :: no_setter_return :: NoSetterReturn , self :: no_string_case_mismatch :: NoStringCaseMismatch , self :: no_unsafe_finally :: NoUnsafeFinally , self :: no_void_type_return :: NoVoidTypeReturn , self :: use_default_switch_clause_last :: UseDefaultSwitchClauseLast , self :: use_enum_initializers :: UseEnumInitializers , self :: use_flat_map :: UseFlatMap , self :: use_numeric_literals :: UseNumericLiterals , self :: use_valid_for_direction :: UseValidForDirection ,] } }

ADD
crates/rome_js_analyze/src/analyzers/nursery/use_enum_initializers.rs
+use crate::JsRuleAction;
+use rome_analyze::context::RuleContext;
+use rome_analyze::{declare_rule, ActionCategory, Ast, Rule, RuleDiagnostic};
+use rome_console::markup;
+use rome_diagnostics::Applicability;
+use rome_js_factory::make;
+use rome_js_syntax::{AnyJsExpression, AnyJsLiteralExpression, JsSyntaxKind, TsEnumMember};
+use rome_rowan::{AstNode, BatchMutationExt, Direction};
+
+declare_rule! {
+ /// Require that each enum member value be explicitly initialized.
+ ///
+ /// TypeScript enums are a practical way to organize semantically related constant values. Members of enums that don't have explicit values are by default given sequentially increasing numbers.
+ ///
+ /// When the value of enum members are important, allowing implicit values for enum members can cause bugs if enum declarations are modified over time.
+ ///
+ /// Source: https://typescript-eslint.io/rules/prefer-enum-initializers
+ ///
+ /// ## Examples
+ ///
+ /// ### Invalid
+ ///
+ /// ```ts,expect_diagnostic
+ /// enum Version {
+ /// V1,
+ /// }
+ /// ```
+ ///
+ /// ```ts,expect_diagnostic
+ /// enum Status {
+ /// Open = 1,
+ /// Close,
+ /// }
+ /// ```
+ ///
+ /// ```ts,expect_diagnostic
+ /// enum Color {
+ /// Red = "Red",
+ /// Green = "Green",
+ /// Blue,
+ /// }
+ /// ```
+ ///
+ /// ### Valid
+ ///
+ /// ```ts
+ /// enum Status {
+ /// Open = 1,
+ /// Close = 2,
+ /// }
+ /// ```
+ ///
+ /// ```ts
+ /// enum Color {
+ /// Red = "Red",
+ /// Green = "Green",
+ /// Blue = "Blue",
+ /// }
+ /// ```
+ ///
+ pub(crate) UseEnumInitializers {
+ version: "11.0.0",
+ name: "useEnumInitializers",
+ recommended: true,
+ }
+}
+
+impl Rule for UseEnumInitializers {
+ type Query = Ast<TsEnumMember>;
+ type State = ();
+ type Signals = Option<Self::State>;
+ type Options = ();
+
+ fn run(ctx: &RuleContext<Self>) -> Self::Signals {
+ let enum_member = ctx.query();
+ enum_member.initializer().is_none().then_some(())
+ }
+
+ fn diagnostic(ctx: &RuleContext<Self>, _: &Self::State) -> Option<RuleDiagnostic> {
+ let enum_member = ctx.query();
+ Some(RuleDiagnostic::new(
+ rule_category!(),
+ enum_member.range(),
+ markup! {
+ "The "<Emphasis>"enum member"</Emphasis>" should be initialized."
+ },
+ ).note(
+ "Allowing implicit values for enum members can cause bugs if enum declarations are modified over time."
+ ))
+ }
+
+ fn action(ctx: &RuleContext<Self>, _: &Self::State) -> Option<JsRuleAction> {
+ let enum_member = ctx.query();
+ let prev_enum_members = enum_member
+ .syntax()
+ .siblings(Direction::Prev)
+ .into_iter()
+ .skip(1) // consume enum_member
+ .filter_map(TsEnumMember::cast);
+ let mut count = 0;
+ let mut prev_enum_member_info = None;
+ for prev_enum_member in prev_enum_members {
+ count += 1;
+ if let Some(initializer) = prev_enum_member.initializer() {
+ prev_enum_member_info = Some((initializer, prev_enum_member.name().ok()?));
+ break;
+ }
+ }
+ let expr = if let Some((prev_initializer, prev_name)) = prev_enum_member_info {
+ let expr = prev_initializer.expression().ok()?;
+ let expr = expr.as_any_js_literal_expression()?;
+ match expr {
+ AnyJsLiteralExpression::JsNumberLiteralExpression(expr) => {
+ Some(AnyJsLiteralExpression::JsNumberLiteralExpression(
+ make::js_number_literal_expression(make::js_number_literal(
+ expr.as_number()? + f64::from(count as i32),
+ )),
+ ))
+ }
+ AnyJsLiteralExpression::JsStringLiteralExpression(expr) => {
+ let prev_enum_delim_val = expr.value_token().ok()?;
+ let prev_enum_delim_val = prev_enum_delim_val.text();
+ let prev_enum_val = &prev_enum_delim_val[1..(prev_enum_delim_val.len() - 1)];
+ if prev_name.text() == prev_enum_val {
+ let enum_name = enum_member.name().ok()?.text();
+ Some(AnyJsLiteralExpression::JsStringLiteralExpression(
+ make::js_string_literal_expression(make::js_string_literal(&enum_name)),
+ ))
+ } else {
+ None
+ }
+ }
+ _ => None,
+ }
+ } else {
+ Some(AnyJsLiteralExpression::JsNumberLiteralExpression(
+ make::js_number_literal_expression(make::js_number_literal(count)),
+ ))
+ };
+ if let Some(expr) = expr {
+ let new_enum_member =
+ enum_member
+ .to_owned()
+ .with_initializer(Some(make::js_initializer_clause(
+ make::token_decorated_with_space(JsSyntaxKind::EQ),
+ AnyJsExpression::AnyJsLiteralExpression(expr),
+ )));
+ let mut mutation = ctx.root().begin();
+ mutation.replace_node_discard_trivia(enum_member.to_owned(), new_enum_member);
+ Some(JsRuleAction {
+ category: ActionCategory::QuickFix,
+ applicability: Applicability::MaybeIncorrect,
+ message: markup! { "" }.to_owned(),
+ mutation,
+ })
+ } else {
+ None
+ }
+ }
+}

ADD
crates/rome_js_analyze/tests/specs/nursery/useEnumInitializers/invalid.ts
+export enum Status {
+ Close,
+ MidClose = 1,
+ MidOpen = 10,
+ /* implicit */ Open /* 11 */,
+}
+
+export enum ComputedFlags {
+ Flag1 = 1,
+ Flag2 = 1 << 1,
+ Flag3,
+}
+
+export enum Direction {
+ Down,
+ Left,
+ Right,
+ Up,
+}
+
+export enum Color {
+ Red = "Red",
+ Green = "Green",
+ Blue,
+}
+
+export enum IndexedColor {
+ Red = "0",
+ Green = "1",
+ Blue,
+}
\ No newline at end of file

ADD
crates/rome_js_analyze/tests/specs/nursery/useEnumInitializers/valid.ts
+export enum Status {
+ Open = 1,
+ Close = 2,
+}
+
+export enum Color {
+ Red = "Red",
+ Green = "Green",
+ Blue = "Blue",
+}
+
+export enum Mix {
+ Str = "Str",
+ Number = 0,
+}
\ No newline at end of file

ADD
crates/rome_js_analyze/tests/specs/nursery/useEnumInitializers/valid.ts.snap
+---
+source: crates/rome_js_analyze/tests/spec_tests.rs
+assertion_line: 74
+expression: valid.ts
+---
+# Input
+```js
+export enum Status {
+ Open = 1,
+ Close = 2,
+}
+
+export enum Color {
+ Red = "Red",
+ Green = "Green",
+ Blue = "Blue",
+}
+
+export enum Mix {
+ Str = "Str",
+ Number = 0,
+}
+```
+
+

MODIFY
crates/rome_service/src/configuration/linter/rules.rs
crates/rome_service/src/configuration/linter/rules.rs
@@ -781,6 +781,8 @@ struct NurserySchema {
use_const: Option<RuleConfiguration>,
#[doc = "Enforce default clauses in switch statements to be last"]
use_default_switch_clause_last: Option<RuleConfiguration>,
+ #[doc = "Require that each enum member value be explicitly initialized."]
+ use_enum_initializers: Option<RuleConfiguration>,
#[doc = "Enforce all dependencies are correctly specified."]
use_exhaustive_dependencies: Option<RuleConfiguration>,
#[doc = "Promotes the use of .flatMap() when map().flat() are used together."]
@@ -792,7 +794,7 @@ struct NurserySchema {
}
impl Nursery {
const CATEGORY_NAME: &'static str = "nursery";
- pub(crate) const CATEGORY_RULES: [&'static str; 30] = [
+ pub(crate) const CATEGORY_RULES: [&'static str; 31] = [
"noAccessKey",
"noBannedTypes",
"noConditionalAssignment",
@@ -819,12 +821,13 @@ impl Nursery {
"useCamelCase",
"useConst",
"useDefaultSwitchClauseLast",
+ "useEnumInitializers",
"useExhaustiveDependencies",
"useFlatMap",
"useNumericLiterals",
"useValidForDirection",
];
- const RECOMMENDED_RULES: [&'static str; 25] = [
+ const RECOMMENDED_RULES: [&'static str; 26] = [
"noAccessKey",
"noBannedTypes",
"noConditionalAssignment",
@@ -846,12 +849,13 @@ impl Nursery {
"useAriaPropsForRole",
"useConst",
"useDefaultSwitchClauseLast",
+ "useEnumInitializers",
"useExhaustiveDependencies",
"useFlatMap",
"useNumericLiterals",
"useValidForDirection",
];
- const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 25] = [
+ const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 26] = [
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[0]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[1]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[2]),
@@ -877,6 +881,7 @@ impl Nursery {
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[27]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[28]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[29]),
+ RuleFilter::Rule("nursery", Self::CATEGORY_RULES[30]),
];
pub(crate) fn is_recommended(&self) -> bool { !matches!(self.recommended, Some(false)) }
pub(crate) fn get_enabled_rules(&self) -> IndexSet<RuleFilter> {
@@ -903,7 +908,7 @@ 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>; 25] {
+ pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 26] {
Self::RECOMMENDED_RULES_AS_FILTERS
}
}

MODIFY
editors/vscode/configuration_schema.json
editors/vscode/configuration_schema.json
}
]
},
+ "useEnumInitializers": {
+ "description": "Require that each enum member value be explicitly initialized.",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/RuleConfiguration"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"useExhaustiveDependencies": {
"description": "Enforce all dependencies are correctly specified.",
"anyOf": [

MODIFY
npm/backend-jsonrpc/src/workspace.ts
npm/backend-jsonrpc/src/workspace.ts
@@ -455,6 +455,10 @@ export interface Nursery {
* Enforce default clauses in switch statements to be last
*/
useDefaultSwitchClauseLast?: RuleConfiguration;
+ /**
+ * Require that each enum member value be explicitly initialized.
+ */
+ useEnumInitializers?: RuleConfiguration;
/**
* Enforce all dependencies are correctly specified.
*/
@@ -698,6 +702,7 @@ export type Category =
| "lint/nursery/useCamelCase"
| "lint/nursery/useConst"
| "lint/nursery/useDefaultSwitchClauseLast"
+ | "lint/nursery/useEnumInitializers"
| "lint/nursery/useExhaustiveDependencies"
| "lint/nursery/useFlatMap"
| "lint/nursery/useNumericLiterals"

MODIFY
npm/js-api/src/wasm.ts
npm/js-api/src/wasm.ts
@@ -11,15 +11,15 @@ export enum Distribution {
/**
* Use this if you want to communicate with the WebAssembly client built for bundlers
*/
- BUNDLER,
+ BUNDLER = 0,
/**
* Use this if you want to communicate with the WebAssembly client built for Node.JS
*/
- NODE,
+ NODE = 1,
/**
* Use this if you want to communicate with the WebAssembly client built for the Web
*/
- WEB,
+ WEB = 2,
}
const isInitialized = {

MODIFY
npm/rome/configuration_schema.json
npm/rome/configuration_schema.json
}
]
},
+ "useEnumInitializers": {
+ "description": "Require that each enum member value be explicitly initialized.",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/RuleConfiguration"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"useExhaustiveDependencies": {
"description": "Enforce all dependencies are correctly specified.",
"anyOf": [

MODIFY
website/src/pages/lint/rules/index.mdx
website/src/pages/lint/rules/index.mdx
@@ -602,6 +602,12 @@ Require <code>const</code> declarations for variables that are never reassigned
Enforce default clauses in switch statements to be last
</section>
<section class="rule">
+<h3 data-toc-exclude id="useEnumInitializers">
+ <a href="/lint/rules/useEnumInitializers">useEnumInitializers</a>
+</h3>
+Require that each enum member value be explicitly initialized.
+</section>
+<section class="rule">
<h3 data-toc-exclude id="useExhaustiveDependencies">
<a href="/lint/rules/useExhaustiveDependencies">useExhaustiveDependencies</a>
</h3>

MODIFY
website/src/playground/types.ts
website/src/playground/types.ts
@@ -34,9 +34,9 @@ export enum TrailingComma {
}
export enum LoadingState {
- Loading,
- Success,
- Error,
+ Loading = 0,
+ Success = 1,
+ Error = 2,
}
export enum Semicolons {

feat(rome_js_analyze): `useEnumInitializers` (#3839)

Co-authored-by: Micha Reiser <micha@rome.tools>