Skip to content

Commit ac3a71f

Browse files
authored
feat(js_analyze): implement useConsistentEnumValueType (#8714)
1 parent f6bb010 commit ac3a71f

15 files changed

Lines changed: 378 additions & 1 deletion

File tree

.changeset/twenty-mice-cough.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
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.

crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/src/analyzer/linter/rules.rs

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/src/generated/domain_selector.rs

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_diagnostics_categories/src/categories.rs

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_js_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ pub mod no_vue_setup_props_reactivity_loss;
5050
pub mod use_array_sort_compare;
5151
pub mod use_await_thenable;
5252
pub mod use_consistent_arrow_return;
53+
pub mod use_consistent_enum_value_type;
5354
pub mod use_destructuring;
5455
pub mod use_error_cause;
5556
pub mod use_exhaustive_switch_cases;
@@ -65,4 +66,4 @@ pub mod use_spread;
6566
pub mod use_vue_consistent_define_props_declaration;
6667
pub mod use_vue_define_macros_order;
6768
pub mod use_vue_multi_word_component_names;
68-
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 ,] } }
69+
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 ,] } }
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
use biome_analyze::{
2+
Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
3+
};
4+
use biome_console::markup;
5+
use biome_js_syntax::TsEnumDeclaration;
6+
use biome_rowan::{AstNode, TextRange};
7+
use biome_rule_options::use_consistent_enum_value_type::UseConsistentEnumValueTypeOptions;
8+
9+
use crate::services::typed::Typed;
10+
11+
declare_lint_rule! {
12+
/// Disallow enums from having both number and string members.
13+
///
14+
/// TypeScript enums are allowed to assign numeric or string values to their members.
15+
/// Most enums contain either all numbers or all strings, but in theory you can mix-and-match within the same enum.
16+
/// Mixing enum member types is generally considered confusing and a bad practice.
17+
///
18+
/// ## Examples
19+
///
20+
/// ### Invalid
21+
///
22+
/// ```ts,expect_diagnostic
23+
/// enum Status {
24+
/// Unknown,
25+
/// Closed = 1,
26+
/// Open = 'open',
27+
/// }
28+
/// ```
29+
///
30+
/// ### Valid
31+
///
32+
/// ```ts
33+
/// enum Status {
34+
/// Unknown = 0,
35+
/// Closed = 1,
36+
/// Open = 2,
37+
/// }
38+
/// ```
39+
///
40+
/// ```ts
41+
/// enum Status {
42+
/// Unknown,
43+
/// Closed,
44+
/// Open,
45+
/// }
46+
/// ```
47+
///
48+
/// ```ts
49+
/// enum Status {
50+
/// Unknown = 'unknown',
51+
/// Closed = 'closed',
52+
/// Open = 'open',
53+
/// }
54+
/// ```
55+
///
56+
pub UseConsistentEnumValueType {
57+
version: "next",
58+
name: "useConsistentEnumValueType",
59+
language: "ts",
60+
recommended: false,
61+
domains: &[RuleDomain::Project],
62+
sources: &[RuleSource::EslintTypeScript("no-mixed-enums").same()],
63+
}
64+
}
65+
66+
#[derive(Eq, PartialEq, Clone, Debug)]
67+
pub enum EnumValueType {
68+
Number,
69+
String,
70+
Unknown,
71+
}
72+
73+
impl Rule for UseConsistentEnumValueType {
74+
type Query = Typed<TsEnumDeclaration>;
75+
type State = Vec<TextRange>;
76+
type Signals = Option<Self::State>;
77+
type Options = UseConsistentEnumValueTypeOptions;
78+
79+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
80+
let node = ctx.query();
81+
let mut found = vec![];
82+
let mut enum_type: Option<EnumValueType> = None;
83+
84+
for member in node.members() {
85+
let Some(member) = member.ok() else {
86+
continue;
87+
};
88+
89+
let Some(initializer) = member.initializer() else {
90+
if let Some(enum_type) = enum_type.clone() {
91+
if enum_type != EnumValueType::Number {
92+
found.push(member.range());
93+
}
94+
} else {
95+
enum_type = Some(EnumValueType::Number);
96+
}
97+
continue;
98+
};
99+
let Some(expr) = initializer.expression().ok() else {
100+
continue;
101+
};
102+
103+
let expr_type = ctx.type_of_expression(&expr);
104+
105+
if expr_type.is_string_or_string_literal() {
106+
if let Some(enum_type) = enum_type.clone() {
107+
if enum_type != EnumValueType::String {
108+
found.push(member.range());
109+
}
110+
} else {
111+
enum_type = Some(EnumValueType::String);
112+
}
113+
continue;
114+
}
115+
116+
if expr_type.is_number_or_number_literal() {
117+
if let Some(enum_type) = enum_type.clone() {
118+
if enum_type != EnumValueType::Number {
119+
found.push(member.range());
120+
}
121+
} else {
122+
enum_type = Some(EnumValueType::Number);
123+
}
124+
continue;
125+
}
126+
127+
if let Some(enum_type) = enum_type.clone() {
128+
if enum_type != EnumValueType::Unknown {
129+
found.push(member.range());
130+
}
131+
} else {
132+
enum_type = Some(EnumValueType::Unknown);
133+
}
134+
}
135+
136+
if found.is_empty() { None } else { Some(found) }
137+
}
138+
139+
fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
140+
let mut diagnostic = RuleDiagnostic::new(
141+
rule_category!(),
142+
state.first()?,
143+
markup! {
144+
"Inconsistent enum value type."
145+
},
146+
);
147+
148+
for range in &state[1..] {
149+
diagnostic = diagnostic.detail(
150+
range,
151+
markup! {
152+
"Another inconsistent enum value type."
153+
},
154+
);
155+
}
156+
157+
Some(diagnostic.note(markup! {
158+
"Mixing number and string enums can be confusing. Make sure to use a consistent value type within your enum."
159+
}))
160+
}
161+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/* should generate diagnostics */
2+
enum Invalid1 {
3+
Unknown,
4+
Closed = 1,
5+
Open = 'open',
6+
}
7+
8+
function getInvalidValue() {
9+
return 0
10+
}
11+
12+
enum Invalid2 {
13+
Unknown = getInvalidValue(),
14+
Closed = "closed",
15+
Open = getInvalidValue(),
16+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: invalid.ts
4+
---
5+
# Input
6+
```ts
7+
/* should generate diagnostics */
8+
enum Invalid1 {
9+
Unknown,
10+
Closed = 1,
11+
Open = 'open',
12+
}
13+
14+
function getInvalidValue() {
15+
return 0
16+
}
17+
18+
enum Invalid2 {
19+
Unknown = getInvalidValue(),
20+
Closed = "closed",
21+
Open = getInvalidValue(),
22+
}
23+
24+
```
25+
26+
# Diagnostics
27+
```
28+
invalid.ts:5:2 lint/nursery/useConsistentEnumValueType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
29+
30+
i Inconsistent enum value type.
31+
32+
3 │ Unknown,
33+
4 │ Closed = 1,
34+
> 5 │ Open = 'open',
35+
│ ^^^^^^^^^^^^^
36+
6 │ }
37+
7 │
38+
39+
i Mixing number and string enums can be confusing. Make sure to use a consistent value type within your enum.
40+
41+
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.
42+
43+
44+
```
45+
46+
```
47+
invalid.ts:14:2 lint/nursery/useConsistentEnumValueType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
48+
49+
i Inconsistent enum value type.
50+
51+
12 │ enum Invalid2 {
52+
13Unknown = getInvalidValue(),
53+
> 14Closed = "closed",
54+
^^^^^^^^^^^^^^^^^
55+
15Open = getInvalidValue(),
56+
16}
57+
58+
i Mixing number and string enums can be confusing. Make sure to use a consistent value type within your enum.
59+
60+
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.
61+
62+
63+
```
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/* should not generate diagnostics */
2+
3+
enum Valid1 {
4+
Unknown = 0,
5+
Closed = 1,
6+
Open = 2,
7+
}
8+
9+
enum Valid2 {
10+
Unknown,
11+
Closed,
12+
Open,
13+
}
14+
15+
enum Valid3 {
16+
Unknown = 'unknown',
17+
Closed = 'closed',
18+
Open = 'open',
19+
}
20+
21+
function getValidValue() {
22+
return 0
23+
}
24+
25+
enum Valid4 {
26+
Unknown = getValidValue(),
27+
Closed = 1,
28+
Open = getValidValue(),
29+
}

0 commit comments

Comments
 (0)