Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/twenty-mice-cough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

Added new nursery rule [`use-consistent-enum-value-type`](https://biomejs.dev/linter/rules/use-consistent-enum-value-type). This rule disallows enums from having both number and string members.
12 changes: 12 additions & 0 deletions crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion crates/biome_js_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub mod no_vue_setup_props_reactivity_loss;
pub mod use_array_sort_compare;
pub mod use_await_thenable;
pub mod use_consistent_arrow_return;
pub mod use_consistent_enum_value_type;
pub mod use_destructuring;
pub mod use_error_cause;
pub mod use_exhaustive_switch_cases;
Expand All @@ -65,4 +66,4 @@ pub mod use_spread;
pub mod use_vue_consistent_define_props_declaration;
pub mod use_vue_define_macros_order;
pub mod use_vue_multi_word_component_names;
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_ambiguous_anchor_text :: NoAmbiguousAnchorText , self :: no_before_interactive_script_outside_document :: NoBeforeInteractiveScriptOutsideDocument , self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_div_regex :: NoDivRegex , self :: no_duplicate_enum_values :: NoDuplicateEnumValues , self :: no_duplicated_spread_props :: NoDuplicatedSpreadProps , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , self :: no_excessive_classes_per_file :: NoExcessiveClassesPerFile , self :: no_excessive_lines_per_file :: NoExcessiveLinesPerFile , self :: no_floating_classes :: NoFloatingClasses , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_jsx_props_bind :: NoJsxPropsBind , self :: no_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , self :: no_multi_assign :: NoMultiAssign , self :: no_multi_str :: NoMultiStr , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_proto :: NoProto , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_return_assign :: NoReturnAssign , self :: no_script_url :: NoScriptUrl , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_undeclared_env_vars :: NoUndeclaredEnvVars , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_options_api :: NoVueOptionsApi , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: no_vue_setup_props_reactivity_loss :: NoVueSetupPropsReactivityLoss , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_await_thenable :: UseAwaitThenable , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_destructuring :: UseDestructuring , self :: use_error_cause :: UseErrorCause , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_inline_script_id :: UseInlineScriptId , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_regexp_exec :: UseRegexpExec , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_consistent_define_props_declaration :: UseVueConsistentDefinePropsDeclaration , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_ambiguous_anchor_text :: NoAmbiguousAnchorText , self :: no_before_interactive_script_outside_document :: NoBeforeInteractiveScriptOutsideDocument , self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_div_regex :: NoDivRegex , self :: no_duplicate_enum_values :: NoDuplicateEnumValues , self :: no_duplicated_spread_props :: NoDuplicatedSpreadProps , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , self :: no_excessive_classes_per_file :: NoExcessiveClassesPerFile , self :: no_excessive_lines_per_file :: NoExcessiveLinesPerFile , self :: no_floating_classes :: NoFloatingClasses , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_jsx_props_bind :: NoJsxPropsBind , self :: no_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , self :: no_multi_assign :: NoMultiAssign , self :: no_multi_str :: NoMultiStr , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_proto :: NoProto , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_return_assign :: NoReturnAssign , self :: no_script_url :: NoScriptUrl , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_undeclared_env_vars :: NoUndeclaredEnvVars , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_options_api :: NoVueOptionsApi , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: no_vue_setup_props_reactivity_loss :: NoVueSetupPropsReactivityLoss , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_await_thenable :: UseAwaitThenable , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_consistent_enum_value_type :: UseConsistentEnumValueType , self :: use_destructuring :: UseDestructuring , self :: use_error_cause :: UseErrorCause , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_inline_script_id :: UseInlineScriptId , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_regexp_exec :: UseRegexpExec , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_consistent_define_props_declaration :: UseVueConsistentDefinePropsDeclaration , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
use biome_analyze::{
Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
};
use biome_console::markup;
use biome_js_syntax::TsEnumDeclaration;
use biome_rowan::{AstNode, TextRange};
use biome_rule_options::use_consistent_enum_value_type::UseConsistentEnumValueTypeOptions;

use crate::services::typed::Typed;

declare_lint_rule! {
/// Disallow enums from having both number and string members.
///
/// TypeScript enums are allowed to assign numeric or string values to their members.
/// Most enums contain either all numbers or all strings, but in theory you can mix-and-match within the same enum.
/// Mixing enum member types is generally considered confusing and a bad practice.
///
/// ## Examples
///
/// ### Invalid
///
/// ```ts,expect_diagnostic
/// enum Status {
/// Unknown,
/// Closed = 1,
/// Open = 'open',
/// }
/// ```
///
/// ### Valid
///
/// ```ts
/// enum Status {
/// Unknown = 0,
/// Closed = 1,
/// Open = 2,
/// }
/// ```
///
/// ```ts
/// enum Status {
/// Unknown,
/// Closed,
/// Open,
/// }
/// ```
///
/// ```ts
/// enum Status {
/// Unknown = 'unknown',
/// Closed = 'closed',
/// Open = 'open',
/// }
/// ```
///
pub UseConsistentEnumValueType {
version: "next",
name: "useConsistentEnumValueType",
language: "ts",
recommended: false,
domains: &[RuleDomain::Project],
sources: &[RuleSource::EslintTypeScript("no-mixed-enums").same()],
}
}

#[derive(Eq, PartialEq, Clone, Debug)]
pub enum EnumValueType {
Number,
String,
Unknown,
}

impl Rule for UseConsistentEnumValueType {
type Query = Typed<TsEnumDeclaration>;
type State = Vec<TextRange>;
type Signals = Option<Self::State>;
type Options = UseConsistentEnumValueTypeOptions;

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();
let mut found = vec![];
let mut enum_type: Option<EnumValueType> = None;

for member in node.members() {
let Some(member) = member.ok() else {
continue;
};

let Some(initializer) = member.initializer() else {
if let Some(enum_type) = enum_type.clone() {
if enum_type != EnumValueType::Number {
found.push(member.range());
}
} else {
enum_type = Some(EnumValueType::Number);
}
continue;
};
let Some(expr) = initializer.expression().ok() else {
continue;
};

let expr_type = ctx.type_of_expression(&expr);

if expr_type.is_string_or_string_literal() {
if let Some(enum_type) = enum_type.clone() {
if enum_type != EnumValueType::String {
found.push(member.range());
}
} else {
enum_type = Some(EnumValueType::String);
}
continue;
}

if expr_type.is_number_or_number_literal() {
if let Some(enum_type) = enum_type.clone() {
if enum_type != EnumValueType::Number {
found.push(member.range());
}
} else {
enum_type = Some(EnumValueType::Number);
}
continue;
}

if let Some(enum_type) = enum_type.clone() {
if enum_type != EnumValueType::Unknown {
found.push(member.range());
}
} else {
enum_type = Some(EnumValueType::Unknown);
}
}
Comment on lines +127 to +134
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential false positives when type inference fails.

When the first member's type can't be determined (Unknown), subsequent members with known types (Number or String) will be incorrectly flagged as inconsistent. For example:

enum Foo {
  A = computedValue(), // Unknown
  B = 1,               // Flagged incorrectly
  C = 2,               // Flagged incorrectly  
}

Consider skipping the type assignment when the inferred type is Unknown, so it doesn't establish a baseline that causes false positives.

🐛 Proposed fix
-            if let Some(enum_type) = enum_type.clone() {
-                if enum_type != EnumValueType::Unknown {
-                    found.push(member.range());
-                }
-            } else {
-                enum_type = Some(EnumValueType::Unknown);
-            }
+            // Don't set enum_type for unknown expressions - we can't determine
+            // if they're consistent without knowing the actual type.
+            if let Some(ref et) = enum_type {
+                if *et != EnumValueType::Unknown {
+                    found.push(member.range());
+                }
+            }
+            // Skip setting enum_type to Unknown - let later members establish the type
🤖 Prompt for AI Agents
In @crates/biome_js_analyze/src/lint/nursery/use_consistent_enum_value_type.rs
around lines 127 - 134, The code currently sets enum_type =
Some(EnumValueType::Unknown) when the first member's inferred type is Unknown,
which makes Unknown become the baseline and causes later known types to be
flagged; change the logic in the loop that handles each member so you only
assign to enum_type when the inferred EnumValueType is not Unknown: for each
member, get its inferred type (EnumValueType), if it's Unknown just
skip/type-ignore that member; if it's Number or String and enum_type is None set
enum_type to that type, otherwise compare to enum_type and push(member.range())
on mismatch. Ensure you remove the branch that sets enum_type to
Some(EnumValueType::Unknown) and use the symbols enum_type, EnumValueType,
member, and found to locate and update the code.


if found.is_empty() { None } else { Some(found) }
}

fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
let mut diagnostic = RuleDiagnostic::new(
rule_category!(),
state.first()?,
markup! {
"Inconsistent enum value type."
},
);

for range in &state[1..] {
diagnostic = diagnostic.detail(
range,
markup! {
"Another inconsistent enum value type."
},
);
}

Some(diagnostic.note(markup! {
"Mixing number and string enums can be confusing. Make sure to use a consistent value type within your enum."
}))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* should generate diagnostics */
enum Invalid1 {
Unknown,
Closed = 1,
Open = 'open',
}

function getInvalidValue() {
return 0
}

enum Invalid2 {
Unknown = getInvalidValue(),
Closed = "closed",
Open = getInvalidValue(),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: invalid.ts
---
# Input
```ts
/* should generate diagnostics */
enum Invalid1 {
Unknown,
Closed = 1,
Open = 'open',
}

function getInvalidValue() {
return 0
}

enum Invalid2 {
Unknown = getInvalidValue(),
Closed = "closed",
Open = getInvalidValue(),
}

```

# Diagnostics
```
invalid.ts:5:2 lint/nursery/useConsistentEnumValueType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Inconsistent enum value type.

3 │ Unknown,
4 │ Closed = 1,
> 5 │ Open = 'open',
│ ^^^^^^^^^^^^^
6 │ }
7 │

i Mixing number and string enums can be confusing. Make sure to use a consistent value type within your enum.

i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.


```

```
invalid.ts:14:2 lint/nursery/useConsistentEnumValueType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Inconsistent enum value type.

12 │ enum Invalid2 {
13 │ Unknown = getInvalidValue(),
> 14 │ Closed = "closed",
│ ^^^^^^^^^^^^^^^^^
15 │ Open = getInvalidValue(),
16 │ }

i Mixing number and string enums can be confusing. Make sure to use a consistent value type within your enum.

i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.


```
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* should not generate diagnostics */

enum Valid1 {
Unknown = 0,
Closed = 1,
Open = 2,
}

enum Valid2 {
Unknown,
Closed,
Open,
}

enum Valid3 {
Unknown = 'unknown',
Closed = 'closed',
Open = 'open',
}

function getValidValue() {
return 0
}

enum Valid4 {
Unknown = getValidValue(),
Closed = 1,
Open = getValidValue(),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: valid.ts
---
# Input
```ts
/* should not generate diagnostics */

enum Valid1 {
Unknown = 0,
Closed = 1,
Open = 2,
}

enum Valid2 {
Unknown,
Closed,
Open,
}

enum Valid3 {
Unknown = 'unknown',
Closed = 'closed',
Open = 'open',
}

function getValidValue() {
return 0
}

enum Valid4 {
Unknown = getValidValue(),
Closed = 1,
Open = getValidValue(),
}

```
Loading
Loading