+
Skip to content
Open
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
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@
},
"nursery": {
"noFloatingPromises": "error",
"noImportCycles": "error"
"noImportCycles": "error",
"useSortedInterfaceMembers": "error"
}
}
},
Expand Down
16 changes: 16 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.

27 changes: 24 additions & 3 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ define_categories! {
"lint/nursery/useQwikValidLexicalScope": "https://biomejs.dev/linter/rules/use-qwik-valid-lexical-scope",
"lint/nursery/useReactFunctionComponents": "https://biomejs.dev/linter/rules/use-react-function-components",
"lint/nursery/useSortedClasses": "https://biomejs.dev/linter/rules/use-sorted-classes",
"lint/nursery/useSortedInterfaceMembers": "https://biomejs.dev/linter/rules/use-sorted-interface-members",
"lint/nursery/useVueMultiWordComponentNames": "https://biomejs.dev/linter/rules/use-vue-multi-word-component-names",
"lint/performance/noAccumulatingSpread": "https://biomejs.dev/linter/rules/no-accumulating-spread",
"lint/performance/noAwaitInLoops": "https://biomejs.dev/linter/rules/no-await-in-loops",
Expand Down Expand Up @@ -391,6 +392,7 @@ define_categories! {
"lint/suspicious/useStrictMode": "https://biomejs.dev/linter/rules/use-strict-mode",
// end lint rules
// start assist actions
"assist/source/useSortedInterfaceMembers": "https://biomejs.dev/assist/actions/use-sorted-interface-members",
"assist/source/useSortedKeys": "https://biomejs.dev/assist/actions/use-sorted-keys",
"assist/source/useSortedProperties": "https://biomejs.dev/assist/actions/use-sorted-properties",
"assist/source/useSortedAttributes": "https://biomejs.dev/assist/actions/use-sorted-attributes",
Expand Down
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 @@ -35,5 +35,6 @@ pub mod use_qwik_method_usage;
pub mod use_qwik_valid_lexical_scope;
pub mod use_react_function_components;
pub mod use_sorted_classes;
pub mod use_sorted_interface_members;
pub mod use_vue_multi_word_component_names;
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_floating_promises :: NoFloatingPromises , self :: no_import_cycles :: NoImportCycles , self :: no_jsx_literals :: NoJsxLiterals , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChain , self :: no_qwik_use_visible_task :: NoQwikUseVisibleTask , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , 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_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_anchor_href :: UseAnchorHref , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_consistent_type_definitions :: UseConsistentTypeDefinitions , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_image_size :: UseImageSize , self :: use_max_params :: UseMaxParams , self :: use_qwik_classlist :: UseQwikClasslist , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_react_function_components :: UseReactFunctionComponents , self :: use_sorted_classes :: UseSortedClasses , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_floating_promises :: NoFloatingPromises , self :: no_import_cycles :: NoImportCycles , self :: no_jsx_literals :: NoJsxLiterals , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChain , self :: no_qwik_use_visible_task :: NoQwikUseVisibleTask , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , 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_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_anchor_href :: UseAnchorHref , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_consistent_type_definitions :: UseConsistentTypeDefinitions , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_image_size :: UseImageSize , self :: use_max_params :: UseMaxParams , self :: use_qwik_classlist :: UseQwikClasslist , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_react_function_components :: UseReactFunctionComponents , self :: use_sorted_classes :: UseSortedClasses , self :: use_sorted_interface_members :: UseSortedInterfaceMembers , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
use biome_analyze::{
Ast, FixKind, Rule, RuleAction, RuleDiagnostic, RuleSource, context::RuleContext,
declare_lint_rule,
};

use biome_console::markup;
use biome_deserialize::TextRange;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix incorrect TextRange import (compile error)

TextRange comes from biome_rowan, not biome_deserialize.

Apply this diff:

-use biome_deserialize::TextRange;
+use biome_rowan::TextRange;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
use biome_deserialize::TextRange;
use biome_rowan::TextRange;
🤖 Prompt for AI Agents
In crates/biome_js_analyze/src/assist/source/use_sorted_interface_members.rs
around line 7, the code imports TextRange from the wrong crate
(biome_deserialize) causing a compile error; replace that import with TextRange
from biome_rowan by changing the use statement to import TextRange from
biome_rowan (i.e., remove or replace the existing biome_deserialize::TextRange
import so the file uses biome_rowan::TextRange).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

All checks have passed without this suggestion.

use biome_js_factory::make;
use biome_js_syntax::{
AnyJsObjectMemberName, AnyTsTypeMember, TsInterfaceDeclaration, TsTypeMemberList,
};

use crate::JsRuleAction;
use biome_rowan::{AstNode, AstNodeList, BatchMutationExt};
use biome_string_case::comparable_token::ComparableToken;
declare_lint_rule! {
/// Sort interface members by key.
///
/// Interface members are sorted according to their names. The rule distinguishes between
/// two types of members:
///
/// **Sortable members** - Members with explicit, fixed names that can be alphabetically sorted:
/// - Property signatures: `property: type`
/// - Method signatures: `method(): type`
/// - Getter signatures: `get property(): type`
/// - Setter signatures: `set property(value: type): void`
///
/// **Non-sortable members** - Members without fixed names or with dynamic/computed names:
/// - Call signatures: `(): type` (represents the interface as a callable function)
/// - Construct signatures: `new (): type` (represents the interface as a constructor)
/// - Index signatures: `[key: string]: type` (represents dynamic property access)
///
/// The rule sorts all sortable members alphabetically and places them first,
/// followed by non-sortable members in their original order. Non-sortable members
/// cannot be meaningfully sorted by name since they represent different interface
/// contracts rather than named properties or methods.
///
/// # Examples
///
/// ## Invalid
///
/// ```ts,expect_diagnostic
/// interface MixedMembers {
/// z: string;
/// a: number;
/// (): void; // Call signature
/// y: boolean;
/// new (): MixedMembers; // Construct signature
/// b: string;
/// [key: string]: any; // Index signature
/// }
/// ```
///
/// ## Valid
///
/// ```ts
/// interface MixedMembers {
/// a: number;
/// b: string;
/// y: boolean;
/// z: string;
/// (): void; // Non-sortable members remain in original order
/// new (): MixedMembers;
/// [key: string]: any;
/// }
/// ```
///
pub UseSortedInterfaceMembers {
version: "next",
name: "useSortedInterfaceMembers",
language: "ts",
recommended: false,
sources: &[RuleSource::EslintPerfectionist("sort-interfaces").inspired()],
fix_kind: FixKind::Safe,
}
}
impl Rule for UseSortedInterfaceMembers {
type Query = Ast<TsInterfaceDeclaration>;
type State = ();
type Signals = Option<Self::State>;
type Options = ();
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let interface = ctx.query();
let body = interface.members();
let comparator = ComparableToken::ascii_nat_cmp;

if is_interface_members_sorted(&body, comparator) {
None
} else {
Some(())
}
}
fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
let interface = ctx.query();
let body = interface.members();

Some(RuleDiagnostic::new(
rule_category!(),
body.range(),
markup! {
"The interface members are not sorted by key."
},
))
}
fn action(ctx: &RuleContext<Self>, (): &Self::State) -> Option<JsRuleAction> {
let interface = ctx.query();
let list = interface.members();
let mut mutation = ctx.root().begin();
let comparator = ComparableToken::ascii_nat_cmp;
let new_list = sort_interface_members(&list, comparator);
mutation.replace_node(list, new_list);

Some(RuleAction::new(
ctx.metadata().action_category(ctx.category(), ctx.group()),
ctx.metadata().applicability(),
markup! { "Sort the interface members by key." },
mutation,
))
}
fn text_range(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<TextRange> {
Some(ctx.query().range())
}
}
fn get_type_member_name(member: &AnyTsTypeMember) -> Option<AnyJsObjectMemberName> {
match member {
// Property signatures have names
AnyTsTypeMember::TsPropertySignatureTypeMember(prop) => prop.name().ok(),
AnyTsTypeMember::TsMethodSignatureTypeMember(method) => method.name().ok(),
AnyTsTypeMember::TsGetterSignatureTypeMember(getter) => getter.name().ok(),
AnyTsTypeMember::TsSetterSignatureTypeMember(setter) => setter.name().ok(),
// Call signatures, construct signatures, and index signatures don't have sortable names
_ => None,
}
}
fn is_interface_members_sorted(
list: &TsTypeMemberList,
comparator: impl Fn(&ComparableToken, &ComparableToken) -> std::cmp::Ordering,
) -> bool {
use std::cmp::Ordering;
let mut prev_key: Option<ComparableToken> = None;
let mut saw_non_sortable = false;

for member in list.iter() {
if let Some(name) = get_type_member_name(&member)
&& let Some(token_text) = name.name()
{
if saw_non_sortable {
// sortable member found after a non-sortable
return false;
}

let current = ComparableToken::new(token_text);

if let Some(prev) = &prev_key
&& comparator(prev, &current) == Ordering::Greater
{
return false;
}

prev_key = Some(current);

continue;
}

// Non-sortable member
saw_non_sortable = true;
}
true
}
fn sort_interface_members(
list: &TsTypeMemberList,
comparator: impl Fn(&ComparableToken, &ComparableToken) -> std::cmp::Ordering,
) -> TsTypeMemberList {
let mut sortable_members = Vec::new();
let mut non_sortable_members = Vec::new();

// Separate sortable from non-sortable members
for member in list.iter() {
if let Some(name) = get_type_member_name(&member) {
if let Some(token_text) = name.name() {
sortable_members.push((member, ComparableToken::new(token_text)));
} else {
// Name exists but is not sortable (computed/dynamic)
non_sortable_members.push(member);
}
} else {
// No name (call signatures, construct signatures, index signatures)
non_sortable_members.push(member);
}
}

// Sort the sortable members
sortable_members.sort_by(|(_, a), (_, b)| comparator(a, b));

// Combine: all sortable members first, then all non-sortable members
let mut new_members: Vec<AnyTsTypeMember> = sortable_members
.into_iter()
.map(|(member, _)| member)
.collect();

new_members.extend(non_sortable_members);

make::ts_type_member_list(new_members)
}
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/options.rs

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,11 @@
interface MultipleNonSortableInterface {
zProperty: string;
aProperty: number;
new (): any; // Construct signature
(): void; // Call signature
yMethod(): boolean;
[index: number]: string; // Index signature with number
bMethod(): string;
[key: string]: any; // Index signature with string
cField: object;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: multiple_non_sortable.ts
---
# Input
```ts
interface MultipleNonSortableInterface {
zProperty: string;
aProperty: number;
new (): any; // Construct signature
(): void; // Call signature
yMethod(): boolean;
[index: number]: string; // Index signature with number
bMethod(): string;
[key: string]: any; // Index signature with string
cField: object;
}

```

# Diagnostics
```
multiple_non_sortable.ts:2:2 lint/nursery/useSortedInterfaceMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━

i The interface members are not sorted by key.

1 │ interface MultipleNonSortableInterface {
> 2 │ zProperty: string;
│ ^^^^^^^^^^^^^^^^^^
> 3 │ aProperty: number;
> 4 │ new (): any; // Construct signature
...
> 9 │ [key: string]: any; // Index signature with string
> 10 │ cField: object;
│ ^^^^^^^^^^^^^^^
11 │ }
12 │

i Safe fix: Sort the interface members by key.

1 1 │ interface MultipleNonSortableInterface {
2 │ - → zProperty:·string;
3 │ - → aProperty:·number;
4 │ - → new·():·any;··//·Construct·signature
2 │ + → aProperty:·number;
3 │ + → bMethod():·string;
4 │ + → cField:·object;
5 │ + → yMethod():·boolean;
6 │ + → zProperty:·string;
7 │ + → new·():·any;··//·Construct·signature
5 8 │ (): void; // Call signature
6 │ - → yMethod():·boolean;
7 │ - → [index:·number]:·string;··//·Index·signature·with·number
8 │ - → bMethod():·string;
9 │ - → [key:·string]:·any;··//·Index·signature·with·string
10 │ - → cField:·object;
9 │ + → [index:·number]:·string;··//·Index·signature·with·number
10 │ + → [key:·string]:·any;
11 11 │ }
12 12 │


```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
interface BadlyOrderedInterface {
aProperty: string;
[index: number]: any; // Non-sortable index signature
bProperty: number; // This should trigger an error - sortable after non-sortable
(): void; // Call signature
cProperty: boolean; // This should also trigger an error
}
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载