Skip to content
Merged
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
12 changes: 12 additions & 0 deletions .changeset/ripe-eyes-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@biomejs/biome": minor
---

Added Qwik Domain to Biome

This release introduces **Qwik domain support** in Biome, enabling Qwik developers to use Biome as a linter and formatter for their projects.

- Added the Qwik domain infrastructure to Biome.
- Enabled the following rules for Qwik:
- [`useJsxKeyInIterable`](https://biomejs.dev/linter/rules/use-jsx-key-in-iterable)
- [`noReactSpecificProps`](https://biomejs.dev/linter/rules/no-react-specific-props)
14 changes: 14 additions & 0 deletions crates/biome_analyze/src/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ pub enum RuleSource {
EslintN(&'static str),
/// Rules from [Eslint Plugin Next](https://github.com/vercel/next.js/tree/canary/packages/eslint-plugin-next)
EslintNext(&'static str),
/// Rules from [Eslint Plugin Qwik](https://github.com/BuilderIO/eslint-plugin-qwik)
EslintQwik(&'static str),
/// Rules from [Stylelint](https://github.com/stylelint/stylelint)
Stylelint(&'static str),
/// Rules from [Eslint Plugin No Secrets](https://github.com/nickdeis/eslint-plugin-no-secrets)
Expand Down Expand Up @@ -188,6 +190,7 @@ impl std::fmt::Display for RuleSource {
Self::EslintBarrelFiles(_) => write!(f, "eslint-plugin-barrel-files"),
Self::EslintN(_) => write!(f, "eslint-plugin-n"),
Self::EslintNext(_) => write!(f, "@next/eslint-plugin-next"),
Self::EslintQwik(_) => write!(f, "eslint-plugin-qwik"),
Self::Stylelint(_) => write!(f, "Stylelint"),
Self::EslintNoSecrets(_) => write!(f, "eslint-plugin-no-secrets"),
Self::EslintRegexp(_) => write!(f, "eslint-plugin-regexp"),
Expand Down Expand Up @@ -263,6 +266,7 @@ impl RuleSource {
| Self::EslintBarrelFiles(rule_name)
| Self::EslintN(rule_name)
| Self::EslintNext(rule_name)
| Self::EslintQwik(rule_name)
| Self::EslintNoSecrets(rule_name)
| Self::EslintRegexp(rule_name)
| Self::Stylelint(rule_name)
Expand Down Expand Up @@ -297,6 +301,7 @@ impl RuleSource {
Self::EslintBarrelFiles(rule_name) => format!("barrel-files/{rule_name}"),
Self::EslintN(rule_name) => format!("n/{rule_name}"),
Self::EslintNext(rule_name) => format!("@next/{rule_name}"),
Self::EslintQwik(rule_name) => format!("qwik/{rule_name}"),
Self::Stylelint(rule_name) => format!("stylelint/{rule_name}"),
Self::EslintNoSecrets(rule_name) => format!("no-secrets/{rule_name}"),
Self::EslintRegexp(rule_name) => format!("regexp/{rule_name}"),
Expand Down Expand Up @@ -332,6 +337,7 @@ impl RuleSource {
Self::EslintBarrelFiles(rule_name) => format!("https://github.com/thepassle/eslint-plugin-barrel-files/blob/main/docs/rules/{rule_name}.md"),
Self::EslintN(rule_name) => format!("https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/{rule_name}.md"),
Self::EslintNext(rule_name) => format!("https://nextjs.org/docs/messages/{rule_name}"),
Self::EslintQwik(rule_name) => format!("https://github.com/BuilderIO/eslint-plugin-qwik/blob/main/docs/rules/{rule_name}.md"),
Self::Stylelint(rule_name) => format!("https://github.com/stylelint/stylelint/blob/main/lib/rules/{rule_name}/README.md"),
Self::EslintNoSecrets(_) => "https://github.com/nickdeis/eslint-plugin-no-secrets/blob/master/README.md".to_string(),
Self::EslintRegexp(rule_name) => format!("https://ota-meshi.github.io/eslint-plugin-regexp/rules/{rule_name}.html"),
Expand Down Expand Up @@ -408,6 +414,8 @@ pub enum RuleDomain {
Solid,
/// Next.js framework rules
Next,
/// Qwik framework rules
Qwik,
/// Vue.js framework rules
Vue,
/// For rules that require querying multiple files inside a project
Expand All @@ -422,6 +430,7 @@ impl Display for RuleDomain {
Self::Test => fmt.write_str("test"),
Self::Solid => fmt.write_str("solid"),
Self::Next => fmt.write_str("next"),
Self::Qwik => fmt.write_str("qwik"),
Self::Vue => fmt.write_str("vue"),
Self::Project => fmt.write_str("project"),
}
Expand Down Expand Up @@ -456,6 +465,10 @@ impl RuleDomain {
],
Self::Solid => &[&("solid", ">=1.0.0")],
Self::Next => &[&("next", ">=14.0.0")],
Self::Qwik => &[
&("@builder.io/qwik", ">=1.0.0"),
&("@qwik.dev/core", ">=2.0.0"),
],
Self::Vue => &[&("vue", ">=3.0.0")],
Self::Project => &[],
}
Expand All @@ -479,6 +492,7 @@ impl RuleDomain {
],
Self::Solid => &[],
Self::Next => &[],
Self::Qwik => &[],
Self::Vue => &[],
Self::Project => &[],
}
Expand Down
1 change: 1 addition & 0 deletions crates/biome_configuration/tests/invalid/domains.json.snap
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ domains.json:4:7 deserialize ━━━━━━━━━━━━━━━━━
- test
- solid
- next
- qwik
- vue
- project

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ declare_lint_rule! {
/// Warn if an element that likely requires a key prop--namely, one present in an array literal or an arrow function expression.
/// Check out React documentation for [explanation on the why does React need keys.](https://react.dev/learn/rendering-lists#why-does-react-need-keys)
///
/// This rule is intended for use in both React and Qwik applications to prevent missing key props in JSX elements inside iterators.
///
/// ## Examples
///
/// ### Invalid
Expand All @@ -28,14 +30,14 @@ declare_lint_rule! {
/// [<Hello />];
/// ```
/// ```jsx,expect_diagnostic
/// data.map((x) => <Hello>{x}</Hello>);
/// {items.map(item => <li>{item}</li>)}
/// ```
///
/// ### Valid
///
/// ```jsx
/// [<Hello key="first" />, <Hello key="second" />, <Hello key="third" />];
/// data.map((x) => <Hello key={x.id}>{x}</Hello>);
/// {items.map(item => <li key={item.id}>{item}</li>)}
/// ```
///
/// ## Options
Expand Down Expand Up @@ -63,7 +65,7 @@ declare_lint_rule! {
sources: &[RuleSource::EslintReact("jsx-key").same()],
recommended: true,
severity: Severity::Error,
domains: &[RuleDomain::React],
domains: &[RuleDomain::React, RuleDomain::Qwik],
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ use biome_rule_options::no_react_specific_props::NoReactSpecificPropsOptions;
declare_lint_rule! {
/// Prevents React-specific JSX properties from being used.
///
/// This rule is intended for use in JSX-based frameworks (mainly **Solid.js**)
/// that do not use React-style prop names.
/// This rule is intended for use in JSX-based frameworks (such as Qwik, Solid, etc.) that do not use React-style prop names.
///
/// ## Examples
///
Expand All @@ -37,7 +36,7 @@ declare_lint_rule! {
recommended: true,
severity: Severity::Warning,
fix_kind: FixKind::Safe,
domains: &[RuleDomain::Solid],
domains: &[RuleDomain::Solid, RuleDomain::Qwik],
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,28 @@ data.map((x) => {
return <div>{x.value}</div> // no key
}
})


import { component$ } from "@builder.io/qwik";

export default component$(() => {
const items = ["apple", "banana", "cherry"];

return (
<div>
{items.map(item => <li>{item}</li>)}

{items.forEach(item => <span>{item}</span>)}

{items.filter(item => item.length > 5).map(item => <div>{item}</div>)}

{items.map(category =>
category.split('').map(letter => <span>{letter}</span>)
)}

{Object.keys({a: 1, b: 2}).map(key => <div>{key}</div>)}

{[1, 2, 3].map(num => <p>{num}</p>)}
</div>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,31 @@ data.map((x) => {
}
})


import { component$ } from "@builder.io/qwik";

export default component$(() => {
const items = ["apple", "banana", "cherry"];

return (
<div>
{items.map(item => <li>{item}</li>)}

{items.forEach(item => <span>{item}</span>)}

{items.filter(item => item.length > 5).map(item => <div>{item}</div>)}

{items.map(category =>
category.split('').map(letter => <span>{letter}</span>)
)}

{Object.keys({a: 1, b: 2}).map(key => <div>{key}</div>)}

{[1, 2, 3].map(num => <p>{num}</p>)}
</div>
);
});

```

# Diagnostics
Expand Down Expand Up @@ -884,3 +909,116 @@ invalid.jsx:99:11 lint/correctness/useJsxKeyInIterable ━━━━━━━━


```

```
invalid.jsx:111:26 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Missing key property for this element in iterable.

109 │ return (
110 │ <div>
> 111 │ {items.map(item => <li>{item}</li>)}
│ ^^^^
112 │
113 │ {items.forEach(item => <span>{item}</span>)}

i The order of the items may change, and having a key can help React identify which item was moved.

i Check the React documentation.


```

```
invalid.jsx:113:30 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Missing key property for this element in iterable.

111 │ {items.map(item => <li>{item}</li>)}
112 │
> 113 │ {items.forEach(item => <span>{item}</span>)}
│ ^^^^^^
114 │
115 │ {items.filter(item => item.length > 5).map(item => <div>{item}</div>)}

i The order of the items may change, and having a key can help React identify which item was moved.

i Check the React documentation.


```

```
invalid.jsx:115:58 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Missing key property for this element in iterable.

113 │ {items.forEach(item => <span>{item}</span>)}
114 │
> 115 │ {items.filter(item => item.length > 5).map(item => <div>{item}</div>)}
│ ^^^^^
116 │
117 │ {items.map(category =>·

i The order of the items may change, and having a key can help React identify which item was moved.

i Check the React documentation.


```

```
invalid.jsx:118:42 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Missing key property for this element in iterable.

117 │ {items.map(category =>·
> 118 │ category.split('').map(letter => <span>{letter}</span>)
│ ^^^^^^
119 │ )}
120 │

i The order of the items may change, and having a key can help React identify which item was moved.

i Check the React documentation.


```

```
invalid.jsx:121:45 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Missing key property for this element in iterable.

119 │ )}
120 │
> 121 │ {Object.keys({a: 1, b: 2}).map(key => <div>{key}</div>)}
│ ^^^^^
122 │
123 │ {[1, 2, 3].map(num => <p>{num}</p>)}

i The order of the items may change, and having a key can help React identify which item was moved.

i Check the React documentation.


```

```
invalid.jsx:123:29 lint/correctness/useJsxKeyInIterable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Missing key property for this element in iterable.

121 │ {Object.keys({a: 1, b: 2}).map(key => <div>{key}</div>)}
122 │
> 123 │ {[1, 2, 3].map(num => <p>{num}</p>)}
│ ^^^
124 │ </div>
125 │ );

i The order of the items may change, and having a key can help React identify which item was moved.

i Check the React documentation.


```
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* should not generate diagnostics */

import React from "react";

[<Hello key="first" />, <Hello key="second" />, <Hello key="third" />];
Expand Down Expand Up @@ -108,4 +106,47 @@ const Valid = [<>
<p>Test 1</p>
<p>Test 2</p>
<p>Test 3</p>
</>]
</>]


// should not generate diagnostics
import { component$ } from "@builder.io/qwik";

export default component$(() => {
const items = ["apple", "banana", "cherry"];
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" }
];

return (
<div>
{items.map((item, index) => <li key={index}>{item}</li>)}

{users.map(user => <div key={user.id}>{user.name}</div>)}

{items.map(item => <span key={`item-${item}`}>{item}</span>)}

{items.filter(item => item.length > 5).map((item, index) =>
<div key={`filtered-${index}`}>{item}</div>
)}

{items.map((category, catIndex) =>
category.split('').map((letter, letterIndex) =>
<span key={`${catIndex}-${letterIndex}`}>{letter}</span>
)
)}

{Object.keys({a: 1, b: 2}).map((key, index) =>
<div key={`obj-${index}`}>{key}</div>
)}

{[1, 2, 3].map((num, index) => <p key={num}>{num}</p>)}

<div>Static content</div>

{items.map(item => item.toUpperCase())}
</div>
);
});
Loading
Loading