Skip to content

Commit

Permalink
feat(useGuardForIn): add rule (#4104)
Browse files Browse the repository at this point in the history
  • Loading branch information
fireairforce authored Oct 10, 2024
1 parent 2e5b656 commit 970f498
Show file tree
Hide file tree
Showing 13 changed files with 341 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ our [guidelines for writing a good changelog entry](https:/biomejs/b
#### New features

- Add [noDocumentCookie](https://biomejs.dev/linter/rules/no-document-cookie/). Contributed by @tunamaguro
- Add [guardForIn](https://biomejs.dev/linter/rules/guard-for-in/). Contributed by @fireairforce

#### Bug Fixes

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 30 additions & 11 deletions crates/biome_configuration/src/analyzer/linter/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3403,6 +3403,9 @@ pub struct Nursery {
#[serde(skip_serializing_if = "Option::is_none")]
pub use_explicit_function_return_type:
Option<RuleConfiguration<biome_js_analyze::options::UseExplicitFunctionReturnType>>,
#[doc = "Require for-in loops to include an if statement."]
#[serde(skip_serializing_if = "Option::is_none")]
pub use_guard_for_in: Option<RuleConfiguration<biome_js_analyze::options::UseGuardForIn>>,
#[doc = "Disallows package private imports."]
#[serde(skip_serializing_if = "Option::is_none")]
pub use_import_restrictions:
Expand Down Expand Up @@ -3476,6 +3479,7 @@ impl Nursery {
"useConsistentMemberAccessibility",
"useDeprecatedReason",
"useExplicitFunctionReturnType",
"useGuardForIn",
"useImportRestrictions",
"useSortedClasses",
"useStrictMode",
Expand Down Expand Up @@ -3510,7 +3514,7 @@ impl Nursery {
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39]),
];
const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]),
Expand Down Expand Up @@ -3554,6 +3558,7 @@ impl Nursery {
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41]),
];
#[doc = r" Retrieves the recommended rules"]
pub(crate) fn is_recommended_true(&self) -> bool {
Expand Down Expand Up @@ -3750,31 +3755,36 @@ impl Nursery {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35]));
}
}
if let Some(rule) = self.use_import_restrictions.as_ref() {
if let Some(rule) = self.use_guard_for_in.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36]));
}
}
if let Some(rule) = self.use_sorted_classes.as_ref() {
if let Some(rule) = self.use_import_restrictions.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37]));
}
}
if let Some(rule) = self.use_strict_mode.as_ref() {
if let Some(rule) = self.use_sorted_classes.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38]));
}
}
if let Some(rule) = self.use_trim_start_end.as_ref() {
if let Some(rule) = self.use_strict_mode.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39]));
}
}
if let Some(rule) = self.use_valid_autocomplete.as_ref() {
if let Some(rule) = self.use_trim_start_end.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40]));
}
}
if let Some(rule) = self.use_valid_autocomplete.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41]));
}
}
index_set
}
pub(crate) fn get_disabled_rules(&self) -> FxHashSet<RuleFilter<'static>> {
Expand Down Expand Up @@ -3959,31 +3969,36 @@ impl Nursery {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35]));
}
}
if let Some(rule) = self.use_import_restrictions.as_ref() {
if let Some(rule) = self.use_guard_for_in.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36]));
}
}
if let Some(rule) = self.use_sorted_classes.as_ref() {
if let Some(rule) = self.use_import_restrictions.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37]));
}
}
if let Some(rule) = self.use_strict_mode.as_ref() {
if let Some(rule) = self.use_sorted_classes.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38]));
}
}
if let Some(rule) = self.use_trim_start_end.as_ref() {
if let Some(rule) = self.use_strict_mode.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39]));
}
}
if let Some(rule) = self.use_valid_autocomplete.as_ref() {
if let Some(rule) = self.use_trim_start_end.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40]));
}
}
if let Some(rule) = self.use_valid_autocomplete.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41]));
}
}
index_set
}
#[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"]
Expand Down Expand Up @@ -4164,6 +4179,10 @@ impl Nursery {
.use_explicit_function_return_type
.as_ref()
.map(|conf| (conf.level(), conf.get_options())),
"useGuardForIn" => self
.use_guard_for_in
.as_ref()
.map(|conf| (conf.level(), conf.get_options())),
"useImportRestrictions" => self
.use_import_restrictions
.as_ref()
Expand Down
1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ define_categories! {
"lint/nursery/useConsistentMemberAccessibility": "https://biomejs.dev/linter/rules/use-consistent-member-accessibility",
"lint/nursery/useDeprecatedReason": "https://biomejs.dev/linter/rules/use-deprecated-reason",
"lint/nursery/useExplicitFunctionReturnType": "https://biomejs.dev/linter/rules/use-explicit-function-return-type",
"lint/nursery/useGuardForIn": "https://biomejs.dev/linter/rules/use-guard-for-in",
"lint/nursery/useImportRestrictions": "https://biomejs.dev/linter/rules/use-import-restrictions",
"lint/nursery/useJsxCurlyBraceConvention": "https://biomejs.dev/linter/rules/use-jsx-curly-brace-convention",
"lint/nursery/useSortedClasses": "https://biomejs.dev/linter/rules/use-sorted-classes",
Expand Down
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub mod use_component_export_only_modules;
pub mod use_consistent_curly_braces;
pub mod use_consistent_member_accessibility;
pub mod use_explicit_function_return_type;
pub mod use_guard_for_in;
pub mod use_import_restrictions;
pub mod use_sorted_classes;
pub mod use_strict_mode;
Expand Down Expand Up @@ -66,6 +67,7 @@ declare_lint_group! {
self :: use_consistent_curly_braces :: UseConsistentCurlyBraces ,
self :: use_consistent_member_accessibility :: UseConsistentMemberAccessibility ,
self :: use_explicit_function_return_type :: UseExplicitFunctionReturnType ,
self :: use_guard_for_in :: UseGuardForIn ,
self :: use_import_restrictions :: UseImportRestrictions ,
self :: use_sorted_classes :: UseSortedClasses ,
self :: use_strict_mode :: UseStrictMode ,
Expand Down
136 changes: 136 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery/use_guard_for_in.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
use biome_analyze::{
context::RuleContext, declare_lint_rule, Ast, Rule, RuleDiagnostic, RuleSource,
};
use biome_console::markup;
use biome_js_syntax::{AnyJsStatement, JsForInStatement};
use biome_rowan::{AstNode, AstNodeList};

declare_lint_rule! {
/// Require `for-in` loops to include an `if` statement.
///
/// Looping over objects with a `for-in` loop will include properties inherited through the prototype chain.
/// This behavior can lead to unexpected items in your for loop.
///
/// For codebases that do not support ES2022, `Object.prototype.hasOwnProperty.call(foo, key)` can be used as a check that the property is not inherited.
///
/// For codebases that do support ES2022, `Object.hasOwn(foo, key)` can be used as a shorter and more reliable alternative.
///
/// ## Examples
///
/// ### Invalid
///
/// ```js,expect_diagnostic
/// for (key in foo) {
/// doSomething(key);
/// }
/// ```
///
/// ### Valid
///
/// ```js
/// for (key in foo) {
/// if (Object.hasOwn(foo, key)) {
/// doSomething(key);
/// }
/// }
/// ```
///
/// ```js
/// for (key in foo) {
/// if (Object.prototype.hasOwnProperty.call(foo, key)) {
/// doSomething(key);
/// }
/// }
/// ```
///
/// ```js
/// for (key in foo) {
/// if ({}.hasOwnProperty.call(foo, key)) {
/// doSomething(key);
/// }
/// }
/// ```
///
pub UseGuardForIn {
version: "next",
name: "useGuardForIn",
language: "js",
sources: &[RuleSource::Eslint("guard-for-in")],
recommended: false,
}
}

impl Rule for UseGuardForIn {
type Query = Ast<JsForInStatement>;
type State = ();
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();
let body = node.body().ok()?;

match body {
AnyJsStatement::JsEmptyStatement(_) | AnyJsStatement::JsIfStatement(_) => None,
AnyJsStatement::JsBlockStatement(block) => {
let statements = block.statements();

match statements.len() {
0 => None,
1 => {
let first_statement = statements.first()?;
if first_statement.as_js_if_statement().is_none() {
Some(())
} else {
None
}
}
_ => {
let first_statement = statements.first()?;
if let Some(first_if_statement) = first_statement.as_js_if_statement() {
match first_if_statement.consequent().ok()? {
AnyJsStatement::JsBlockStatement(block)
if block.statements().len() == 1 =>
{
if block
.statements()
.first()?
.as_js_continue_statement()
.is_none()
{
Some(())
} else {
None
}
}
AnyJsStatement::JsContinueStatement(_) => None,
_ => Some(()),
}
} else {
Some(())
}
}
}
}
_ => Some(()),
}
}

fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
let node = ctx.query();
Some(
RuleDiagnostic::new(
rule_category!(),
node.range(),
markup! {
"The body of a for-in should be wrapped in an `if` statement."
},
)
.note(markup! {
"Looping over the object with for-in loop will include properties that are inherited through the prototype chain, the behaviour can lead to some unexpected items in your loop."
}).note(markup! {
"To resolve this issue, add an if statement like `if (Object.hasOwn(foo, key)) {...}` to filter out the extraneous properties. "
}),
)
}
}
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,6 @@
for (var x in o) { if (x) { f(); continue; } g(); }
for (var x in o) { if (x) { continue; f(); } g(); }
for (var x in o) { if (x) { f(); } g(); }
for (var x in o) { if (x) f(); g(); }
for (var x in o) { foo() }
for (var x in o) foo();
Loading

0 comments on commit 970f498

Please sign in to comment.