From c6d970274a0455ae7b3f2a5e43dbd252c1128966 Mon Sep 17 00:00:00 2001 From: Liam Date: Fri, 27 Feb 2026 23:58:10 -0500 Subject: [PATCH 1/5] feat(lint): add nursery rule `useImportsFirst` --- .../src/analyzer/linter/rules.rs | 4 + .../src/categories.rs | 7 +- .../src/lint/nursery/use_imports_first.rs | 91 +++++++++++++++++++ .../specs/nursery/useImportsFirst/invalid.js | 5 + .../nursery/useImportsFirst/invalid.js.snap | 51 +++++++++++ .../nursery/useImportsFirst/invalid_mixed.js | 5 + .../useImportsFirst/invalid_mixed.js.snap | 51 +++++++++++ .../specs/nursery/useImportsFirst/valid.js | 5 + .../nursery/useImportsFirst/valid.js.snap | 13 +++ .../useImportsFirst/valid_with_export.js | 4 + .../useImportsFirst/valid_with_export.js.snap | 12 +++ crates/biome_rule_options/src/lib.rs | 1 + .../src/use_imports_first.rs | 6 ++ .../@biomejs/biome/configuration_schema.json | 26 ++++++ 14 files changed, 278 insertions(+), 3 deletions(-) create mode 100644 crates/biome_js_analyze/src/lint/nursery/use_imports_first.rs create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid.js create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid.js.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid_mixed.js create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid_mixed.js.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/valid.js create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/valid.js.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/valid_with_export.js create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/valid_with_export.js.snap create mode 100644 crates/biome_rule_options/src/use_imports_first.rs diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs index 08c0a471ca95..bdf4c37827ff 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -454,6 +454,7 @@ pub enum RuleName { UseImageSize, UseImportExtensions, UseImportType, + UseImportsFirst, UseIndexOf, UseInlineScriptId, UseInputName, @@ -917,6 +918,7 @@ impl RuleName { Self::UseImageSize => "useImageSize", Self::UseImportExtensions => "useImportExtensions", Self::UseImportType => "useImportType", + Self::UseImportsFirst => "useImportsFirst", Self::UseIndexOf => "useIndexOf", Self::UseInlineScriptId => "useInlineScriptId", Self::UseInputName => "useInputName", @@ -1376,6 +1378,7 @@ impl RuleName { Self::UseImageSize => RuleGroup::Correctness, Self::UseImportExtensions => RuleGroup::Correctness, Self::UseImportType => RuleGroup::Style, + Self::UseImportsFirst => RuleGroup::Nursery, Self::UseIndexOf => RuleGroup::Complexity, Self::UseInlineScriptId => RuleGroup::Nursery, Self::UseInputName => RuleGroup::Nursery, @@ -1844,6 +1847,7 @@ impl std::str::FromStr for RuleName { "useImageSize" => Ok(Self::UseImageSize), "useImportExtensions" => Ok(Self::UseImportExtensions), "useImportType" => Ok(Self::UseImportType), + "useImportsFirst" => Ok(Self::UseImportsFirst), "useIndexOf" => Ok(Self::UseIndexOf), "useInlineScriptId" => Ok(Self::UseInlineScriptId), "useInputName" => Ok(Self::UseInputName), diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 39121bc4d1b0..78d38a1109a9 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -173,10 +173,10 @@ define_categories! { "lint/correctness/useValidForDirection": "https://biomejs.dev/linter/rules/use-valid-for-direction", "lint/correctness/useValidTypeof": "https://biomejs.dev/linter/rules/use-valid-typeof", "lint/correctness/useYield": "https://biomejs.dev/linter/rules/use-yield", - "lint/nursery/useExpect": "https://biomejs.dev/linter/rules/use-expect", "lint/nursery/noAmbiguousAnchorText": "https://biomejs.dev/linter/rules/no-ambiguous-anchor-text", "lint/nursery/noBeforeInteractiveScriptOutsideDocument": "https://biomejs.dev/linter/rules/no-before-interactive-script-outside-document", "lint/nursery/noColorInvalidHex": "https://biomejs.dev/linter/rules/no-color-invalid-hex", + "lint/nursery/noConditionalExpect": "https://biomejs.dev/linter/rules/no-conditional-expect", "lint/nursery/noContinue": "https://biomejs.dev/linter/rules/no-continue", "lint/nursery/noDeprecatedMediaType": "https://biomejs.dev/linter/rules/no-deprecated-media-type", "lint/nursery/noDivRegex": "https://biomejs.dev/linter/rules/no-div-regex", @@ -206,7 +206,6 @@ define_categories! { "lint/nursery/noMultiStr": "https://biomejs.dev/linter/rules/no-multi-str", "lint/nursery/noNestedPromises": "https://biomejs.dev/linter/rules/no-nested-promises", "lint/nursery/noParametersOnlyUsedInRecursion": "https://biomejs.dev/linter/rules/no-parameters-only-used-in-recursion", - "lint/nursery/noConditionalExpect": "https://biomejs.dev/linter/rules/no-conditional-expect", "lint/nursery/noPlaywrightElementHandle": "https://biomejs.dev/linter/rules/no-playwright-element-handle", "lint/nursery/noPlaywrightEval": "https://biomejs.dev/linter/rules/no-playwright-eval", "lint/nursery/noPlaywrightForceOption": "https://biomejs.dev/linter/rules/no-playwright-force-option", @@ -246,19 +245,21 @@ define_categories! { "lint/nursery/useDestructuring": "https://biomejs.dev/linter/rules/use-destructuring", "lint/nursery/useErrorCause": "https://biomejs.dev/linter/rules/use-error-cause", "lint/nursery/useExhaustiveSwitchCases": "https://biomejs.dev/linter/rules/use-exhaustive-switch-cases", + "lint/nursery/useExpect": "https://biomejs.dev/linter/rules/use-expect", "lint/nursery/useExplicitFunctionReturnType": "https://biomejs.dev/linter/rules/use-explicit-type", "lint/nursery/useExplicitType": "https://biomejs.dev/linter/rules/use-explicit-type", "lint/nursery/useFind": "https://biomejs.dev/linter/rules/use-find", "lint/nursery/useGlobalThis": "https://biomejs.dev/linter/rules/use-global-this", "lint/nursery/useImportRestrictions": "https://biomejs.dev/linter/rules/use-import-restrictions", + "lint/nursery/useImportsFirst": "https://biomejs.dev/linter/rules/use-imports-first", "lint/nursery/useInlineScriptId": "https://biomejs.dev/linter/rules/use-inline-script-id", "lint/nursery/useInputName": "https://biomejs.dev/linter/rules/use-input-name", "lint/nursery/useJsxCurlyBraceConvention": "https://biomejs.dev/linter/rules/use-jsx-curly-brace-convention", "lint/nursery/useLoneAnonymousOperation": "https://biomejs.dev/linter/rules/use-lone-anonymous-operation", "lint/nursery/useLoneExecutableDefinition": "https://biomejs.dev/linter/rules/use-lone-executable-definition", - "lint/nursery/useNullishCoalescing": "https://biomejs.dev/linter/rules/use-nullish-coalescing", "lint/nursery/useMaxParams": "https://biomejs.dev/linter/rules/use-max-params", "lint/nursery/useNamedCaptureGroup": "https://biomejs.dev/linter/rules/use-named-capture-group", + "lint/nursery/useNullishCoalescing": "https://biomejs.dev/linter/rules/use-nullish-coalescing", "lint/nursery/usePlaywrightValidDescribeCallback": "https://biomejs.dev/linter/rules/use-playwright-valid-describe-callback", "lint/nursery/useQwikMethodUsage": "https://biomejs.dev/linter/rules/use-qwik-method-usage", "lint/nursery/useQwikValidLexicalScope": "https://biomejs.dev/linter/rules/use-qwik-valid-lexical-scope", diff --git a/crates/biome_js_analyze/src/lint/nursery/use_imports_first.rs b/crates/biome_js_analyze/src/lint/nursery/use_imports_first.rs new file mode 100644 index 000000000000..2764fcb53302 --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/use_imports_first.rs @@ -0,0 +1,91 @@ +use biome_analyze::{ + Ast, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule, +}; +use biome_console::markup; +use biome_js_syntax::{AnyJsModuleItem, JsModuleItemList}; +use biome_rowan::{AstNode, AstNodeList, TextRange}; + +declare_lint_rule! { + /// Enforce that all imports appear at the top of the module. + /// + /// Import statements that appear after non-import statements are harder to + /// find and may indicate disorganized code. Keeping all imports together at + /// the top makes dependencies immediately visible. + /// + /// Note that directives such as `"use strict"` are always allowed before + /// imports, since they are parsed separately from module items. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```js,expect_diagnostic + /// import { foo } from "foo"; + /// const bar = 1; + /// import { baz } from "baz"; + /// ``` + /// + /// ### Valid + /// + /// ```js + /// import { foo } from "foo"; + /// import { bar } from "bar"; + /// const baz = 1; + /// ``` + /// + /// ```js + /// "use strict"; + /// import { foo } from "foo"; + /// ``` + /// + pub UseImportsFirst { + version: "next", + name: "useImportsFirst", + language: "js", + recommended: false, + sources: &[RuleSource::EslintImport("first").same()], + } +} + +impl Rule for UseImportsFirst { + type Query = Ast; + type State = TextRange; + type Signals = Vec; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let items = ctx.query(); + let mut seen_non_import = false; + let mut signals = Vec::new(); + + for item in items.iter() { + match item { + AnyJsModuleItem::JsImport(_) => { + if seen_non_import { + signals.push(item.range()); + } + } + _ => { + seen_non_import = true; + } + } + } + + signals + } + + fn diagnostic(_ctx: &RuleContext, range: &Self::State) -> Option { + Some( + RuleDiagnostic::new( + rule_category!(), + range, + markup! { + "This import should appear at the top of the module." + }, + ) + .note(markup! { + "Move all import statements before any other statements." + }), + ) + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid.js new file mode 100644 index 000000000000..09885471233d --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid.js @@ -0,0 +1,5 @@ +import { foo } from "foo"; +const bar = 1; +import { baz } from "baz"; + +import { qux } from "qux"; diff --git a/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid.js.snap new file mode 100644 index 000000000000..d5ce77857da6 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid.js.snap @@ -0,0 +1,51 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.js +--- +# Input +```js +import { foo } from "foo"; +const bar = 1; +import { baz } from "baz"; + +import { qux } from "qux"; + +``` + +# Diagnostics +``` +invalid.js:3:1 lint/nursery/useImportsFirst ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This import should appear at the top of the module. + + 1 │ import { foo } from "foo"; + 2 │ const bar = 1; + > 3 │ import { baz } from "baz"; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^ + 4 │ + 5 │ import { qux } from "qux"; + + i Move all import statements before any other statements. + + 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.js:5:1 lint/nursery/useImportsFirst ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This import should appear at the top of the module. + + 3 │ import { baz } from "baz"; + 4 │ + > 5 │ import { qux } from "qux"; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^ + 6 │ + + i Move all import statements before any other statements. + + 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. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid_mixed.js b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid_mixed.js new file mode 100644 index 000000000000..9c56a26f1417 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid_mixed.js @@ -0,0 +1,5 @@ +import foo from "foo"; +foo.init(); +import bar from "bar"; +export { bar }; +import baz from "baz"; diff --git a/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid_mixed.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid_mixed.js.snap new file mode 100644 index 000000000000..f04b48d00ecc --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid_mixed.js.snap @@ -0,0 +1,51 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid_mixed.js +--- +# Input +```js +import foo from "foo"; +foo.init(); +import bar from "bar"; +export { bar }; +import baz from "baz"; + +``` + +# Diagnostics +``` +invalid_mixed.js:3:1 lint/nursery/useImportsFirst ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This import should appear at the top of the module. + + 1 │ import foo from "foo"; + 2 │ foo.init(); + > 3 │ import bar from "bar"; + │ ^^^^^^^^^^^^^^^^^^^^^^ + 4 │ export { bar }; + 5 │ import baz from "baz"; + + i Move all import statements before any other statements. + + 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_mixed.js:5:1 lint/nursery/useImportsFirst ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This import should appear at the top of the module. + + 3 │ import bar from "bar"; + 4 │ export { bar }; + > 5 │ import baz from "baz"; + │ ^^^^^^^^^^^^^^^^^^^^^^ + 6 │ + + i Move all import statements before any other statements. + + 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. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/valid.js b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/valid.js new file mode 100644 index 000000000000..ec8dbac0b3a3 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/valid.js @@ -0,0 +1,5 @@ +/* should not generate diagnostics */ +import { foo } from "foo"; +import { bar } from "bar"; +import { baz } from "baz"; +const qux = 1; diff --git a/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/valid.js.snap new file mode 100644 index 000000000000..5e877498a296 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/valid.js.snap @@ -0,0 +1,13 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.js +--- +# Input +```js +/* should not generate diagnostics */ +import { foo } from "foo"; +import { bar } from "bar"; +import { baz } from "baz"; +const qux = 1; + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/valid_with_export.js b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/valid_with_export.js new file mode 100644 index 000000000000..56a6fbadd080 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/valid_with_export.js @@ -0,0 +1,4 @@ +/* should not generate diagnostics */ +import { foo } from "foo"; +export { foo }; +const bar = 1; diff --git a/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/valid_with_export.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/valid_with_export.js.snap new file mode 100644 index 000000000000..8667f2c7eaf9 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/valid_with_export.js.snap @@ -0,0 +1,12 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid_with_export.js +--- +# Input +```js +/* should not generate diagnostics */ +import { foo } from "foo"; +export { foo }; +const bar = 1; + +``` diff --git a/crates/biome_rule_options/src/lib.rs b/crates/biome_rule_options/src/lib.rs index bbc0e332b626..53630f3cc38e 100644 --- a/crates/biome_rule_options/src/lib.rs +++ b/crates/biome_rule_options/src/lib.rs @@ -369,6 +369,7 @@ pub mod use_iframe_title; pub mod use_image_size; pub mod use_import_extensions; pub mod use_import_type; +pub mod use_imports_first; pub mod use_index_of; pub mod use_inline_script_id; pub mod use_input_name; diff --git a/crates/biome_rule_options/src/use_imports_first.rs b/crates/biome_rule_options/src/use_imports_first.rs new file mode 100644 index 000000000000..86ba5827c2fd --- /dev/null +++ b/crates/biome_rule_options/src/use_imports_first.rs @@ -0,0 +1,6 @@ +use biome_deserialize_macros::{Deserializable, Merge}; +use serde::{Deserialize, Serialize}; +#[derive(Default, Clone, Debug, Deserialize, Deserializable, Merge, Eq, PartialEq, Serialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields, default)] +pub struct UseImportsFirstOptions {} diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index bf9cffcaf875..4dc950b27086 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -6232,6 +6232,13 @@ { "type": "null" } ] }, + "useImportsFirst": { + "description": "Succinct description of the rule.\nSee https://biomejs.dev/linter/rules/use-imports-first", + "anyOf": [ + { "$ref": "#/$defs/UseImportsFirstConfiguration" }, + { "type": "null" } + ] + }, "useInlineScriptId": { "description": "Enforce id attribute on next/script components with inline content or dangerouslySetInnerHTML.\nSee https://biomejs.dev/linter/rules/use-inline-script-id", "anyOf": [ @@ -10428,6 +10435,15 @@ "additionalProperties": false, "required": ["level"] }, + "RuleWithUseImportsFirstOptions": { + "type": "object", + "properties": { + "level": { "$ref": "#/$defs/RulePlainConfiguration" }, + "options": { "$ref": "#/$defs/UseImportsFirstOptions" } + }, + "additionalProperties": false, + "required": ["level"] + }, "RuleWithUseIndexOfOptions": { "type": "object", "properties": { @@ -13644,6 +13660,16 @@ "type": "string", "enum": ["auto", "inlineType", "separatedType"] }, + "UseImportsFirstConfiguration": { + "oneOf": [ + { "$ref": "#/$defs/RulePlainConfiguration" }, + { "$ref": "#/$defs/RuleWithUseImportsFirstOptions" } + ] + }, + "UseImportsFirstOptions": { + "type": "object", + "additionalProperties": false + }, "UseIndexOfConfiguration": { "oneOf": [ { "$ref": "#/$defs/RulePlainConfiguration" }, From e5f52451f120642b71c10381133b57dca8d5777d Mon Sep 17 00:00:00 2001 From: Liam Date: Sat, 28 Feb 2026 12:09:10 -0500 Subject: [PATCH 2/5] Add changeset --- .changeset/add-use-imports-first.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/add-use-imports-first.md diff --git a/.changeset/add-use-imports-first.md b/.changeset/add-use-imports-first.md new file mode 100644 index 000000000000..0629358cda34 --- /dev/null +++ b/.changeset/add-use-imports-first.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +Added the nursery rule [`useImportsFirst`](https://biomejs.dev/linter/rules/use-imports-first/) that enforces all import statements appear before any non-import statements in a module. Inspired by the ESLint [`import/first`](https://github.com/import-js/eslint-plugin-import/blob/HEAD/docs/rules/first.md) rule. From bb4718f7c038cc8566412934d4a8c61d57be0a93 Mon Sep 17 00:00:00 2001 From: Liam Date: Sat, 28 Feb 2026 12:21:27 -0500 Subject: [PATCH 3/5] fix(lint): add "why" note to useImportsFirst diagnostic --- crates/biome_js_analyze/src/lint/nursery/use_imports_first.rs | 3 +++ .../tests/specs/nursery/useImportsFirst/invalid.js.snap | 4 ++++ .../tests/specs/nursery/useImportsFirst/invalid_mixed.js.snap | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/crates/biome_js_analyze/src/lint/nursery/use_imports_first.rs b/crates/biome_js_analyze/src/lint/nursery/use_imports_first.rs index 2764fcb53302..357d807e6f6f 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_imports_first.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_imports_first.rs @@ -83,6 +83,9 @@ impl Rule for UseImportsFirst { "This import should appear at the top of the module." }, ) + .note(markup! { + "Scattering imports makes it harder to see the module's dependencies at a glance." + }) .note(markup! { "Move all import statements before any other statements." }), diff --git a/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid.js.snap index d5ce77857da6..5e4d37153ea8 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid.js.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid.js.snap @@ -25,6 +25,8 @@ invalid.js:3:1 lint/nursery/useImportsFirst ━━━━━━━━━━━━ 4 │ 5 │ import { qux } from "qux"; + i Scattering imports makes it harder to see the module's dependencies at a glance. + i Move all import statements before any other statements. 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. @@ -43,6 +45,8 @@ invalid.js:5:1 lint/nursery/useImportsFirst ━━━━━━━━━━━━ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^ 6 │ + i Scattering imports makes it harder to see the module's dependencies at a glance. + i Move all import statements before any other statements. 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. diff --git a/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid_mixed.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid_mixed.js.snap index f04b48d00ecc..0171a58d25ab 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid_mixed.js.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid_mixed.js.snap @@ -25,6 +25,8 @@ invalid_mixed.js:3:1 lint/nursery/useImportsFirst ━━━━━━━━━━ 4 │ export { bar }; 5 │ import baz from "baz"; + i Scattering imports makes it harder to see the module's dependencies at a glance. + i Move all import statements before any other statements. 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. @@ -43,6 +45,8 @@ invalid_mixed.js:5:1 lint/nursery/useImportsFirst ━━━━━━━━━━ │ ^^^^^^^^^^^^^^^^^^^^^^ 6 │ + i Scattering imports makes it harder to see the module's dependencies at a glance. + i Move all import statements before any other statements. 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. From 2a6ad5c5f05f96b647a16ce1e545f32777d483fe Mon Sep 17 00:00:00 2001 From: Liam Date: Sat, 28 Feb 2026 12:21:33 -0500 Subject: [PATCH 4/5] test(lint): add useImportsFirst test with all imports after statements --- .../useImportsFirst/invalid_all_after.js | 5 ++ .../useImportsFirst/invalid_all_after.js.snap | 55 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid_all_after.js create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid_all_after.js.snap diff --git a/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid_all_after.js b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid_all_after.js new file mode 100644 index 000000000000..b59579a85192 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid_all_after.js @@ -0,0 +1,5 @@ +const foo = 1; +let bar = 2; +function baz() {} +import { qux } from "qux"; +import { quux } from "quux"; diff --git a/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid_all_after.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid_all_after.js.snap new file mode 100644 index 000000000000..f2750fa63a38 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useImportsFirst/invalid_all_after.js.snap @@ -0,0 +1,55 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid_all_after.js +--- +# Input +```js +const foo = 1; +let bar = 2; +function baz() {} +import { qux } from "qux"; +import { quux } from "quux"; + +``` + +# Diagnostics +``` +invalid_all_after.js:4:1 lint/nursery/useImportsFirst ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This import should appear at the top of the module. + + 2 │ let bar = 2; + 3 │ function baz() {} + > 4 │ import { qux } from "qux"; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^ + 5 │ import { quux } from "quux"; + 6 │ + + i Scattering imports makes it harder to see the module's dependencies at a glance. + + i Move all import statements before any other statements. + + 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_all_after.js:5:1 lint/nursery/useImportsFirst ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This import should appear at the top of the module. + + 3 │ function baz() {} + 4 │ import { qux } from "qux"; + > 5 │ import { quux } from "quux"; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 6 │ + + i Scattering imports makes it harder to see the module's dependencies at a glance. + + i Move all import statements before any other statements. + + 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. + + +``` From 70dec323b16b9c1eef7c7b2a11be3896433378d0 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 28 Feb 2026 20:09:04 +0000 Subject: [PATCH 5/5] [autofix.ci] apply automated fixes --- .../migrate/eslint_any_rule_to_biome.rs | 12 +++++++++++ .../@biomejs/backend-jsonrpc/src/workspace.ts | 20 ++++++++++++++++--- .../@biomejs/biome/configuration_schema.json | 2 +- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs index 3466e3e4cd7f..02b7f07d33ff 100644 --- a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs +++ b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs @@ -1293,6 +1293,18 @@ pub(crate) fn migrate_eslint_any_rule( .get_or_insert(Default::default()); rule.set_level(rule.level().max(rule_severity.into())); } + "import/first" => { + if !options.include_nursery { + results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery); + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group + .unwrap_group_as_mut() + .use_imports_first + .get_or_insert(Default::default()); + rule.set_level(rule.level().max(rule_severity.into())); + } "import/named" => { if !options.include_inspired { results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Inspired); diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 807df9bdb07e..e187fc4af364 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -2385,6 +2385,11 @@ See https://biomejs.dev/linter/rules/use-global-this */ useGlobalThis?: UseGlobalThisConfiguration; /** + * Enforce that all imports appear at the top of the module. +See https://biomejs.dev/linter/rules/use-imports-first + */ + useImportsFirst?: UseImportsFirstConfiguration; + /** * Enforce id attribute on next/script components with inline content or dangerouslySetInnerHTML. See https://biomejs.dev/linter/rules/use-inline-script-id */ @@ -4254,6 +4259,9 @@ export type UseFindConfiguration = export type UseGlobalThisConfiguration = | RulePlainConfiguration | RuleWithUseGlobalThisOptions; +export type UseImportsFirstConfiguration = + | RulePlainConfiguration + | RuleWithUseImportsFirstOptions; export type UseInlineScriptIdConfiguration = | RulePlainConfiguration | RuleWithUseInlineScriptIdOptions; @@ -5955,6 +5963,10 @@ export interface RuleWithUseGlobalThisOptions { level: RulePlainConfiguration; options?: UseGlobalThisOptions; } +export interface RuleWithUseImportsFirstOptions { + level: RulePlainConfiguration; + options?: UseImportsFirstOptions; +} export interface RuleWithUseInlineScriptIdOptions { level: RulePlainConfiguration; options?: UseInlineScriptIdOptions; @@ -7409,6 +7421,7 @@ export type UseExpectOptions = {}; export type UseExplicitTypeOptions = {}; export type UseFindOptions = {}; export type UseGlobalThisOptions = {}; +export type UseImportsFirstOptions = {}; export type UseInlineScriptIdOptions = {}; export interface UseInputNameOptions { /** @@ -8319,10 +8332,10 @@ export type Category = | "lint/correctness/useValidForDirection" | "lint/correctness/useValidTypeof" | "lint/correctness/useYield" - | "lint/nursery/useExpect" | "lint/nursery/noAmbiguousAnchorText" | "lint/nursery/noBeforeInteractiveScriptOutsideDocument" | "lint/nursery/noColorInvalidHex" + | "lint/nursery/noConditionalExpect" | "lint/nursery/noContinue" | "lint/nursery/noDeprecatedMediaType" | "lint/nursery/noDivRegex" @@ -8352,7 +8365,6 @@ export type Category = | "lint/nursery/noMultiStr" | "lint/nursery/noNestedPromises" | "lint/nursery/noParametersOnlyUsedInRecursion" - | "lint/nursery/noConditionalExpect" | "lint/nursery/noPlaywrightElementHandle" | "lint/nursery/noPlaywrightEval" | "lint/nursery/noPlaywrightForceOption" @@ -8392,19 +8404,21 @@ export type Category = | "lint/nursery/useDestructuring" | "lint/nursery/useErrorCause" | "lint/nursery/useExhaustiveSwitchCases" + | "lint/nursery/useExpect" | "lint/nursery/useExplicitFunctionReturnType" | "lint/nursery/useExplicitType" | "lint/nursery/useFind" | "lint/nursery/useGlobalThis" | "lint/nursery/useImportRestrictions" + | "lint/nursery/useImportsFirst" | "lint/nursery/useInlineScriptId" | "lint/nursery/useInputName" | "lint/nursery/useJsxCurlyBraceConvention" | "lint/nursery/useLoneAnonymousOperation" | "lint/nursery/useLoneExecutableDefinition" - | "lint/nursery/useNullishCoalescing" | "lint/nursery/useMaxParams" | "lint/nursery/useNamedCaptureGroup" + | "lint/nursery/useNullishCoalescing" | "lint/nursery/usePlaywrightValidDescribeCallback" | "lint/nursery/useQwikMethodUsage" | "lint/nursery/useQwikValidLexicalScope" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 77e9fa0a9868..e9daed7c03a9 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -6233,7 +6233,7 @@ ] }, "useImportsFirst": { - "description": "Succinct description of the rule.\nSee https://biomejs.dev/linter/rules/use-imports-first", + "description": "Enforce that all imports appear at the top of the module.\nSee https://biomejs.dev/linter/rules/use-imports-first", "anyOf": [ { "$ref": "#/$defs/UseImportsFirstConfiguration" }, { "type": "null" }