Skip to content

Commit e7b3b10

Browse files
ViniciusDev26claudeautofix-ci[bot]
authored
feat(lint): add noDrizzleDeleteWithoutWhere and noDrizzleUpdateWithoutWhere rules (#9525)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 2f8bf80 commit e7b3b10

30 files changed

Lines changed: 868 additions & 1 deletion

File tree

.changeset/drizzle-update-rule.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the rule [`noDrizzleUpdateWithoutWhere`](https://biomejs.dev/linter/rules/no-drizzle-update-without-where/) to prevent accidental full-table updates when using Drizzle ORM without a `.where()` clause.

.changeset/wide-symbols-post.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the rule [`noDrizzleDeleteWithoutWhere`](https://biomejs.dev/linter/rules/no-drizzle-delete-without-where/) to prevent accidental full-table deletes when using Drizzle ORM without a `.where()` clause.

crates/biome_analyze/src/rule.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ pub enum RuleSource<'a> {
188188
EslintYml(&'a str),
189189
/// Rules from [Eslint CSS](https://github.com/eslint/css)
190190
EslintCss(&'a str),
191+
/// Rules from [Eslint Plugin Drizzle](https://orm.drizzle.team/docs/eslint-plugin)
192+
EslintDrizzle(&'a str),
191193
}
192194

193195
impl<'a> std::fmt::Display for RuleSource<'a> {
@@ -242,6 +244,7 @@ impl<'a> std::fmt::Display for RuleSource<'a> {
242244
Self::EslintMarkdown(_) => write!(f, "@eslint/markdown"),
243245
Self::EslintYml(_) => write!(f, "eslint-plugin-yml"),
244246
Self::EslintCss(_) => write!(f, "@eslint/css"),
247+
Self::EslintDrizzle(_) => write!(f, "eslint-plugin-drizzle"),
245248
}
246249
}
247250
}
@@ -307,6 +310,7 @@ impl<'a> RuleSource<'a> {
307310
Self::EslintMarkdown(_) => 42,
308311
Self::EslintYml(_) => 43,
309312
Self::EslintCss(_) => 44,
313+
Self::EslintDrizzle(_) => 45,
310314
}
311315
}
312316

@@ -370,7 +374,8 @@ impl<'a> RuleSource<'a> {
370374
| Self::EslintPlaywright(rule_name)
371375
| Self::EslintJson(rule_name)
372376
| Self::EslintMarkdown(rule_name)
373-
| Self::EslintYml(rule_name) => rule_name,
377+
| Self::EslintYml(rule_name)
378+
| Self::EslintDrizzle(rule_name) => rule_name,
374379
}
375380
}
376381

@@ -421,6 +426,7 @@ impl<'a> RuleSource<'a> {
421426
Self::EslintMarkdown(_) => "markdown",
422427
Self::EslintYml(_) => "yml",
423428
Self::EslintCss(_) => "css",
429+
Self::EslintDrizzle(_) => "drizzle",
424430
}
425431
}
426432

@@ -479,6 +485,7 @@ impl<'a> RuleSource<'a> {
479485
Self::EslintMarkdown(rule_name) => format!("https://github.com/eslint/markdown/blob/main/docs/rules/{rule_name}.md"),
480486
Self::EslintYml(rule_name) => format!("https://ota-meshi.github.io/eslint-plugin-yml/rules/{rule_name}.html"),
481487
Self::EslintCss(rule_name) => format!("https://github.com/eslint/css/blob/main/docs/rules/{rule_name}.md"),
488+
Self::EslintDrizzle(rule_name) => format!("https://orm.drizzle.team/docs/eslint-plugin#{rule_name}"),
482489
}
483490
}
484491

@@ -566,6 +573,8 @@ impl RuleSourceKind {
566573
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
567574
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
568575
pub enum RuleDomain {
576+
/// Drizzle ORM rules
577+
Drizzle,
569578
/// React library rules
570579
React,
571580
/// Testing rules
@@ -594,6 +603,7 @@ impl Display for RuleDomain {
594603
fn fmt(&self, fmt: &mut Formatter) -> std::io::Result<()> {
595604
// use lower case naming, it needs to match the name of the configuration
596605
match self {
606+
Self::Drizzle => fmt.write_str("drizzle"),
597607
Self::React => fmt.write_str("react"),
598608
Self::Test => fmt.write_str("test"),
599609
Self::Solid => fmt.write_str("solid"),
@@ -647,6 +657,7 @@ impl RuleDomain {
647657
Self::Turborepo => &[&("turbo", ">=1.0.0")],
648658
Self::Playwright => &[&("@playwright/test", ">=1.0.0")],
649659
Self::Types => &[],
660+
Self::Drizzle => &[&("drizzle-orm", ">=0.9.0")],
650661
}
651662
}
652663

@@ -687,6 +698,7 @@ impl RuleDomain {
687698
Self::Turborepo => &[],
688699
Self::Playwright => &["test", "expect"],
689700
Self::Types => &[],
701+
Self::Drizzle => &[],
690702
}
691703
}
692704

@@ -703,6 +715,7 @@ impl RuleDomain {
703715
Self::Turborepo => "turborepo",
704716
Self::Playwright => "playwright",
705717
Self::Types => "types",
718+
Self::Drizzle => "drizzle",
706719
}
707720
}
708721
}
@@ -723,6 +736,7 @@ impl FromStr for RuleDomain {
723736
"turborepo" => Ok(Self::Turborepo),
724737
"playwright" => Ok(Self::Playwright),
725738
"types" => Ok(Self::Types),
739+
"drizzle" => Ok(Self::Drizzle),
726740
_ => Err("Invalid rule domain"),
727741
}
728742
}

crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/src/analyzer/linter/rules.rs

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/src/generated/domain_selector.rs

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/src/generated/linter_options_check.rs

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/tests/invalid/domains.json.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ domains.json:4:7 deserialize ━━━━━━━━━━━━━━━━━
1515

1616
i Accepted values:
1717

18+
- drizzle
1819
- react
1920
- test
2021
- solid

crates/biome_diagnostics_categories/src/categories.rs

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use biome_js_syntax::{AnyJsExpression, JsCallExpression, JsStaticMemberExpression, JsSyntaxKind};
2+
use biome_rowan::{AstNode, SyntaxNode};
3+
4+
pub(crate) fn get_identifier_name(expr: &AnyJsExpression) -> Option<biome_rowan::TokenText> {
5+
match expr {
6+
AnyJsExpression::JsIdentifierExpression(id) => {
7+
Some(id.name().ok()?.value_token().ok()?.token_text_trimmed())
8+
}
9+
_ => None,
10+
}
11+
}
12+
13+
pub(crate) fn has_where_in_chain(node: &SyntaxNode<biome_js_syntax::JsLanguage>) -> bool {
14+
let mut current = node.parent();
15+
loop {
16+
let Some(parent) = current else { break };
17+
18+
if let Some(member_expr) = JsStaticMemberExpression::cast_ref(&parent)
19+
&& let Ok(member) = member_expr.member()
20+
&& let Some(name) = member.as_js_name()
21+
&& name
22+
.value_token()
23+
.ok()
24+
.is_some_and(|t| t.token_text_trimmed() == "where")
25+
{
26+
// Only count `.where(...)` as a where clause, not bare `.where` property access.
27+
let is_called = parent
28+
.parent()
29+
.is_some_and(|p| JsCallExpression::cast_ref(&p).is_some());
30+
if is_called {
31+
return true;
32+
}
33+
}
34+
35+
if matches!(
36+
parent.kind(),
37+
JsSyntaxKind::JS_EXPRESSION_STATEMENT | JsSyntaxKind::JS_RETURN_STATEMENT
38+
) {
39+
break;
40+
}
41+
42+
current = parent.parent();
43+
}
44+
false
45+
}

0 commit comments

Comments
 (0)