-
-
Notifications
You must be signed in to change notification settings - Fork 961
feat(js_analyze): implement useConsistentEnumValueType #8714
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. |
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.
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.
| 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); | ||
| } | ||
| } | ||
|
|
||
| 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(), | ||
| } | ||
|
|
||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:Consider skipping the type assignment when the inferred type is
Unknown, so it doesn't establish a baseline that causes false positives.🐛 Proposed fix
🤖 Prompt for AI Agents