Skip to content

feat(react/js): add noComponentHookFactories#9916

Merged
dyc3 merged 4 commits intobiomejs:mainfrom
Jayllyz:feat/react-component-factories
Apr 11, 2026
Merged

feat(react/js): add noComponentHookFactories#9916
dyc3 merged 4 commits intobiomejs:mainfrom
Jayllyz:feat/react-component-factories

Conversation

@Jayllyz
Copy link
Copy Markdown
Contributor

@Jayllyz Jayllyz commented Apr 11, 2026

Code wrote by Claude code Opus 4.6, with Biome skills used.

Summary

Implement this react rule

Test Plan

New tests

Docs

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 11, 2026

🦋 Changeset detected

Latest commit: 7055b16

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
@biomejs/biome Patch
@biomejs/cli-win32-x64 Patch
@biomejs/cli-win32-arm64 Patch
@biomejs/cli-darwin-x64 Patch
@biomejs/cli-darwin-arm64 Patch
@biomejs/cli-linux-x64 Patch
@biomejs/cli-linux-arm64 Patch
@biomejs/cli-linux-x64-musl Patch
@biomejs/cli-linux-arm64-musl Patch
@biomejs/wasm-web Patch
@biomejs/wasm-bundler Patch
@biomejs/wasm-nodejs Patch
@biomejs/backend-jsonrpc Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added A-Project Area: project A-Linter Area: linter L-JavaScript Language: JavaScript and super languages A-Diagnostic Area: diagnostocis labels Apr 11, 2026
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 11, 2026

Merging this PR will not alter performance

✅ 58 untouched benchmarks
⏩ 196 skipped benchmarks1


Comparing Jayllyz:feat/react-component-factories (7055b16) with main (0d0e611)

Open in CodSpeed

Footnotes

  1. 196 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@github-actions github-actions Bot added the A-CLI Area: CLI label Apr 11, 2026
@Jayllyz Jayllyz marked this pull request as ready for review April 11, 2026 08:08
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 11, 2026

Walkthrough

Adds a new nursery lint rule noComponentHookFactories to Biome: implements NoComponentHookFactories (detects React components/hooks created inside functions), exports an AnyComponentLikeDeclaration union and a RuleState enum, and registers rule options with a new NoComponentHookFactoriesOptions struct. Adds a changeset entry and test fixtures (valid.jsx, invalid.jsx) exercising allowed module-level patterns and disallowed in-function factories. No other public APIs were modified.

Suggested reviewers

  • ematipico
  • Netail
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarises the main change: adding a new noComponentHookFactories React linting rule to the codebase.
Description check ✅ Passed The description explains the motivation (implementing a React rule), references relevant external guidelines, and mentions the test plan and AI assistance disclosure.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/biome_js_analyze/src/lint/nursery/no_component_hook_factories.rs (1)

230-272: Consider extending test mock detection for additional frameworks.

The current implementation supports vi.mock and jest.mock. You might want to consider whether other common patterns like sinon.stub, td.replace (testdouble), or Bun's mock.module would benefit from similar treatment. Not blocking, as the current coverage handles the most common cases.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/lint/nursery/no_component_hook_factories.rs`
around lines 230 - 272, is_inside_test_mock_callback currently only recognizes
calls where the member name token equals "mock" and the object identifier equals
"vi" or "jest"; extend its detection to include other common test utilities by
matching additional object+method combinations (e.g., object "sinon" with method
"stub", object "td" with method "replace", and Bun's "mock" module such as
object "mock" with method "module" or similar). Update the checks around
method_token.text_trimmed() and object_token.text_trimmed() inside
is_inside_test_mock_callback to accept those extra pairs (or a small whitelist
map of allowed object→method names) so the function returns true for those
frameworks as well while preserving existing early-returns and structure.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/biome_js_analyze/src/lint/nursery/no_component_hook_factories.rs`:
- Around line 230-272: is_inside_test_mock_callback currently only recognizes
calls where the member name token equals "mock" and the object identifier equals
"vi" or "jest"; extend its detection to include other common test utilities by
matching additional object+method combinations (e.g., object "sinon" with method
"stub", object "td" with method "replace", and Bun's "mock" module such as
object "mock" with method "module" or similar). Update the checks around
method_token.text_trimmed() and object_token.text_trimmed() inside
is_inside_test_mock_callback to accept those extra pairs (or a small whitelist
map of allowed object→method names) so the function returns true for those
frameworks as well while preserving existing early-returns and structure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7b433763-9e95-4480-b24e-e8c5285c4609

📥 Commits

Reviewing files that changed from the base of the PR and between 4d9ac51 and 4c99f6f.

⛔ Files ignored due to path filters (9)
  • crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs is excluded by !**/migrate/eslint_any_rule_to_biome.rs and included by **
  • crates/biome_configuration/src/analyzer/linter/rules.rs is excluded by !**/rules.rs and included by **
  • crates/biome_configuration/src/generated/domain_selector.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_configuration/src/generated/linter_options_check.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_diagnostics_categories/src/categories.rs is excluded by !**/categories.rs and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noComponentHookFactories/invalid.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noComponentHookFactories/valid.jsx.snap is excluded by !**/*.snap and included by **
  • packages/@biomejs/backend-jsonrpc/src/workspace.ts is excluded by !**/backend-jsonrpc/src/workspace.ts and included by **
  • packages/@biomejs/biome/configuration_schema.json is excluded by !**/configuration_schema.json and included by **
📒 Files selected for processing (6)
  • .changeset/dry-dryers-flash.md
  • crates/biome_js_analyze/src/lint/nursery/no_component_hook_factories.rs
  • crates/biome_js_analyze/tests/specs/nursery/noComponentHookFactories/invalid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noComponentHookFactories/valid.jsx
  • crates/biome_rule_options/src/lib.rs
  • crates/biome_rule_options/src/no_component_hook_factories.rs

Copy link
Copy Markdown
Contributor

@dyc3 dyc3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Just a couple notes.

Comment thread crates/biome_js_analyze/src/lint/nursery/no_component_hook_factories.rs Outdated
Comment thread crates/biome_js_analyze/src/lint/nursery/no_component_hook_factories.rs Outdated
Comment thread crates/biome_js_analyze/src/lint/nursery/no_component_hook_factories.rs Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
crates/biome_js_analyze/src/lint/nursery/no_component_hook_factories.rs (2)

239-259: Minor semantic inconsistency in return type usage.

is_test_mock_call returns None when the method isn't "mock" (line 248) but returns Some(false) when the object isn't vi/jest (line 258). Both are treated identically by the caller via unwrap_or(false), so no bug here—just a minor readability nit.

♻️ Optional: consistent return semantics
 fn is_test_mock_call(ancestor: &JsSyntaxNode) -> Option<bool> {
     let call_expr = JsCallExpression::cast_ref(ancestor)?;
     let member = call_expr
         .callee()
         .ok()?
         .as_js_static_member_expression()?
         .clone();
     let method_token = member.member().ok()?.as_js_name()?.value_token().ok()?;
     if method_token.text_trimmed() != "mock" {
-        return None;
+        return Some(false);
     }
     let object_token = member
         .object()
         .ok()?
         .as_js_identifier_expression()?
         .name()
         .ok()?
         .value_token()
         .ok()?;
     Some(matches!(object_token.text_trimmed(), "vi" | "jest"))
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/lint/nursery/no_component_hook_factories.rs`
around lines 239 - 259, The helper is_test_mock_call has inconsistent semantics:
it returns None when the callee method isn't "mock" but returns Some(false) when
the object isn't "vi"/"jest"; change the early exit in is_test_mock_call (the
branch that currently returns None when method_token.text_trimmed() != "mock")
to return Some(false) so the function consistently returns Some(bool) and
callers can rely on the boolean result without treating None specially.

293-299: Heuristic is reasonable but worth documenting.

Checking only the first character catches the common HOC parameter patterns (WrappedComponent, Component), but would also match less conventional names like URL or ID. This is probably fine in practice; a brief comment noting the trade-off could help future readers.

📝 Optional: add clarifying comment
     Some(
+        // PascalCase heuristic: first char uppercase. May match acronyms (URL, ID),
+        // but these are rare as component parameter names.
         name_token
             .text_trimmed()
             .chars()
             .next()
             .is_some_and(char::is_uppercase),
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/lint/nursery/no_component_hook_factories.rs`
around lines 293 - 299, The heuristic that checks only the first character of
name_token (the
Some(name_token.text_trimmed().chars().next().is_some_and(char::is_uppercase)
expression) can misclassify all-uppercase identifiers like URL or ID; add a
concise inline comment above this expression explaining the trade-off (we
intentionally only inspect the first character to catch common HOC param names
like WrappedComponent/Component while accepting rare false positives such as
URL/ID) so future readers understand the reasoning and limitations of the
heuristic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/biome_js_analyze/src/lint/nursery/no_component_hook_factories.rs`:
- Around line 239-259: The helper is_test_mock_call has inconsistent semantics:
it returns None when the callee method isn't "mock" but returns Some(false) when
the object isn't "vi"/"jest"; change the early exit in is_test_mock_call (the
branch that currently returns None when method_token.text_trimmed() != "mock")
to return Some(false) so the function consistently returns Some(bool) and
callers can rely on the boolean result without treating None specially.
- Around line 293-299: The heuristic that checks only the first character of
name_token (the
Some(name_token.text_trimmed().chars().next().is_some_and(char::is_uppercase)
expression) can misclassify all-uppercase identifiers like URL or ID; add a
concise inline comment above this expression explaining the trade-off (we
intentionally only inspect the first character to catch common HOC param names
like WrappedComponent/Component while accepting rare false positives such as
URL/ID) so future readers understand the reasoning and limitations of the
heuristic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f488339f-e30d-494b-aaae-bfbbe24ef45d

📥 Commits

Reviewing files that changed from the base of the PR and between 4c99f6f and 903ea9c.

📒 Files selected for processing (1)
  • crates/biome_js_analyze/src/lint/nursery/no_component_hook_factories.rs

@dyc3 dyc3 force-pushed the feat/react-component-factories branch from 8dafcb7 to 7055b16 Compare April 11, 2026 17:08
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/biome_js_analyze/src/lint/nursery/no_component_hook_factories.rs (1)

280-300: Minor: Unnecessary .clone() on line 285.

The binding() method takes &self, so you can call it directly on the reference from as_js_formal_parameter() without cloning.

♻️ Suggested simplification
 fn is_pascal_case_param(param: SyntaxResult<AnyJsParameter>) -> Option<bool> {
-    let formal = param
+    let name_token = param
         .ok()?
         .as_any_js_formal_parameter()?
-        .as_js_formal_parameter()?
-        .clone();
-    let name_token = formal
+        .as_js_formal_parameter()?
         .binding()
         .ok()?
         .as_any_js_binding()?
         .as_js_identifier_binding()?
         .name_token()
         .ok()?;
     Some(
         name_token
             .text_trimmed()
             .chars()
             .next()
             .is_some_and(char::is_uppercase),
     )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/lint/nursery/no_component_hook_factories.rs`
around lines 280 - 300, The helper function is_pascal_case_param unnecessarily
calls .clone() on the result of as_js_formal_parameter(); remove the .clone()
and call binding() directly on the borrowed formal parameter (i.e., use the
reference returned by as_js_formal_parameter() rather than cloning it). Update
is_pascal_case_param so the chain uses the borrowed value from
as_js_formal_parameter() before calling binding(), preserving the rest of the
unwrap/ok logic (keep names: is_pascal_case_param, as_js_formal_parameter,
binding, name_token).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/biome_js_analyze/src/lint/nursery/no_component_hook_factories.rs`:
- Around line 280-300: The helper function is_pascal_case_param unnecessarily
calls .clone() on the result of as_js_formal_parameter(); remove the .clone()
and call binding() directly on the borrowed formal parameter (i.e., use the
reference returned by as_js_formal_parameter() rather than cloning it). Update
is_pascal_case_param so the chain uses the borrowed value from
as_js_formal_parameter() before calling binding(), preserving the rest of the
unwrap/ok logic (keep names: is_pascal_case_param, as_js_formal_parameter,
binding, name_token).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 736fe590-e938-48b8-a607-11319ed4e1af

📥 Commits

Reviewing files that changed from the base of the PR and between 903ea9c and 7055b16.

⛔ Files ignored due to path filters (9)
  • crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs is excluded by !**/migrate/eslint_any_rule_to_biome.rs and included by **
  • crates/biome_configuration/src/analyzer/linter/rules.rs is excluded by !**/rules.rs and included by **
  • crates/biome_configuration/src/generated/domain_selector.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_configuration/src/generated/linter_options_check.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_diagnostics_categories/src/categories.rs is excluded by !**/categories.rs and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noComponentHookFactories/invalid.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noComponentHookFactories/valid.jsx.snap is excluded by !**/*.snap and included by **
  • packages/@biomejs/backend-jsonrpc/src/workspace.ts is excluded by !**/backend-jsonrpc/src/workspace.ts and included by **
  • packages/@biomejs/biome/configuration_schema.json is excluded by !**/configuration_schema.json and included by **
📒 Files selected for processing (6)
  • .changeset/dry-dryers-flash.md
  • crates/biome_js_analyze/src/lint/nursery/no_component_hook_factories.rs
  • crates/biome_js_analyze/tests/specs/nursery/noComponentHookFactories/invalid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noComponentHookFactories/valid.jsx
  • crates/biome_rule_options/src/lib.rs
  • crates/biome_rule_options/src/no_component_hook_factories.rs
✅ Files skipped from review due to trivial changes (5)
  • crates/biome_rule_options/src/lib.rs
  • .changeset/dry-dryers-flash.md
  • crates/biome_rule_options/src/no_component_hook_factories.rs
  • crates/biome_js_analyze/tests/specs/nursery/noComponentHookFactories/invalid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noComponentHookFactories/valid.jsx

@dyc3 dyc3 enabled auto-merge (squash) April 11, 2026 17:25
@dyc3 dyc3 merged commit 27dd7b1 into biomejs:main Apr 11, 2026
21 checks passed
@github-actions github-actions Bot mentioned this pull request Apr 11, 2026
@Jayllyz Jayllyz deleted the feat/react-component-factories branch April 11, 2026 17:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-CLI Area: CLI A-Diagnostic Area: diagnostocis A-Linter Area: linter A-Project Area: project L-JavaScript Language: JavaScript and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants