From 1aefcb186f47df33685aeb2fdd249cd90bdf1892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20R=C3=A8gne?= Date: Sat, 13 Jun 2026 17:18:10 +0200 Subject: [PATCH 01/18] feat(js-api): add spanInBytesToSpanInCodeUnits in subpath exports (#9940) --- .../add-span-conversion-helper-in-subpath.md | 18 ++++++++++++++++++ packages/@biomejs/js-api/README.md | 19 +++++++++++++++++-- packages/@biomejs/js-api/src/bundler.ts | 1 + packages/@biomejs/js-api/src/nodejs.ts | 1 + packages/@biomejs/js-api/src/web.ts | 1 + 5 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 .changeset/add-span-conversion-helper-in-subpath.md diff --git a/.changeset/add-span-conversion-helper-in-subpath.md b/.changeset/add-span-conversion-helper-in-subpath.md new file mode 100644 index 000000000000..6c8461e5a782 --- /dev/null +++ b/.changeset/add-span-conversion-helper-in-subpath.md @@ -0,0 +1,18 @@ +--- +"@biomejs/js-api": minor +--- + +Added `spanInBytesToSpanInCodeUnits` helper function in subpath exports of `@biomejs/js-api`. + +```js +import { spanInBytesToSpanInCodeUnits } from "@biomejs/js-api/nodejs"; +// Or: +// import { spanInBytesToSpanInCodeUnits } from "@biomejs/js-api/bundler"; +// import { spanInBytesToSpanInCodeUnits } from "@biomejs/js-api/web"; + +const [start, end] = spanInBytesToSpanInCodeUnits( + diagnostic.location.span, + content +); +const text = content.slice(start, end); // Correctly extracts the text +``` diff --git a/packages/@biomejs/js-api/README.md b/packages/@biomejs/js-api/README.md index 7aab21cdd90b..d9306d4a9719 100644 --- a/packages/@biomejs/js-api/README.md +++ b/packages/@biomejs/js-api/README.md @@ -22,8 +22,8 @@ You need to install one of the `@biomejs/wasm-*` package as a **peer dependency* ```js import { Biome } from "@biomejs/js-api/nodejs"; // Or: -// import { Biome, Distribution } from "@biomejs/js-api/bundler"; -// import { Biome, Distribution } from "@biomejs/js-api/web"; +// import { Biome } from "@biomejs/js-api/bundler"; +// import { Biome } from "@biomejs/js-api/web"; const biome = new Biome(); const { projectKey } = biome.openProject("path/to/project/dir"); @@ -53,6 +53,21 @@ const html = biome.printDiagnostics(result.diagnostics, { console.log("Lint diagnostics: ", html); ``` +If you want to work with diagnostics in your program, you can use the `spanInBytesToSpanInCodeUnits` helper function to convert byte-based segments from Biome diagnostics into UTF-16 code unit segments (used in JavaScript). + +```js +import { Biome, spanInBytesToSpanInCodeUnits } from "@biomejs/js-api/nodejs"; + +for (const diagnostic of result.diagnostics) { + const [start, end] = spanInBytesToSpanInCodeUnits( + diagnostic.location.span, + formatted.content, + ); + // Correctly extracts the text + const text = formatted.content.slice(start, end); +} +``` + ## Philosophy The project philosophy can be found on our [website](https://biomejs.dev/internals/philosophy/). diff --git a/packages/@biomejs/js-api/src/bundler.ts b/packages/@biomejs/js-api/src/bundler.ts index 315aa41ebc77..12447126c2c5 100644 --- a/packages/@biomejs/js-api/src/bundler.ts +++ b/packages/@biomejs/js-api/src/bundler.ts @@ -3,6 +3,7 @@ import * as moduleBundler from "@biomejs/wasm-bundler"; import { BiomeCommon } from "./common"; export type * from "./common"; +export { spanInBytesToSpanInCodeUnits } from "./common"; export type { Configuration, Diagnostic }; export class Biome extends BiomeCommon { diff --git a/packages/@biomejs/js-api/src/nodejs.ts b/packages/@biomejs/js-api/src/nodejs.ts index 90d5c6fbe2db..8e8a859dfb7e 100644 --- a/packages/@biomejs/js-api/src/nodejs.ts +++ b/packages/@biomejs/js-api/src/nodejs.ts @@ -3,6 +3,7 @@ import * as moduleNodeJs from "@biomejs/wasm-nodejs"; import { BiomeCommon } from "./common"; export type * from "./common"; +export { spanInBytesToSpanInCodeUnits } from "./common"; export type { Configuration, Diagnostic }; export class Biome extends BiomeCommon { diff --git a/packages/@biomejs/js-api/src/web.ts b/packages/@biomejs/js-api/src/web.ts index 058b54524a8c..4483eb573834 100644 --- a/packages/@biomejs/js-api/src/web.ts +++ b/packages/@biomejs/js-api/src/web.ts @@ -3,6 +3,7 @@ import * as moduleWeb from "@biomejs/wasm-web"; import { BiomeCommon } from "./common"; export type * from "./common"; +export { spanInBytesToSpanInCodeUnits } from "./common"; export type { Configuration, Diagnostic }; export class Biome extends BiomeCommon { From 1077238e6361e163ab2462a3a81616cd026a7e4d Mon Sep 17 00:00:00 2001 From: Thibaut LaBarre <59576766+thibaut-pro@users.noreply.github.com> Date: Thu, 9 Jul 2026 23:03:02 -0700 Subject: [PATCH 02/18] feat(useSortedAttributes): add sortFirst option (#10631) Co-authored-by: Emanuele Stoppa --- .../use-sorted-attributes-sort-first.md | 32 ++++++ .../src/shared/sort_attributes.rs | 25 +++++ .../assist/source/use_sorted_attributes.rs | 28 ++++- .../useSortedAttributes/html/invalid.html | 3 + .../html/invalid.html.snap | 56 ++++++++++ .../html/invalid.options.json | 15 +++ .../useSortedAttributes/html/valid.html | 2 + .../useSortedAttributes/html/valid.html.snap | 10 ++ .../html/valid.options.json | 15 +++ .../assist/source/use_sorted_attributes.rs | 30 ++++- .../source/useSortedAttributes/invalid.jsx | 6 + .../useSortedAttributes/invalid.jsx.snap | 105 ++++++++++++++++++ .../useSortedAttributes/invalid.options.json | 15 +++ .../source/useSortedAttributes/valid.jsx | 3 + .../source/useSortedAttributes/valid.jsx.snap | 11 ++ .../useSortedAttributes/valid.options.json | 15 +++ .../src/use_sorted_attributes.rs | 8 ++ .../@biomejs/backend-jsonrpc/src/workspace.ts | 8 ++ .../@biomejs/biome/configuration_schema.json | 5 + 19 files changed, 388 insertions(+), 4 deletions(-) create mode 100644 .changeset/use-sorted-attributes-sort-first.md create mode 100644 crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/invalid.html create mode 100644 crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/invalid.html.snap create mode 100644 crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/invalid.options.json create mode 100644 crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/valid.html create mode 100644 crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/valid.html.snap create mode 100644 crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/valid.options.json create mode 100644 crates/biome_js_analyze/tests/specs/source/useSortedAttributes/invalid.jsx create mode 100644 crates/biome_js_analyze/tests/specs/source/useSortedAttributes/invalid.jsx.snap create mode 100644 crates/biome_js_analyze/tests/specs/source/useSortedAttributes/invalid.options.json create mode 100644 crates/biome_js_analyze/tests/specs/source/useSortedAttributes/valid.jsx create mode 100644 crates/biome_js_analyze/tests/specs/source/useSortedAttributes/valid.jsx.snap create mode 100644 crates/biome_js_analyze/tests/specs/source/useSortedAttributes/valid.options.json diff --git a/.changeset/use-sorted-attributes-sort-first.md b/.changeset/use-sorted-attributes-sort-first.md new file mode 100644 index 000000000000..d79ba1a91f38 --- /dev/null +++ b/.changeset/use-sorted-attributes-sort-first.md @@ -0,0 +1,32 @@ +--- +"@biomejs/biome": minor +--- + +Added the `sortFirst` option to the [useSortedAttributes](https://biomejs.dev/assist/actions/use-sorted-attributes/) action (JSX and HTML). It takes a list of attribute names that are sorted before all other attributes, in the order given, while the remaining attributes keep their usual sort. This is useful to keep attributes such as `key` first. + +With the following configuration: + +```json +{ + "assist": { + "actions": { + "source": { + "useSortedAttributes": { + "level": "on", + "options": { "sortFirst": ["key"] } + } + } + } + } +} +``` + +The attributes are reordered so that `key` comes first: + +```jsx +// Before +
; + +// After +
; +``` diff --git a/crates/biome_analyze/src/shared/sort_attributes.rs b/crates/biome_analyze/src/shared/sort_attributes.rs index 461b298a18e3..90ec95f033e7 100644 --- a/crates/biome_analyze/src/shared/sort_attributes.rs +++ b/crates/biome_analyze/src/shared/sort_attributes.rs @@ -38,6 +38,31 @@ pub trait SortableAttribute { (None, None) => Ordering::Equal, } } + + fn sort_first_index(&self, sort_first: &[Box]) -> usize { + self.name() + .and_then(|name| { + sort_first + .iter() + .position(|listed| listed.as_ref() == name.text_trimmed()) + }) + .unwrap_or(sort_first.len()) + } + + fn cmp_sort_first( + &self, + other: &Self, + sort_first: &[Box], + base: impl Fn(&Self, &Self) -> Ordering, + ) -> Ordering { + if sort_first.is_empty() { + return base(self, other); + } + + self.sort_first_index(sort_first) + .cmp(&other.sort_first_index(sort_first)) + .then_with(|| base(self, other)) + } } #[derive(Clone)] diff --git a/crates/biome_html_analyze/src/assist/source/use_sorted_attributes.rs b/crates/biome_html_analyze/src/assist/source/use_sorted_attributes.rs index 06b002dace7e..f410bafa4eb0 100644 --- a/crates/biome_html_analyze/src/assist/source/use_sorted_attributes.rs +++ b/crates/biome_html_analyze/src/assist/source/use_sorted_attributes.rs @@ -117,6 +117,22 @@ declare_source_rule! { /// /// ``` /// + /// ### `sortFirst` + /// A list of attribute names that should be sorted before all other attributes, + /// in the order they appear in this list. The remaining attributes are sorted + /// after the listed ones. Listed attributes take precedence over the category-based ordering. + /// + /// ```json,options + /// { + /// "options": { + /// "sortFirst": ["type"] + /// } + /// } + /// ``` + /// ```html,use_options,expect_diagnostic + /// + /// ``` + /// pub UseSortedAttributes { version: "next", name: "useSortedAttributes", @@ -140,8 +156,12 @@ impl Rule for UseSortedAttributes { let mut current_attr_group = AttributeGroup::default(); let mut attr_groups = Vec::new(); let sort_by = options.sort_order.unwrap_or_default(); + let sort_first = options.sort_first.as_deref().unwrap_or_default(); - let comparator = get_comparator(sort_by); + let base = get_comparator(sort_by); + let comparator = |a: &SortableHtmlAttribute, b: &SortableHtmlAttribute| { + a.cmp_sort_first(b, sort_first, base) + }; // Convert to boolean-based comparator for is_sorted_by let boolean_comparator = |a: &SortableHtmlAttribute, b: &SortableHtmlAttribute| { @@ -202,8 +222,12 @@ impl Rule for UseSortedAttributes { let mut mutation = ctx.root().begin(); let options = ctx.options(); let sort_by = options.sort_order.unwrap_or_default(); + let sort_first = options.sort_first.as_deref().unwrap_or_default(); - let comparator = get_comparator(sort_by); + let base = get_comparator(sort_by); + let comparator = |a: &SortableHtmlAttribute, b: &SortableHtmlAttribute| { + a.cmp_sort_first(b, sort_first, base) + }; for (SortableHtmlAttribute(attr), SortableHtmlAttribute(sorted_attr)) in zip(state.attrs.iter(), state.get_sorted_attributes(comparator)?) diff --git a/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/invalid.html b/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/invalid.html new file mode 100644 index 000000000000..b081156e669d --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/invalid.html @@ -0,0 +1,3 @@ + + + diff --git a/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/invalid.html.snap b/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/invalid.html.snap new file mode 100644 index 000000000000..4f266d56828c --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/invalid.html.snap @@ -0,0 +1,56 @@ +--- +source: crates/biome_html_analyze/tests/spec_tests.rs +expression: invalid.html +--- +# Input +```html + + + + +``` + +# Diagnostics +``` +invalid.html:2:1 assist/source/useSortedAttributes FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i The attributes are not sorted. + + 1 │ + > 2 │ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 3 │ + 4 │ + + i Safe fix: Sort the HTML attributes. + + 1 1 │ + 2 │ - + 2 │ + + 3 3 │ + 4 4 │ + + +``` + +``` +invalid.html:3:1 assist/source/useSortedAttributes FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i The attributes are not sorted. + + 1 │ + 2 │ + > 3 │ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 4 │ + + i Safe fix: Sort the HTML attributes. + + 1 1 │ + 2 2 │ + 3 │ - + 3 │ + + 4 4 │ + + +``` diff --git a/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/invalid.options.json b/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/invalid.options.json new file mode 100644 index 000000000000..1f078f2b52e3 --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/invalid.options.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../../../../../packages/@biomejs/biome/configuration_schema.json", + "assist": { + "actions": { + "source": { + "useSortedAttributes": { + "level": "on", + "options": { + "sortFirst": ["type", "id"] + } + } + } + } + } +} diff --git a/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/valid.html b/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/valid.html new file mode 100644 index 000000000000..6bb1e7c7a4c8 --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/valid.html @@ -0,0 +1,2 @@ + + diff --git a/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/valid.html.snap b/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/valid.html.snap new file mode 100644 index 000000000000..fcbbad5af5d6 --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/valid.html.snap @@ -0,0 +1,10 @@ +--- +source: crates/biome_html_analyze/tests/spec_tests.rs +expression: valid.html +--- +# Input +```html + + + +``` diff --git a/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/valid.options.json b/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/valid.options.json new file mode 100644 index 000000000000..1f078f2b52e3 --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/valid.options.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../../../../../packages/@biomejs/biome/configuration_schema.json", + "assist": { + "actions": { + "source": { + "useSortedAttributes": { + "level": "on", + "options": { + "sortFirst": ["type", "id"] + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/src/assist/source/use_sorted_attributes.rs b/crates/biome_js_analyze/src/assist/source/use_sorted_attributes.rs index cd4f566b7ba6..b18a84b801a0 100644 --- a/crates/biome_js_analyze/src/assist/source/use_sorted_attributes.rs +++ b/crates/biome_js_analyze/src/assist/source/use_sorted_attributes.rs @@ -71,6 +71,22 @@ declare_source_rule! { /// ; /// ``` /// + /// ### `sortFirst` + /// A list of attribute names that should be sorted before all other attributes, + /// in the order they appear in this list. The remaining attributes are sorted + /// after the listed ones. This is useful to keep attributes such as `key` first. + /// + /// ```json,options + /// { + /// "options": { + /// "sortFirst": ["key"] + /// } + /// } + /// ``` + /// ```jsx,use_options,expect_diagnostic + /// ; + /// ``` + /// pub UseSortedAttributes { version: "2.0.0", name: "useSortedAttributes", @@ -93,12 +109,17 @@ impl Rule for UseSortedAttributes { let mut prop_groups = Vec::new(); let options = ctx.options(); let sort_by = options.sort_order.unwrap_or_default(); + let sort_first = options.sort_first.as_deref().unwrap_or_default(); - let comparator = match sort_by { + let base = match sort_by { SortOrder::Natural => SortableJsxAttribute::ascii_nat_cmp, SortOrder::Lexicographic => SortableJsxAttribute::lexicographic_cmp, }; + let comparator = |a: &SortableJsxAttribute, b: &SortableJsxAttribute| { + a.cmp_sort_first(b, sort_first, base) + }; + // Convert to boolean-based comparator for is_sorted_by let boolean_comparator = |a: &SortableJsxAttribute, b: &SortableJsxAttribute| { comparator(a, b) != Ordering::Greater @@ -156,12 +177,17 @@ impl Rule for UseSortedAttributes { let mut mutation = ctx.root().begin(); let options = ctx.options(); let sort_by = options.sort_order.unwrap_or_default(); + let sort_first = options.sort_first.as_deref().unwrap_or_default(); - let comparator = match sort_by { + let base = match sort_by { SortOrder::Natural => SortableJsxAttribute::ascii_nat_cmp, SortOrder::Lexicographic => SortableJsxAttribute::lexicographic_cmp, }; + let comparator = |a: &SortableJsxAttribute, b: &SortableJsxAttribute| { + a.cmp_sort_first(b, sort_first, base) + }; + for (SortableJsxAttribute(attr), SortableJsxAttribute(sorted_attr)) in zip(state.attrs.iter(), state.get_sorted_attributes(comparator)?) { diff --git a/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/invalid.jsx b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/invalid.jsx new file mode 100644 index 000000000000..e94b64c0c822 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/invalid.jsx @@ -0,0 +1,6 @@ +/* should generate diagnostics */ +; + +; + +; diff --git a/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/invalid.jsx.snap b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/invalid.jsx.snap new file mode 100644 index 000000000000..d785f1cc29f2 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/invalid.jsx.snap @@ -0,0 +1,105 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.jsx +--- +# Input +```jsx +/* should generate diagnostics */ +; + +; + +; + +``` + +# Diagnostics +``` +invalid.jsx:2:1 assist/source/useSortedAttributes FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i The attributes are not sorted. + + 1 │ /* should generate diagnostics */ + > 2 │ ; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 3 │ + 4 │ ; + + i Safe fix: Sort the JSX props. + + 1 1 │ /* should generate diagnostics */ + 2 │ - ; + 2 │ + ; + 3 3 │ + 4 4 │ ; + + +``` + +``` +invalid.jsx:4:1 assist/source/useSortedAttributes FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i The attributes are not sorted. + + 2 │ ; + 3 │ + > 4 │ ; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 5 │ + 6 │ ; + + i Safe fix: Sort the JSX props. + + 2 2 │ ; + 3 3 │ + 4 │ - ; + 4 │ + ; + 5 5 │ + 6 6 │ ; + + +``` + +``` +invalid.jsx:6:1 assist/source/useSortedAttributes FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i The attributes are not sorted. + + 4 │ ; + 5 │ + > 6 │ ; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 7 │ + + i Safe fix: Sort the JSX props. + + 4 4 │ ; + 5 5 │ + 6 │ - ; + 6 │ + ; + 7 7 │ + + +``` + +``` +invalid.jsx:6:1 assist/source/useSortedAttributes FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i The attributes are not sorted. + + 4 │ ; + 5 │ + > 6 │ ; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 7 │ + + i Safe fix: Sort the JSX props. + + 4 4 │ ; + 5 5 │ + 6 │ - ; + 6 │ + ; + 7 7 │ + + +``` diff --git a/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/invalid.options.json b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/invalid.options.json new file mode 100644 index 000000000000..dba8334b54cc --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/invalid.options.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "assist": { + "actions": { + "source": { + "useSortedAttributes": { + "level": "on", + "options": { + "sortFirst": ["key", "ref"] + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/valid.jsx b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/valid.jsx new file mode 100644 index 000000000000..20a6a2fa76c2 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/valid.jsx @@ -0,0 +1,3 @@ +/* should not generate diagnostics */ +; +; diff --git a/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/valid.jsx.snap new file mode 100644 index 000000000000..41f9229e9931 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/valid.jsx.snap @@ -0,0 +1,11 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.jsx +--- +# Input +```jsx +/* should not generate diagnostics */ +; +; + +``` diff --git a/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/valid.options.json b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/valid.options.json new file mode 100644 index 000000000000..dba8334b54cc --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/valid.options.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "assist": { + "actions": { + "source": { + "useSortedAttributes": { + "level": "on", + "options": { + "sortFirst": ["key", "ref"] + } + } + } + } + } +} diff --git a/crates/biome_rule_options/src/use_sorted_attributes.rs b/crates/biome_rule_options/src/use_sorted_attributes.rs index 452cd174fd4e..5f356080226b 100644 --- a/crates/biome_rule_options/src/use_sorted_attributes.rs +++ b/crates/biome_rule_options/src/use_sorted_attributes.rs @@ -8,4 +8,12 @@ use serde::{Deserialize, Serialize}; pub struct UseSortedAttributesOptions { #[serde(skip_serializing_if = "Option::<_>::is_none")] pub sort_order: Option, + + /// A list of attribute names that should be sorted before all other + /// attributes, in the order they appear in this list. The remaining + /// attributes are sorted after the listed ones. + /// + /// This is useful to keep attributes such as `key` first. + #[serde(skip_serializing_if = "Option::<_>::is_none")] + pub sort_first: Option]>>, } diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 47b71822c2d7..4b632abaec94 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -5599,6 +5599,14 @@ Default: `natural`. sortBareImports?: boolean; } export interface UseSortedAttributesOptions { + /** + * A list of attribute names that should be sorted before all other +attributes, in the order they appear in this list. The remaining +attributes are sorted after the listed ones. + +This is useful to keep attributes such as `key` first. + */ + sortFirst?: string[]; sortOrder?: SortOrder; } export type UseSortedEnumMembersOptions = {}; diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 1fda0d389d57..c2752df94514 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -16056,6 +16056,11 @@ "UseSortedAttributesOptions": { "type": "object", "properties": { + "sortFirst": { + "description": "A list of attribute names that should be sorted before all other\nattributes, in the order they appear in this list. The remaining\nattributes are sorted after the listed ones.\n\nThis is useful to keep attributes such as `key` first.", + "type": ["array", "null"], + "items": { "type": "string" } + }, "sortOrder": { "anyOf": [{ "$ref": "#/$defs/SortOrder" }, { "type": "null" }] } From 40de5a0383473d37a4667f7cdb46817e0e73b686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9amus=20O=27Connor?= Date: Thu, 23 Jul 2026 02:26:12 -0700 Subject: [PATCH 03/18] feat(plugins): add resolutionKind for object-syntax plugin entries (#10711) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .changeset/eight-lamps-follow.md | 19 +++ .../biome_cli/tests/cases/config_extends.rs | 48 +++++++ crates/biome_cli/tests/cases/monorepo.rs | 92 ++++++++++++++ ...object_syntax_plugin_from_npm_package.snap | 92 ++++++++++++++ ...d_config_missing_plugin_reports_error.snap | 51 ++++++++ ...ded_config_work_from_declaring_config.snap | 61 +++++++++ .../src/analyzer_grit_plugin.rs | 4 +- .../biome_plugin_loader/src/configuration.rs | 120 +++++++++++++++++- crates/biome_plugin_loader/src/diagnostics.rs | 13 ++ crates/biome_plugin_loader/src/lib.rs | 36 +++++- crates/biome_service/src/configuration.rs | 19 ++- crates/biome_service/src/workspace.tests.rs | 1 + .../@biomejs/backend-jsonrpc/src/workspace.ts | 14 +- .../@biomejs/biome/configuration_schema.json | 22 +++- 14 files changed, 581 insertions(+), 11 deletions(-) create mode 100644 .changeset/eight-lamps-follow.md create mode 100644 crates/biome_cli/tests/snapshots/main_cases_config_extends/extends_config_with_object_syntax_plugin_from_npm_package.snap create mode 100644 crates/biome_cli/tests/snapshots/main_cases_monorepo/object_syntax_plugins_in_extended_config_missing_plugin_reports_error.snap create mode 100644 crates/biome_cli/tests/snapshots/main_cases_monorepo/object_syntax_plugins_in_extended_config_work_from_declaring_config.snap diff --git a/.changeset/eight-lamps-follow.md b/.changeset/eight-lamps-follow.md new file mode 100644 index 000000000000..4511b97a8475 --- /dev/null +++ b/.changeset/eight-lamps-follow.md @@ -0,0 +1,19 @@ +--- +"@biomejs/biome": minor +--- + +Adds a new `resolutionKind` option for object-syntax plugin entries. + +This allows shared Biome configs in monorepos to load local Grit plugins from the package that declares them instead of resolving plugin paths from the consuming project. + +```json +{ + "plugins": [ + { "path": "./grit/no-debugger.grit", "resolutionKind": "config" }, + { "path": "local-plugin.grit", "resolutionKind": "project" }, + { "path": "local-plugin.grit" } + ] +} +``` + +Use `resolutionKind: "config"` to resolve the plugin from the configuration file that declares it. Use `resolutionKind: "project"` to resolve the plugin from the consuming project. When omitted, `resolutionKind` defaults to `project`. This only affects plugin resolution; `includes` still apply to end-user project files as usual. diff --git a/crates/biome_cli/tests/cases/config_extends.rs b/crates/biome_cli/tests/cases/config_extends.rs index 26d053b1f56b..6bd719bf7471 100644 --- a/crates/biome_cli/tests/cases/config_extends.rs +++ b/crates/biome_cli/tests/cases/config_extends.rs @@ -228,6 +228,54 @@ fn extends_config_ok_from_npm_package_with_condition_names() { )); } +#[test] +fn extends_config_with_object_syntax_plugin_from_npm_package() { + let mut fs = TemporaryFs::new("extends_config_with_object_syntax_plugin_from_npm_package"); + + fs.create_file("biome.json", r#"{ "extends": ["@shared/config/biome"] }"#); + + fs.create_file( + "node_modules/@shared/config/biome.jsonc", + r#"{ "root": false, "plugins": [{ "path": "./grit/no-object-assign.grit", "resolutionKind": "config" }], "linter": { "enabled": true } }"#, + ); + fs.create_file( + "node_modules/@shared/config/package.json", + r#"{ + "name": "@shared/config", + "exports": { + "./biome": "./biome.jsonc" + } +}"#, + ); + fs.create_file( + "node_modules/@shared/config/grit/no-object-assign.grit", + r#"`$fn($args)` where { + $fn <: `Object.assign`, + register_diagnostic( + span = $fn, + message = "Prefer object spread instead of Object.assign()", + severity = "warn" + ) +}"#, + ); + fs.create_file("test.js", "const merged = Object.assign({}, a, b);\n"); + + let mut console = BufferConsole::default(); + let result = run_cli_with_dyn_fs( + Box::new(fs.create_os()), + &mut console, + Args::from(["lint", &format!("{}/test.js", fs.cli_path())].as_slice()), + ); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "extends_config_with_object_syntax_plugin_from_npm_package", + fs.create_mem(), + console, + result, + )); +} + #[test] fn extends_config_ok_linter_not_formatter() { let fs = MemoryFileSystem::default(); diff --git a/crates/biome_cli/tests/cases/monorepo.rs b/crates/biome_cli/tests/cases/monorepo.rs index ee69277d66d1..f580a21075f4 100644 --- a/crates/biome_cli/tests/cases/monorepo.rs +++ b/crates/biome_cli/tests/cases/monorepo.rs @@ -815,3 +815,95 @@ fn plugins_from_root_config_work_in_child_config_extends_root() { result, )); } + +#[test] +fn object_syntax_plugins_in_extended_config_work_from_declaring_config() { + let mut fs = + TemporaryFs::new("object_syntax_plugins_in_extended_config_work_from_declaring_config"); + + fs.create_file("biome.json", r#"{ "root": true }"#); + + fs.create_file( + "packages/tools/biome.shared.jsonc", + r#"{ "plugins": [{ "path": "./biome-plugins/no-object-assign.grit", "resolutionKind": "config" }], "linter": { "enabled": true } }"#, + ); + + fs.create_file( + "packages/tools/biome-plugins/no-object-assign.grit", + r#"`$fn($args)` where { + $fn <: `Object.assign`, + register_diagnostic( + span = $fn, + message = "Prefer object spread instead of Object.assign()", + severity = "warn" + ) +}"#, + ); + + fs.create_file( + "packages/mobile/biome.json", + r#"{ "root": false, "extends": ["../tools/biome.shared.jsonc"] }"#, + ); + fs.create_file("packages/mobile/src/file.js", "Object.assign({}, a, b);\n"); + + let mut console = BufferConsole::default(); + let result = run_cli_with_dyn_fs( + Box::new(fs.create_os()), + &mut console, + Args::from( + [ + "lint", + &format!("{}/packages/mobile/src/file.js", fs.cli_path()), + ] + .as_slice(), + ), + ); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "object_syntax_plugins_in_extended_config_work_from_declaring_config", + fs.create_mem(), + console, + result, + )); +} + +#[test] +fn object_syntax_plugins_in_extended_config_missing_plugin_reports_error() { + let mut fs = + TemporaryFs::new("object_syntax_plugins_in_extended_config_missing_plugin_reports_error"); + + fs.create_file("biome.json", r#"{ "root": true }"#); + + fs.create_file( + "packages/tools/biome.shared.jsonc", + r#"{ "plugins": [{ "path": "./biome-plugins/no-object-assign.grit", "resolutionKind": "config" }], "linter": { "enabled": true } }"#, + ); + + fs.create_file( + "packages/mobile/biome.json", + r#"{ "root": false, "extends": ["../tools/biome.shared.jsonc"] }"#, + ); + fs.create_file("packages/mobile/src/file.js", "Object.assign({}, a, b);\n"); + + let mut console = BufferConsole::default(); + let result = run_cli_with_dyn_fs( + Box::new(fs.create_os()), + &mut console, + Args::from( + [ + "lint", + &format!("{}/packages/mobile/src/file.js", fs.cli_path()), + ] + .as_slice(), + ), + ); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "object_syntax_plugins_in_extended_config_missing_plugin_reports_error", + fs.create_mem(), + console, + result, + )); +} diff --git a/crates/biome_cli/tests/snapshots/main_cases_config_extends/extends_config_with_object_syntax_plugin_from_npm_package.snap b/crates/biome_cli/tests/snapshots/main_cases_config_extends/extends_config_with_object_syntax_plugin_from_npm_package.snap new file mode 100644 index 000000000000..1cabf0648b3c --- /dev/null +++ b/crates/biome_cli/tests/snapshots/main_cases_config_extends/extends_config_with_object_syntax_plugin_from_npm_package.snap @@ -0,0 +1,92 @@ +--- +source: crates/biome_cli/tests/snap_test.rs +assertion_line: 549 +expression: redactor(content) +--- +## `node_modules/@shared/config/biome.jsonc` + +```json +{ + "root": false, + "plugins": [ + { "path": "./grit/no-object-assign.grit", "resolutionKind": "config" } + ], + "linter": { "enabled": true } +} +``` + +## `biome.json` + +```json +{ "extends": ["@shared/config/biome"] } +``` + +## `node_modules/@shared/config/grit/no-object-assign.grit` + +```grit +`$fn($args)` where { + $fn <: `Object.assign`, + register_diagnostic( + span = $fn, + message = "Prefer object spread instead of Object.assign()", + severity = "warn" + ) +} +``` + +## `node_modules/@shared/config/package.json` + +```json +{ + "name": "@shared/config", + "exports": { + "./biome": "./biome.jsonc" + } +} +``` + +## `test.js` + +```js +const merged = Object.assign({}, a, b); + +``` + +# Emitted Messages + +```block +test.js:1:16 plugin ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer object spread instead of Object.assign() + + > 1 │ const merged = Object.assign({}, a, b); + │ ^^^^^^^^^^^^^ + 2 │ + + +``` + +```block +test.js:1:7 lint/correctness/noUnusedVariables FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This variable merged is unused. + + > 1 │ const merged = Object.assign({}, a, b); + │ ^^^^^^ + 2 │ + + i Unused variables are often the result of typos, incomplete refactors, or other sources of bugs. + + i Unsafe fix: If this is intentional, prepend merged with an underscore. + + 1 │ - const·merged·=·Object.assign({},·a,·b); + 1 │ + const·_merged·=·Object.assign({},·a,·b); + 2 2 │ + + +``` + +```block +Checked 1 file in