Skip to content

Conversation

@sgarcialaguna
Copy link
Contributor

Summary

Implement react/jsx-no-bind in Biome

#4637

One important case is still missing

function onClick() { console.log('Hello!'); }
<Foo onClick={onClick} />

I would need some help for this as I need to track down where the function was declared and whether it is stable

Test Plan

I tested all the examples for correct and incorrect code given in

https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-bind.md

@changeset-bot
Copy link

changeset-bot bot commented Sep 5, 2025

🦋 Changeset detected

Latest commit: 52fd7cb

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 5, 2025

Walkthrough

Adds a new nursery lint rule noJsxPropsBind that flags inline JSX prop bindings (arrow functions, function expressions and .bind() calls) by semantically analysing JSXAttribute initialisers. Introduces InvalidKind (ArrowFunction, Function, Bind) and NoJsxPropsBindState (with attribute range), implements Rule for Semantic<JsxAttribute> and returns state when a problematic initializer is found. Emits kind-specific diagnostics with notes and suggests extracting the function or using useCallback. Adds options stubs, tests (valid/invalid) and changelog entry.

Suggested reviewers

  • ematipico
  • dyc3

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly matches the main change: implementing the no-jsx-props-bind lint rule.
Description check ✅ Passed The description clearly relates to the changeset, explaining the react/jsx-no-bind implementation with test coverage and known limitations.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 086f6ac and 9f23288.

⛔ Files ignored due to path filters (1)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/valid.jsx.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (1)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/valid.jsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/valid.jsx

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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 Sep 5, 2025
@sgarcialaguna sgarcialaguna changed the title Nojsxpropsbind Feat: Nojsxpropsbind Sep 5, 2025
@sgarcialaguna sgarcialaguna changed the title Feat: Nojsxpropsbind feat: Nojsxpropsbind Sep 5, 2025
@sgarcialaguna sgarcialaguna changed the title feat: Nojsxpropsbind feat: no-jsx-props-bind Sep 5, 2025
Copy link
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.

Actionable comments posted: 3

🧹 Nitpick comments (11)
crates/biome_rule_options/src/no_jsx_props_bind.rs (1)

6-12: Add brief field docs for schema consumers

Tiny win: add rustdoc comments per field to improve generated schema/help text.

crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowBind.jsx (1)

1-3: Good coverage for allowBind=true

Consider adding a case with partial application to ensure we only relax .bind when configured:

  • onClick={this._handleClick.bind(this, 1)}
crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/valid.jsx (1)

1-3: Add cases to exercise “stable by reference” resolution

To cover the open “declared elsewhere” gap, please add:

  • Valid: function handle() {} …
  • Invalid: function Component(){ const handle = () => {}; return }
    This will guide the implementation that resolves identifiers to their defining scope.
crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowArrowFunctions.jsx (1)

1-3: Nice. Add a couple more arrow samples for breadth

  • Inside map: items.map(i => <Btn onClick={() => doThing(i)} />)
  • TS params: <Btn onClick={(e: MouseEvent) => doThing(e)} />
crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowFunctions.jsx (2)

3-12: Split fixtures to isolate behaviours

This mixes two behaviours: inline function expressions and referencing a named function. Consider separate files (e.g. validAllowFunctions.inline.jsx and valid.functionReference.jsx) so failures pinpoint the precise allowance.


9-12: Add a case for imported/stable references

Since you’re exploring stability of referenced functions, add a case like import { onClick } from "./handler"; <Foo onClick={onClick} /> to lock in expected behaviour for external declarations.

Happy to draft the extra fixture if you confirm desired behaviour (always valid like ESLint, or only when proven stable?).

crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreRefs.jsx (1)

3-10: Good coverage for callback refs; consider adding object ref too

Add a case like const r = React.createRef(); <Foo ref={r} /> (or useRef) to ensure ignoreRefs also covers object refs, not only callback refs.

crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalid.jsx (1)

1-12: Add guard cases to pin expected behaviour

  • Add a valid case onClick={onClick} (declared elsewhere) to capture the “reference is fine” scenario.
  • Add a valid case onClick={foo()} to ensure we do NOT misreport generic call expressions as “.bind()”.

I can draft these fixtures if you’d like.

crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (3)

10-11: Rule summary should mention arrow functions explicitly

Title says “.bind() or function declaration” but the rule also forbids inline arrow functions.

Apply:

-    /// Disallow .bind() or function declaration in JSX props
+    /// Disallow .bind(), inline arrow functions, and function expressions in JSX props

54-78: Minor: be tolerant of non-expression attribute values

You already short-circuit on expression values; consider returning early if the attribute value isn’t an expression container to avoid extra work.


48-53: Future enhancement: flag unstable identifiers (optional, behind a flag)

To cover the “passed-by-reference but unstable” gap:

  • If the value is an identifier or this.method, resolve its binding via ctx.model().
  • If the binding is declared inside the same function component body (and not wrapped in useCallback) or is a function-valued prop created in render, treat it as unstable.
  • Keep this off by default for ESLint parity; gate behind an option (e.g. checkIdentifiers).

I can sketch the symbol-resolution logic if you want to pursue this next.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0ac9adb and d21b4ff.

⛔ Files ignored due to path filters (12)
  • crates/biome_configuration/src/analyzer/linter/rules.rs is excluded by !**/rules.rs and included by **
  • crates/biome_diagnostics_categories/src/categories.rs is excluded by !**/categories.rs and included by **
  • crates/biome_js_analyze/src/lint/nursery.rs is excluded by !**/nursery.rs and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalid.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/valid.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowArrowFunctions.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowBind.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowFunctions.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreDomComponents.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreRefs.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 (15)
  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalid.jsx (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/valid.jsx (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowArrowFunctions.jsx (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowArrowFunctions.options.json (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowBind.jsx (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowBind.options.json (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowFunctions.jsx (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowFunctions.options.json (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreDomComponents.jsx (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreDomComponents.options.json (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreRefs.jsx (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreRefs.options.json (1 hunks)
  • crates/biome_rule_options/src/lib.rs (1 hunks)
  • crates/biome_rule_options/src/no_jsx_props_bind.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{rs,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Format Rust and TOML files before committing (use just f/just format).

Files:

  • crates/biome_rule_options/src/no_jsx_props_bind.rs
  • crates/biome_rule_options/src/lib.rs
  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
crates/biome_*/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place core crates under /crates/biome_*/

Files:

  • crates/biome_rule_options/src/no_jsx_props_bind.rs
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreDomComponents.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowFunctions.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowFunctions.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowArrowFunctions.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreDomComponents.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowBind.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowBind.jsx
  • crates/biome_rule_options/src/lib.rs
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowArrowFunctions.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/valid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreRefs.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreRefs.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalid.jsx
  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
crates/biome_*_{syntax,parser,formatter,analyze,factory,semantic}/**

📄 CodeRabbit inference engine (CLAUDE.md)

Maintain the per-language crate structure: biome_{lang}_{syntax,parser,formatter,analyze,factory,semantic}

Files:

  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreDomComponents.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowFunctions.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowFunctions.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowArrowFunctions.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreDomComponents.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowBind.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowBind.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowArrowFunctions.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/valid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreRefs.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreRefs.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalid.jsx
  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
**/tests/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place test files under a tests/ directory in each crate

Files:

  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreDomComponents.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowFunctions.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowFunctions.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowArrowFunctions.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreDomComponents.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowBind.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowBind.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowArrowFunctions.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/valid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreRefs.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreRefs.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalid.jsx
🧠 Learnings (13)
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_rule_options/lib/**/*.rs : Derive Serialize, Deserialize, Deserializable (and schemars JsonSchema behind feature) on options types; use serde attributes rename_all="camelCase", deny_unknown_fields, default

Applied to files:

  • crates/biome_rule_options/src/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/tests/specs/nursery/** : Place snapshot test cases for new rules under tests/specs/nursery/<ruleName>/ with files prefixed by invalid/valid

Applied to files:

  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreDomComponents.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowFunctions.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowArrowFunctions.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowBind.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowBind.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/valid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreRefs.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreRefs.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalid.jsx
📚 Learning: 2025-08-05T14:43:29.581Z
Learnt from: dyc3
PR: biomejs/biome#7081
File: packages/@biomejs/biome/configuration_schema.json:7765-7781
Timestamp: 2025-08-05T14:43:29.581Z
Learning: The file `packages/biomejs/biome/configuration_schema.json` is auto-generated and should not be manually edited or reviewed for schema issues; any changes should be made at the code generation source.

Applied to files:

  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreDomComponents.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowFunctions.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowArrowFunctions.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowBind.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreRefs.options.json
📚 Learning: 2025-08-11T11:48:27.774Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_formatter/CONTRIBUTING.md:0-0
Timestamp: 2025-08-11T11:48:27.774Z
Learning: Applies to crates/biome_formatter/biome_html_formatter/tests/specs/html/**/options.json : When non-default formatting options are needed for a test group, place an options.json (biome.json format) alongside the .html files in that folder

Applied to files:

  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreDomComponents.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowFunctions.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreRefs.options.json
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/tests/specs/**/*.jsonc : Use .jsonc files containing arrays of code snippets for snapshot tests that require multiple script snippets

Applied to files:

  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreDomComponents.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowFunctions.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowArrowFunctions.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowBind.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreRefs.options.json
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/lib/src/lint/nursery/**/*.rs : Place all new rules in the nursery group under biome_js_analyze/lib/src/lint/nursery

Applied to files:

  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreDomComponents.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowFunctions.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowArrowFunctions.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowBind.options.json
  • crates/biome_rule_options/src/lib.rs
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreRefs.options.json
  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_rule_options/lib/**/*.rs : Define per‑rule options in the biome_rule_options crate (one file per rule, e.g., use_my_rule.rs)

Applied to files:

  • crates/biome_rule_options/src/lib.rs
📚 Learning: 2025-08-11T11:48:52.001Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_js_formatter/CONTRIBUTING.md:0-0
Timestamp: 2025-08-11T11:48:52.001Z
Learning: Applies to crates/biome_js_formatter/**/Cargo.toml : Add biome_js_formatter as a path dependency in Cargo.toml: biome_js_formatter = { version = "0.0.1", path = "../biome_js_formatter" }

Applied to files:

  • crates/biome_rule_options/src/lib.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/**/src/**/*.rs : In declare_lint_rule! metadata, set version: "next"

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/**/src/**/*.rs : When deprecating a rule, add deprecated: "<reason>" to declare_lint_rule!

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/**/src/**/*.rs : Set the language field in declare_lint_rule! to the most specific applicable language (js/jsx/ts/tsx)

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/**/src/**/*.rs : When porting a lint rule, set sources in declare_lint_rule! (RuleSource::Eslint(...).same() or .inspired())

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/**/src/**/*.rs : If a rule provides a code action, declare fix_kind in declare_lint_rule!

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
🧬 Code graph analysis (1)
crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (1)
crates/biome_analyze/src/rule.rs (4)
  • recommended (538-541)
  • sources (553-556)
  • inspired (241-246)
  • domains (568-571)
🔇 Additional comments (9)
crates/biome_rule_options/src/no_jsx_props_bind.rs (2)

1-12: Options shape, derives, and serde config look solid

Matches our conventions (Line 5), public and schema-friendly. Nice one.


3-11: No custom Default required: all options default to false upstream
Deriving Default already mirrors eslint-plugin-react’s jsx-no-bind defaults (all five are false), so no change needed.

crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreRefs.options.json (1)

1-16: Config file LGTM

Schema path, rule path, and option key casing all look correct.

crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreDomComponents.options.json (1)

1-16: Config looks sound

Matches the new rule and option; pathing to the schema also looks correct for this folder depth.

crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowBind.options.json (1)

1-16: LGTM

Option wiring for allowBind is correct and consistent with the other options files.

crates/biome_rule_options/src/lib.rs (1)

114-114: Module exposure is correct

pub mod no_jsx_props_bind; is in the expected position and naming matches the file. Remember this file is generated; ensure xtask/codegen was run so it doesn’t get clobbered later.

crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowArrowFunctions.options.json (1)

1-16: Config looks correct

Camel-cased option matches schema expectations. No issues.

crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validAllowFunctions.options.json (1)

1-16: Config looks correct

Option name aligns with the rule options. All good.

crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (1)

38-45: Confirm TSX coverage

With language: "jsx", does this fire for TSX as well? If not, we should either widen language or add a TSX variant and tests.

Would you add a TSX test fixture mirroring the current JSX ones to confirm?

Comment on lines 61 to 63
if options.ignore_dom_components && element.is_element() {
return None;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

ignoreDomComponents currently disables the rule for (almost) all elements

element.is_element() checks “is JSX element vs fragment”, not “is DOM (lowercase) vs custom (capitalised)”. This will skip both DOM and custom components whenever the option is true.

Use the tag name to detect lowercase DOM components:

-        if options.ignore_dom_components && element.is_element() {
-            return None;
-        }
+        if options.ignore_dom_components {
+            // Ignore only built-in DOM components (lowercase tag names)
+            let is_dom_component = if let Ok(token) = element.name_value_token() {
+                let text = token.text();
+                matches!(text.chars().next(), Some(c) if c.is_ascii_lowercase())
+            } else {
+                false
+            };
+            if is_dom_component {
+                return None;
+            }
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if options.ignore_dom_components && element.is_element() {
return None;
}
if options.ignore_dom_components {
// Ignore only built-in DOM components (lowercase tag names)
let is_dom_component = if let Ok(token) = element.name_value_token() {
let text = token.text();
matches!(text.chars().next(), Some(c) if c.is_ascii_lowercase())
} else {
false
};
if is_dom_component {
return None;
}
}
🤖 Prompt for AI Agents
In crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs around lines
61-63, the current check uses element.is_element() which only distinguishes
element vs fragment and wrongly disables the rule for custom components too;
instead, inspect the JSX tag name (from the element's opening element/name) and
determine if it is a DOM element by checking the identifier's first character is
lowercase (or otherwise treating lowercase tag names as DOM), and only return
None when the tag name indicates a lowercase DOM element; update the conditional
to extract the tag identifier safely and test its first character rather than
using is_element().

Comment on lines 95 to 102
AnyJsExpression::JsCallExpression(_) => {
// TODO: Check this
if options.allow_bind {
None
} else {
Some("JSX props should not use .bind()")
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Call-expression check mislabels any call as “.bind()”

This will flag onClick={foo()} as if it were .bind(). We must specifically detect a member call where the property is named “bind”.

Narrow the check to .bind:

-            AnyJsExpression::JsCallExpression(_) => {
-                // TODO: Check this
-                if options.allow_bind {
-                    None
-                } else {
-                    Some("JSX props should not use .bind()")
-                }
-            }
+            AnyJsExpression::JsCallExpression(call) => {
+                if options.allow_bind {
+                    return None;
+                }
+                let is_bind = call
+                    .callee()
+                    .ok()
+                    .and_then(|c| c.as_js_member_expression().cloned())
+                    .and_then(|m| m.member_name().ok())
+                    .and_then(|n| n.value_token().ok())
+                    .map(|t| t.text() == "bind")
+                    .unwrap_or(false);
+                if is_bind {
+                    Some("JSX props should not use .bind()")
+                } else {
+                    None
+                }
+            }

Also add a test that onClick={foo()} is not reported.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
AnyJsExpression::JsCallExpression(_) => {
// TODO: Check this
if options.allow_bind {
None
} else {
Some("JSX props should not use .bind()")
}
}
AnyJsExpression::JsCallExpression(call) => {
if options.allow_bind {
return None;
}
let is_bind = call
.callee()
.ok()
.and_then(|c| c.as_js_member_expression().cloned())
.and_then(|m| m.member_name().ok())
.and_then(|n| n.value_token().ok())
.map(|t| t.text() == "bind")
.unwrap_or(false);
if is_bind {
Some("JSX props should not use .bind()")
} else {
None
}
}
🤖 Prompt for AI Agents
In crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs around lines
95-102, the current match arm treats any call expression as a .bind() usage and
reports it incorrectly; change the detection to only match a MemberCall (or call
where callee is a member expression) whose property/identifier is named "bind"
so plain calls like onClick={foo()} are not flagged, and add a unit test
asserting that onClick={foo()} produces no diagnostic while
onClick={foo.bind(this)} does.

Comment on lines 9 to 16
<span onClick={() => console.log("Hello!")} />; (
<button
type="button"
onClick={function () {
alert("1337");
}}
/>
</>;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Syntax error: stray ; ( breaks parsing

There’s an extra ; ( after the <span /> which makes the file invalid JSX, so the test will fail before the rule runs.

Apply this fix:

-	<span onClick={() => console.log("Hello!")} />; (
+	<span onClick={() => console.log("Hello!")} />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span onClick={() => console.log("Hello!")} />; (
<button
type="button"
onClick={function () {
alert("1337");
}}
/>
</>;
<span onClick={() => console.log("Hello!")} />
<button
type="button"
onClick={function () {
alert("1337");
}}
/>
</>;
🤖 Prompt for AI Agents
In
crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreDomComponents.jsx
around lines 9–16, the JSX is broken by an extra "; (" immediately after the
self-closing <span />, which produces a syntax error; remove the stray semicolon
and opening parenthesis so the fragment contains only valid JSX nodes (e.g.,
<span onClick={...} /> <button ... /> inside the fragment) and ensure the
fragment is properly opened/closed with no stray punctuation.

Copy link
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.

Actionable comments posted: 0

🧹 Nitpick comments (1)
crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalidIgnoreDomComponents.jsx (1)

1-5: Broaden coverage with a couple of quick companions

  • Add a self-closing variant to avoid any oddities with children.
  • Ensure there’s a valid DOM case under the same options (e.g. <div onClick={() => {}} />) to prove the ignore actually works.
  • When you tackle identifier references, add:
    • Stable: module-scope function onClick(){} then <Foo onClick={onClick} /> (valid).
    • Unstable: inside component const onClick = () => {}; return <Foo onClick={onClick} />; (invalid, once supported).

Happy to sketch these fixtures if helpful.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d21b4ff and 3ab2037.

⛔ Files ignored due to path filters (4)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalid.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalidIgnoreDomComponents.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/valid.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreDomComponents.jsx.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (6)
  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalid.jsx (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalidIgnoreDomComponents.jsx (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalidIgnoreDomComponents.options.json (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/valid.jsx (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreDomComponents.jsx (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalidIgnoreDomComponents.options.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/validIgnoreDomComponents.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/valid.jsx
  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
🧰 Additional context used
📓 Path-based instructions (3)
crates/biome_*_{syntax,parser,formatter,analyze,factory,semantic}/**

📄 CodeRabbit inference engine (CLAUDE.md)

Maintain the per-language crate structure: biome_{lang}_{syntax,parser,formatter,analyze,factory,semantic}

Files:

  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalidIgnoreDomComponents.jsx
crates/biome_*/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place core crates under /crates/biome_*/

Files:

  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalidIgnoreDomComponents.jsx
**/tests/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place test files under a tests/ directory in each crate

Files:

  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalidIgnoreDomComponents.jsx
🔇 Additional comments (1)
crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalidIgnoreDomComponents.jsx (1)

1-5: Good invalid fixture for custom components with ignoreDomComponents enabled

This correctly exercises an inline arrow on a non-DOM component; should still be flagged. All good.

Copy link
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.

For that case, you can use the Semantic model to look up where the binding was defined.

Also, to fix the merge conflicts:

  • rebase on main (you'll get conflicts)
  • run just gen-all
  • continue the rebase git rebase --continue

}

impl Rule for NoJsxPropsBind {
type Query = Ast<JsxAttribute>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Here's how you get access to the semantic model. It'll be available in ctx.model()

Suggested change
type Query = Ast<JsxAttribute>;
type Query = Semantic<JsxAttribute>;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the hint, although I'm still struggling with this to be honest. Let me try for a couple more hours.

Comment on lines 7 to 11
pub ignore_dom_components: bool,
pub ignore_refs: bool,
pub allow_arrow_functions: bool,
pub allow_functions: bool,
pub allow_bind: bool,
Copy link
Contributor

Choose a reason for hiding this comment

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

We generally don't add options on the first iteration of a new rule because we want to have concrete use cases for them.

Most of these options seem to defeat the purpose of the rule too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just went off the ESLint rule. Anyway, I've removed them all as requested.

ignoreDOMComponents at least makes sense to me, ignoreRefs probably as well.


impl Rule for NoJsxPropsBind {
type Query = Ast<JsxAttribute>;
type State = &'static str;
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a rather unusual way to emit varying diagnostic messages. Typically, you pass along the information needed to write the diagnostic message instead of passing the message itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, I adapted how this was done in other rules.

Comment on lines 119 to 129
//
// Read our guidelines to write great diagnostics:
// https://docs.rs/biome_analyze/latest/biome_analyze/#what-a-rule-should-say-to-the-user
//
let node = ctx.query();
Some(RuleDiagnostic::new(
rule_category!(),
node.range(),
markup! {
{state}
},
Copy link
Contributor

Choose a reason for hiding this comment

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

Please read the guide and make sure to follow the rule pillars. The diagnostic does not contain enough info.

/// Using `.bind()` on a function or declaring a function directly in props
/// creates a new function on every render, which is treated as a completely different function.
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be good to elaborate and explain the performance impact.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I slightly rephrased it from here. Maybe we can just copy/paste the entire opening paragraph?

https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-bind.md

@github-actions github-actions bot added the A-CLI Area: CLI label Sep 6, 2025
Copy link
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.

Actionable comments posted: 1

🧹 Nitpick comments (4)
crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (4)

9-13: Docs: call out arrow functions explicitly and tighten the rationale.

The summary says “function declaration”, but we’re flagging arrow/function expressions. Also, briefly explain the render-time allocation cost.

-    /// Disallow .bind() or function declaration in JSX props
+    /// Disallow .bind() and inline function expressions (including arrow functions) in JSX props
@@
-    /// Using `.bind()` on a function or declaring a function directly in props
-    /// creates a new function on every render, which is treated as a completely different function.
-    /// This may cause unnecessary rerenders.
+    /// Using `.bind()` or creating a function inline in props allocates a new
+    /// function every render, changing identity and potentially triggering unnecessary rerenders.

Also applies to: 15-27, 29-34


58-63: Switch to Semantic query to unlock identifier resolution (needed for the “referenced function” case).

This gives you ctx.model() to resolve onClick={onClick} to its declaration and assess stability.

-use biome_analyze::{
-    Ast, Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
-};
+use biome_analyze::{
+    Ast, Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule, Semantic,
+};
@@
-    type Query = Ast<JsxAttribute>;
+    type Query = Semantic<JsxAttribute>;

Skeleton for later (where you match the prop expression):

if let AnyJsExpression::JsIdentifierExpression(id_expr) = &expression {
    let model = ctx.model();
    if let Ok(name_ref) = id_expr.name() {
        if let Some(binding) = model.binding(&name_ref) {
            // TODO: inspect `binding.tree()` or `binding.syntax()`:
            // - Is it a class method (this.handleClick)? Consider stable.
            // - Is it a top-level function? Consider stable.
            // - Is it a variable assigned to an arrow/function inside a function component?
            //   If not wrapped in useCallback, consider unstable.
        }
    }
}

Happy to flesh this out with concrete AST checks against Biome’s semantic model API.


111-126: Diagnostic: include the prop name and a concrete reason.

Small UX win and clearer action.

-    fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
+    fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
+        let prop = ctx
+            .query()
+            .name()
+            .ok()
+            .and_then(|n| n.value_token().ok())
+            .map(|t| t.text().to_string())
+            .unwrap_or_else(|| "prop".to_string());
         let note = match state.invalid_kind {
             InvalidKind::ArrowFunction => "JSX props should not use arrow functions",
             InvalidKind::Bind => "JSX props should not use .bind()",
             InvalidKind::Function => "JSX props should not use functions",
         };
         Some(
             RuleDiagnostic::new(
                 rule_category!(),
                 state.attribute_range,
-                "Pass stable functions as props to avoid unnecessary rerenders.",
+                format!("Avoid creating functions in '{prop}'; it allocates a new function each render."),
             )
             .note(note)
             .note("Consider extracting the function or wrapping it with React.useCallback"),
         )
     }

64-109: Heuristic for referenced identifiers (what’s “stable”).

If you want to go beyond eslint parity, a pragmatic heuristic is:

  • this.handleClick: point to a class method/property in the enclosing class → allow.
  • Top-level function: allow.
  • Variable in a function component initialised to an arrow/function:
    • If initialiser is inside React.useCallback(...) → allow.
    • Otherwise, consider unstable (warn).

I can wire this up using ctx.model() with conservative fallbacks.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3ab2037 and c73fcdc.

⛔ Files ignored due to path filters (8)
  • 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_diagnostics_categories/src/categories.rs is excluded by !**/categories.rs and included by **
  • crates/biome_js_analyze/src/lint/nursery.rs is excluded by !**/nursery.rs and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalid.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/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 (5)
  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalid.jsx (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/valid.jsx (1 hunks)
  • crates/biome_rule_options/src/lib.rs (1 hunks)
  • crates/biome_rule_options/src/no_jsx_props_bind.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalid.jsx
  • crates/biome_rule_options/src/no_jsx_props_bind.rs
  • crates/biome_rule_options/src/lib.rs
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/valid.jsx
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{rs,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Format Rust and TOML files before committing (use just f/just format).

Files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
crates/biome_*_{syntax,parser,formatter,analyze,factory,semantic}/**

📄 CodeRabbit inference engine (CLAUDE.md)

Maintain the per-language crate structure: biome_{lang}_{syntax,parser,formatter,analyze,factory,semantic}

Files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
crates/biome_*/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place core crates under /crates/biome_*/

Files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/lib/src/lint/nursery/**/*.rs : Place all new rules in the nursery group under biome_js_analyze/lib/src/lint/nursery
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/**/src/**/*.rs : Set the language field in declare_lint_rule! to the most specific applicable language (js/jsx/ts/tsx)
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/**/src/**/*.rs : When deprecating a rule, add deprecated: "<reason>" to declare_lint_rule!

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
🧬 Code graph analysis (1)
crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (1)
crates/biome_analyze/src/rule.rs (4)
  • recommended (554-557)
  • sources (569-572)
  • inspired (249-254)
  • domains (584-587)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (26)
  • GitHub Check: Test Node.js API
  • GitHub Check: Documentation
  • GitHub Check: Bench (biome_package)
  • GitHub Check: Bench (biome_configuration)
  • GitHub Check: Test (depot-windows-2022-16)
  • GitHub Check: Test (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Bench (biome_html_parser)
  • GitHub Check: Bench (biome_css_analyze)
  • GitHub Check: Bench (biome_graphql_parser)
  • GitHub Check: Check JS Files
  • GitHub Check: Bench (biome_module_graph)
  • GitHub Check: Lint project (depot-windows-2022)
  • GitHub Check: End-to-end tests
  • GitHub Check: Lint project (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Check Dependencies
  • GitHub Check: Bench (biome_html_formatter)
  • GitHub Check: Bench (biome_graphql_formatter)
  • GitHub Check: Bench (biome_json_formatter)
  • GitHub Check: Bench (biome_js_analyze)
  • GitHub Check: Bench (biome_json_analyze)
  • GitHub Check: Bench (biome_json_parser)
  • GitHub Check: Bench (biome_css_formatter)
  • GitHub Check: Bench (biome_js_parser)
  • GitHub Check: Bench (biome_css_parser)
  • GitHub Check: Bench (biome_js_formatter)
  • GitHub Check: autofix
🔇 Additional comments (3)
crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (3)

37-44: Rule metadata LGTM.

Name, language ("jsx"), source and domains look correct for a nursery rule.


75-86: Arrow/function expression detection LGTM.

Covers the common inline cases.


29-34: Add a “Valid” example for identifier and call cases.

To lock behaviour:

  • onClick={onClick} (referenced function)
  • onClick={foo()} (plain call; should be allowed)

I can add fixtures mirroring eslint-plugin-react’s examples and the extra cases above.

Comment on lines 87 to 102
AnyJsExpression::JsCallExpression(call) => {
// This will still throw a false positive on e.g. window.bind()
let is_bind = call
.callee()
.ok()
.and_then(|c| c.as_js_static_member_expression().cloned())
.and_then(|m| m.member().ok())
.and_then(|n| n.value_token().ok())
.map(|t| t.text() == "bind")
.unwrap_or(false);
if is_bind {
Some(NoJsxPropsBindState {
invalid_kind: InvalidKind::Bind,
attribute_range: expression.range(),
})
// Some("JSX props should not use .bind()")
} else {
None
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

.bind() detection: support computed/optional members and avoid gaps.

Current check only recognises static members. Cover obj["bind"](…) and optional chaining obj.bind?.(…).

-            AnyJsExpression::JsCallExpression(call) => {
-                // This will still throw a false positive on e.g. window.bind()
-                let is_bind = call
-                    .callee()
-                    .ok()
-                    .and_then(|c| c.as_js_static_member_expression().cloned())
-                    .and_then(|m| m.member().ok())
-                    .and_then(|n| n.value_token().ok())
-                    .map(|t| t.text() == "bind")
-                    .unwrap_or(false);
+            AnyJsExpression::JsCallExpression(call) => {
+                use biome_js_syntax::AnyJsMemberExpression;
+
+                // Unwrap optional chaining: (obj.bind)?.(...)
+                let callee = call.callee().ok().and_then(|c| {
+                    if let Some(chain) = c.as_js_chain_expression() {
+                        chain.expression().ok()
+                    } else {
+                        Some(c)
+                    }
+                });
+
+                let member = callee.and_then(|c| c.as_js_member_expression().cloned());
+                let is_bind = member
+                    .and_then(|m: AnyJsMemberExpression| m.member_name().ok())
+                    .and_then(|n| n.value_token().ok())
+                    .map(|t| t.text() == "bind")
+                    .unwrap_or(false);
                 if is_bind {
-                    Some(NoJsxPropsBindState {
-                        invalid_kind: InvalidKind::Bind,
-                        attribute_range: expression.range(),
-                    })
+                    Some(NoJsxPropsBindState {
+                        invalid_kind: InvalidKind::Bind,
+                        // Highlight just the callee if we can
+                        attribute_range: callee.map(|c| c.range()).unwrap_or_else(|| expression.range()),
+                    })
                 } else {
                     None
                 }
             }

Add tests for:

  • onClick={foo()} (not reported)
  • onClick={obj"bind"}
  • onClick={obj.bind?.(this)}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
AnyJsExpression::JsCallExpression(call) => {
// This will still throw a false positive on e.g. window.bind()
let is_bind = call
.callee()
.ok()
.and_then(|c| c.as_js_static_member_expression().cloned())
.and_then(|m| m.member().ok())
.and_then(|n| n.value_token().ok())
.map(|t| t.text() == "bind")
.unwrap_or(false);
if is_bind {
Some(NoJsxPropsBindState {
invalid_kind: InvalidKind::Bind,
attribute_range: expression.range(),
})
// Some("JSX props should not use .bind()")
} else {
None
}
}
AnyJsExpression::JsCallExpression(call) => {
use biome_js_syntax::AnyJsMemberExpression;
// Unwrap optional chaining: (obj.bind)?.(...)
let callee = call.callee().ok().and_then(|c| {
if let Some(chain) = c.as_js_chain_expression() {
chain.expression().ok()
} else {
Some(c)
}
});
let member = callee.and_then(|c| c.as_js_member_expression().cloned());
let is_bind = member
.and_then(|m: AnyJsMemberExpression| m.member_name().ok())
.and_then(|n| n.value_token().ok())
.map(|t| t.text() == "bind")
.unwrap_or(false);
if is_bind {
Some(NoJsxPropsBindState {
invalid_kind: InvalidKind::Bind,
// Highlight just the callee if we can
attribute_range: callee.map(|c| c.range()).unwrap_or_else(|| expression.range()),
})
} else {
None
}
}
🤖 Prompt for AI Agents
In crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs around lines 87
to 106, the current .bind() detection only matches static member expressions and
misses computed member calls (obj["bind"](...)) and optional chaining/member
calls (obj.bind?.(...)) and may produce gaps; update the callee analysis to also
handle JsComputedMemberExpression and
JsOptionalCallExpression/JsOptionalMemberExpression (or the AST variants in this
crate) by extracting the member name from computed string literal keys and by
unwrapping optional call/member nodes to check the final member token text ==
"bind"; ensure you preserve the negative case for plain calls (onClick={foo()})
and add/update tests for onClick={foo()} (no report),
onClick={obj["bind"](this)} (report), and onClick={obj.bind?.(this)} (report).

@codspeed-hq
Copy link

codspeed-hq bot commented Sep 6, 2025

CodSpeed Performance Report

Merging #7410 will not alter performance

Comparing sgarcialaguna:nojsxpropsbind (52fd7cb) with main (cc3e851)

Summary

✅ 58 untouched
⏩ 95 skipped1

Footnotes

  1. 95 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.

Copy link
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.

Actionable comments posted: 3

♻️ Duplicate comments (1)
crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (1)

58-63: Use the semantic query to enable referenced-function analysis

Switch to Semantic<JsxAttribute> so you can resolve identifiers via ctx.model() and handle the “referenced function declared elsewhere” case.

-use biome_analyze::{
-    Ast, Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
-};
+use biome_analyze::{
+    Ast, Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
+    Semantic,
+};
@@
-    type Query = Ast<JsxAttribute>;
+    type Query = Semantic<JsxAttribute>;

Would you like me to follow up with a targeted implementation to resolve JsReferenceIdentifier to its binding and classify stability by scope (module/global => stable; inside component body without useCallback => unstable)?

🧹 Nitpick comments (3)
crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (3)

9-13: Docs: say “arrow/function expressions”, not “function declaration”, and briefly mention the perf cost

Declarations can’t appear in JSX expressions; what you mean are function expressions. Also add one line on why new identities hurt memoisation.

-    /// Disallow .bind() or function declaration in JSX props
+    /// Disallow .bind(), arrow functions, or function expressions in JSX props
@@
-    /// Using `.bind()` on a function or declaring a function directly in props
-    /// creates a new function on every render, which is treated as a completely different function.
-    /// This may cause unnecessary rerenders.
+    /// Using `.bind()` or creating a function inline in props creates a new function
+    /// on every render, changing identity and defeating memoisation (e.g. `React.memo`,
+    /// `useMemo`, `PureComponent`), which may cause unnecessary rerenders.

Also applies to: 15-27, 29-35


107-122: Diagnostic: include attribute name and precise kind; tweak wording

“functions” is vague; say “function expressions”. Also mention the prop name to make fixes clearer.

-pub struct NoJsxPropsBindState {
-    invalid_kind: InvalidKind,
-    attribute_range: TextRange,
-}
+pub struct NoJsxPropsBindState {
+    invalid_kind: InvalidKind,
+    attribute_range: TextRange,
+    // Optional: surface prop name in the diagnostic
+    // attribute_name: Option<biome_rowan::TextSize>, // or store token range
+}
@@
-        let note = match state.invalid_kind {
-            InvalidKind::ArrowFunction => "JSX props should not use arrow functions",
-            InvalidKind::Bind => "JSX props should not use .bind()",
-            InvalidKind::Function => "JSX props should not use functions",
+        let note = match state.invalid_kind {
+            InvalidKind::ArrowFunction => "JSX props should not use arrow functions",
+            InvalidKind::Bind => "JSX props should not use .bind()",
+            InvalidKind::Function => "JSX props should not use function expressions",
         };
         Some(
             RuleDiagnostic::new(
                 rule_category!(),
                 state.attribute_range,
-                "Pass stable functions as props to avoid unnecessary rerenders.",
+                "Pass stable function references as props to avoid unnecessary rerenders.",
             )
             .note(note)
             .note("Consider extracting the function or wrapping it in useCallback"),
         )

64-105: Support the “referenced function” case (heuristic via semantic model)

Not a blocker, but here’s a minimal approach: if the expression is a reference, resolve its binding and classify stability by where it’s declared.

         match &expression {
+            AnyJsExpression::JsReferenceIdentifier(id) => {
+                let model = ctx.model();
+                // If we can’t resolve, don’t report
+                let Some(binding) = model.binding(id) else { return None; };
+                // Heuristic:
+                // - Declared at module scope or as a class field initializer (arrow) => stable
+                // - Declared inside the current function component body and not produced by a `useCallback` call => unstable
+                // Wire up an `is_unstable_local_function(&binding, model)` helper as a follow‑up.
+                if is_unstable_local_function(&binding, &model) {
+                    return Some(NoJsxPropsBindState {
+                        invalid_kind: InvalidKind::Function,
+                        attribute_range: expression.range(),
+                    });
+                }
+                None
+            }

Happy to send a follow‑up patch with is_unstable_local_function (scope walk + quick useCallback callee check).

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c73fcdc and 13f78e4.

📒 Files selected for processing (1)
  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{rs,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Format Rust and TOML files before committing (use just f/just format).

Files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
crates/biome_*_{syntax,parser,formatter,analyze,factory,semantic}/**

📄 CodeRabbit inference engine (CLAUDE.md)

Maintain the per-language crate structure: biome_{lang}_{syntax,parser,formatter,analyze,factory,semantic}

Files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
crates/biome_*/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place core crates under /crates/biome_*/

Files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/lib/src/lint/nursery/**/*.rs : Place all new rules in the nursery group under biome_js_analyze/lib/src/lint/nursery
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/**/src/**/*.rs : When deprecating a rule, add deprecated: "<reason>" to declare_lint_rule!

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/**/src/**/*.rs : If a rule provides a code action, declare fix_kind in declare_lint_rule!

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
🧬 Code graph analysis (1)
crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (1)
crates/biome_analyze/src/rule.rs (4)
  • recommended (554-557)
  • sources (569-572)
  • inspired (249-254)
  • domains (584-587)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (23)
  • GitHub Check: Check JS Files
  • GitHub Check: Test Node.js API
  • GitHub Check: Documentation
  • GitHub Check: Test (depot-windows-2022-16)
  • GitHub Check: Test (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Check Dependencies
  • GitHub Check: Bench (biome_graphql_parser)
  • GitHub Check: Bench (biome_package)
  • GitHub Check: Bench (biome_css_parser)
  • GitHub Check: Bench (biome_js_parser)
  • GitHub Check: Bench (biome_json_parser)
  • GitHub Check: Bench (biome_js_analyze)
  • GitHub Check: Bench (biome_html_parser)
  • GitHub Check: Bench (biome_module_graph)
  • GitHub Check: Bench (biome_configuration)
  • GitHub Check: Bench (biome_graphql_formatter)
  • GitHub Check: Bench (biome_json_analyze)
  • GitHub Check: Bench (biome_css_formatter)
  • GitHub Check: Bench (biome_json_formatter)
  • GitHub Check: Bench (biome_html_formatter)
  • GitHub Check: Bench (biome_js_formatter)
  • GitHub Check: Bench (biome_css_analyze)
  • GitHub Check: autofix
🔇 Additional comments (2)
crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (2)

37-44: Rule metadata looks good

Name, source mapping, and nursery placement align with project conventions.


37-44: No change required
The language: "jsx" setting is consistent with numerous existing JSX rules (e.g. no_suspicious_semicolon_in_jsx, no_array_index_key, no_duplicate_jsx_props, etc.) and follows the crate’s convention.

Comment on lines +74 to +84
match &expression {
AnyJsExpression::JsArrowFunctionExpression(_) => Some(NoJsxPropsBindState {
invalid_kind: InvalidKind::ArrowFunction,
attribute_range: expression.range(),
}),

AnyJsExpression::JsFunctionExpression(_) => Some(NoJsxPropsBindState {
invalid_kind: InvalidKind::Function,
attribute_range: expression.range(),
}),
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Gate arrow/function matches behind options

Allow users to opt‑in/out via allow_arrow_functions and allow_functions.

-        match &expression {
-            AnyJsExpression::JsArrowFunctionExpression(_) => Some(NoJsxPropsBindState {
+        match &expression {
+            AnyJsExpression::JsArrowFunctionExpression(_) if !options.allow_arrow_functions => Some(NoJsxPropsBindState {
                 invalid_kind: InvalidKind::ArrowFunction,
                 attribute_range: expression.range(),
             }),
 
-            AnyJsExpression::JsFunctionExpression(_) => Some(NoJsxPropsBindState {
+            AnyJsExpression::JsFunctionExpression(_) if !options.allow_functions => Some(NoJsxPropsBindState {
                 invalid_kind: InvalidKind::Function,
                 attribute_range: expression.range(),
             }),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
match &expression {
AnyJsExpression::JsArrowFunctionExpression(_) => Some(NoJsxPropsBindState {
invalid_kind: InvalidKind::ArrowFunction,
attribute_range: expression.range(),
}),
AnyJsExpression::JsFunctionExpression(_) => Some(NoJsxPropsBindState {
invalid_kind: InvalidKind::Function,
attribute_range: expression.range(),
}),
match &expression {
AnyJsExpression::JsArrowFunctionExpression(_) if !options.allow_arrow_functions => Some(NoJsxPropsBindState {
invalid_kind: InvalidKind::ArrowFunction,
attribute_range: expression.range(),
}),
AnyJsExpression::JsFunctionExpression(_) if !options.allow_functions => Some(NoJsxPropsBindState {
invalid_kind: InvalidKind::Function,
attribute_range: expression.range(),
}),
🤖 Prompt for AI Agents
In crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs around lines 74
to 83, the current match always flags arrow and function expressions; change
each arm to respect the linter options by returning Some(NoJsxPropsBindState {
... }) only when the corresponding option disallows that kind (i.e., for
AnyJsExpression::JsArrowFunctionExpression, only create the state if
!options.allow_arrow_functions; for AnyJsExpression::JsFunctionExpression, only
create the state if !options.allow_functions). Ensure you have access to the
options/config in this scope (add it as a parameter or capture it) and otherwise
fall through to None when the option allows the construct.

Comment on lines 84 to 102
AnyJsExpression::JsCallExpression(call) => {
// This will still throw a false positive on e.g. window.bind()
let is_bind = call
.callee()
.ok()
.and_then(|c| c.as_js_static_member_expression().cloned())
.and_then(|m| m.member().ok())
.and_then(|n| n.value_token().ok())
.map(|t| t.text() == "bind")
.unwrap_or(false);
if is_bind {
Some(NoJsxPropsBindState {
invalid_kind: InvalidKind::Bind,
attribute_range: expression.range(),
})
} else {
None
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

.bind() detection: handle chains/computed/optional and honour option

Covers only static members and ignores allow_bind. Also onClick={foo()} must not be flagged.

-            AnyJsExpression::JsCallExpression(call) => {
-                // This will still throw a false positive on e.g. window.bind()
-                let is_bind = call
-                    .callee()
-                    .ok()
-                    .and_then(|c| c.as_js_static_member_expression().cloned())
-                    .and_then(|m| m.member().ok())
-                    .and_then(|n| n.value_token().ok())
-                    .map(|t| t.text() == "bind")
-                    .unwrap_or(false);
-                if is_bind {
+            AnyJsExpression::JsCallExpression(call) => {
+                if options.allow_bind {
+                    return None;
+                }
+                use biome_js_syntax::AnyJsMemberExpression;
+
+                // Unwrap optional chaining: (obj.bind)?.(...)
+                let callee = call.callee().ok().and_then(|c| {
+                    if let Some(chain) = c.as_js_chain_expression() {
+                        chain.expression().ok()
+                    } else {
+                        Some(c)
+                    }
+                });
+
+                // Support static, computed, and optional members
+                let member = callee.as_ref().and_then(|c| c.as_js_member_expression().cloned());
+                let is_bind = member
+                    .and_then(|m: AnyJsMemberExpression| m.member_name().ok())
+                    .and_then(|n| n.value_token().ok())
+                    .map(|t| t.text() == "bind")
+                    .unwrap_or(false);
+
+                if is_bind {
                     Some(NoJsxPropsBindState {
                         invalid_kind: InvalidKind::Bind,
-                        attribute_range: expression.range(),
+                        // Highlight the callee if present
+                        attribute_range: callee.map(|c| c.range()).unwrap_or_else(|| expression.range()),
                     })
                 } else {
                     None
                 }
             }

Please also add tests:

  • invalid: <Foo onClick={obj["bind"](this)} />, <Foo onClick={obj.bind?.(this)} />
  • valid: <Foo onClick={foo()} />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
AnyJsExpression::JsCallExpression(call) => {
// This will still throw a false positive on e.g. window.bind()
let is_bind = call
.callee()
.ok()
.and_then(|c| c.as_js_static_member_expression().cloned())
.and_then(|m| m.member().ok())
.and_then(|n| n.value_token().ok())
.map(|t| t.text() == "bind")
.unwrap_or(false);
if is_bind {
Some(NoJsxPropsBindState {
invalid_kind: InvalidKind::Bind,
attribute_range: expression.range(),
})
} else {
None
}
}
AnyJsExpression::JsCallExpression(call) => {
// Honor the `allow_bind` option first
if options.allow_bind {
return None;
}
use biome_js_syntax::AnyJsMemberExpression;
// Unwrap optional chaining: (obj.bind)?.(...)
let callee = call.callee().ok().and_then(|c| {
if let Some(chain) = c.as_js_chain_expression() {
chain.expression().ok()
} else {
Some(c)
}
});
// Support static, computed, and optional members
let member = callee.as_ref().and_then(|c| c.as_js_member_expression().cloned());
let is_bind = member
.and_then(|m: AnyJsMemberExpression| m.member_name().ok())
.and_then(|n| n.value_token().ok())
.map(|t| t.text() == "bind")
.unwrap_or(false);
if is_bind {
Some(NoJsxPropsBindState {
invalid_kind: InvalidKind::Bind,
// Highlight the specific callee if we have it
attribute_range: callee.map(|c| c.range()).unwrap_or_else(|| expression.range()),
})
} else {
None
}
}
🤖 Prompt for AI Agents
crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs around lines
84-102: the current .bind() detection only handles static member expressions and
misses chained/optional/computed calls and doesn’t respect the allow_bind
option; update the callee analysis to accept MemberExpression or
OptionalMemberExpression (including chained members), detect both static
property name "bind" and computed property access where the property is a string
literal "bind", and skip when the call is not a member call (so plain foo()
won’t be flagged); also read and honor the allow_bind configuration flag to
bypass this check when enabled; finally add tests for invalid cases `<Foo
onClick={obj["bind"](this)} />`, `<Foo onClick={obj.bind?.(this)} />` and a
valid case `<Foo onClick={foo()} />`.

Copy link
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.

Actionable comments posted: 1

♻️ Duplicate comments (4)
crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (4)

58-60: Switch to Semantic query to enable symbol resolution (needed for referenced functions).

To support “is this a stable referenced function?” you’ll need access to the semantic model via ctx.model(). Change the query type to Semantic and import Semantic.

- use biome_analyze::{
-     Ast, Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
- };
+ use biome_analyze::{
+     Ast, Semantic, Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
+ };
@@
-    type Query = Ast<JsxAttribute>;
+    type Query = Semantic<JsxAttribute>;

Happy to wire a follow-up that resolves identifiers and checks stability (e.g. function declarations vs. useCallback/useMemo).

Also applies to: 1-3


64-73: Respect rule options early (ignore refs and DOM elements).

Currently options aren’t consulted; the rule runs on every attribute. Short‑circuit per options before extracting the expression.

     fn run(ctx: &RuleContext<Self>) -> Self::Signals {
-        let expression = ctx
+        let options = ctx.options();
+
+        // ignore ref attributes if configured
+        if options.ignore_refs {
+            if let Ok(name) = ctx.query().name() {
+                if name.text() == "ref" {
+                    return None;
+                }
+            }
+        }
+
+        // ignore lowercase (built-in) DOM components if configured
+        if options.ignore_dom_components {
+            if let Some(opening) = ctx
+                .query()
+                .syntax()
+                .ancestors()
+                .find_map(biome_js_syntax::JsxOpeningElement::cast)
+            {
+                if let Ok(token) = opening.name().and_then(|n| n.value_token()) {
+                    if token.text().chars().next().is_some_and(|c| c.is_ascii_lowercase()) {
+                        return None;
+                    }
+                }
+            }
+        }
+
+        let expression = ctx
             .query()
             .initializer()?
             .value()
             .ok()?
             .as_jsx_expression_attribute_value()?
             .expression()
             .ok()?;

Note: this assumes corresponding fields exist on NoJsxPropsBindOptions (see separate note).


74-84: Gate arrow/function reports behind options.

Allow users to opt in/out for arrows and function expressions.

-        match &expression {
-            AnyJsExpression::JsArrowFunctionExpression(_) => Some(NoJsxPropsBindState {
+        match &expression {
+            AnyJsExpression::JsArrowFunctionExpression(_) if !options.allow_arrow_functions => Some(NoJsxPropsBindState {
                 invalid_kind: InvalidKind::ArrowFunction,
                 attribute_range: expression.range(),
             }),
 
-            AnyJsExpression::JsFunctionExpression(_) => Some(NoJsxPropsBindState {
+            AnyJsExpression::JsFunctionExpression(_) if !options.allow_functions => Some(NoJsxPropsBindState {
                 invalid_kind: InvalidKind::Function,
                 attribute_range: expression.range(),
             }),

85-101: Tighten .bind() detection, honour allow_bind, and support optional/computed members.

Handle obj"bind", obj.bind?.(...), and avoid flagging plain calls. Also, highlight just the callee when possible.

-            AnyJsExpression::JsCallExpression(call) => {
-                // This will still throw a false positive on e.g. window.bind()
-                let is_bind = call
-                    .callee()
-                    .ok()
-                    .and_then(|c| c.as_js_static_member_expression().cloned())
-                    .and_then(|m| m.member().ok())
-                    .and_then(|n| n.value_token().ok())
-                    .map_or(false, |t| t.text() == "bind");
-                if is_bind {
-                    Some(NoJsxPropsBindState {
-                        invalid_kind: InvalidKind::Bind,
-                        attribute_range: expression.range(),
-                    })
-                } else {
-                    None
-                }
-            }
+            AnyJsExpression::JsCallExpression(call) => {
+                if options.allow_bind {
+                    return None;
+                }
+                use biome_js_syntax::AnyJsMemberExpression;
+
+                // Unwrap optional chaining: (obj.bind)?.(...)
+                let callee = call.callee().ok().and_then(|c| {
+                    if let Some(chain) = c.as_js_chain_expression() {
+                        chain.expression().ok()
+                    } else {
+                        Some(c)
+                    }
+                });
+
+                // Accept static/computed/optional member expressions
+                let member = callee.as_ref().and_then(|c| c.as_js_member_expression().cloned());
+                let is_bind = member
+                    .and_then(|m: AnyJsMemberExpression| m.member_name().ok())
+                    .and_then(|n| n.value_token().ok())
+                    .map(|t| t.text() == "bind")
+                    .unwrap_or(false);
+
+                if is_bind {
+                    Some(NoJsxPropsBindState {
+                        invalid_kind: InvalidKind::Bind,
+                        attribute_range: callee.as_ref().map(|c| c.range()).unwrap_or_else(|| expression.range()),
+                    })
+                } else {
+                    None
+                }
+            }

Please add tests for:

  • invalid: <Foo onClick={obj"bind"} />,
  • valid:
🧹 Nitpick comments (2)
crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (2)

106-121: Enrich diagnostic with attribute name; keep message concise.

Naming the prop improves UX and aligns with Biome’s rule pillars.

-    fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
+    fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
+        let prop = ctx.query().name().ok().map(|n| n.text()).unwrap_or("prop".into());
         let note = match state.invalid_kind {
             InvalidKind::ArrowFunction => "JSX props should not use arrow functions",
             InvalidKind::Bind => "JSX props should not use .bind()",
             InvalidKind::Function => "JSX props should not use function expressions",
         };
         Some(
             RuleDiagnostic::new(
                 rule_category!(),
                 state.attribute_range,
-                "Pass stable function references as props to avoid unnecessary rerenders.",
+                format!("Avoid inline binding in JSX: `{prop}` should receive a stable function reference."),
             )
             .note(note)
             .note("Consider extracting the function or wrapping it in useCallback"),
         )
     }

64-104: Roadmap note: resolving referenced functions for “stability”.

To answer the open TODO (“is onClick={onClick} stable?”), resolve the identifier and classify:

  • Function declaration/expression defined outside render scope → stable
  • useCallback(...) result → stable
  • Class method reference → stable
  • Inline/closure capturing render locals → unstable

Sketch (outside this diff): use ctx.model() to resolve AnyJsExpression::JsReferenceIdentifier to a symbol, get its declaration node, and walk upwards to see if it’s top-level/module/class or React hook. I can draft this if helpful.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 13f78e4 and d658604.

⛔ Files ignored due to path filters (1)
  • packages/@biomejs/backend-jsonrpc/src/workspace.ts is excluded by !**/backend-jsonrpc/src/workspace.ts and included by **
📒 Files selected for processing (1)
  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{rs,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Format Rust and TOML files before committing (use just f/just format).

Files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
crates/biome_*_{syntax,parser,formatter,analyze,factory,semantic}/**

📄 CodeRabbit inference engine (CLAUDE.md)

Maintain the per-language crate structure: biome_{lang}_{syntax,parser,formatter,analyze,factory,semantic}

Files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
crates/biome_*/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place core crates under /crates/biome_*/

Files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/lib/src/lint/nursery/**/*.rs : Place all new rules in the nursery group under biome_js_analyze/lib/src/lint/nursery
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/**/src/**/*.rs : When deprecating a rule, add deprecated: "<reason>" to declare_lint_rule!

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/**/src/**/*.rs : Avoid unnecessary string allocations in rules; compare against &str or TokenText instead of calling to_string()

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
🧬 Code graph analysis (1)
crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (1)
crates/biome_analyze/src/rule.rs (4)
  • recommended (554-557)
  • sources (569-572)
  • inspired (249-254)
  • domains (584-587)

@sgarcialaguna sgarcialaguna requested a review from dyc3 September 7, 2025 11:31
Copy link
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.

Actionable comments posted: 1

♻️ Duplicate comments (3)
crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (3)

65-74: Respect options early: ignore refs and DOM components

Consult options before extracting the expression, skipping attributes when configured (mirrors ESLint).

     fn run(ctx: &RuleContext<Self>) -> Self::Signals {
-        let expression: AnyJsExpression = ctx
+        let options = ctx.options();
+
+        // Ignore `ref` attributes if configured
+        if options.ignore_refs {
+            if let Ok(name) = ctx.query().name() {
+                if name.text() == "ref" {
+                    return None;
+                }
+            }
+        }
+
+        // Ignore lowercase DOM components if configured
+        if options.ignore_dom_components {
+            if let Some(opening) = ctx
+                .query()
+                .syntax()
+                .ancestors()
+                .find_map(biome_js_syntax::JsxOpeningElement::cast)
+            {
+                if let Ok(token) = opening.name().and_then(|n| n.value_token()) {
+                    if token.text().chars().next().is_some_and(|c| c.is_ascii_lowercase()) {
+                        return None;
+                    }
+                }
+            }
+        }
+
+        let expression: AnyJsExpression = ctx
             .query()
             .initializer()?
             .value()
             .ok()?
             .as_jsx_expression_attribute_value()?
             .expression()
             .ok()?;

75-84: Gate arrow/function matches behind options

Allow users to enable arrow/functions if desired.

-            AnyJsExpression::JsArrowFunctionExpression(_) => Some(NoJsxPropsBindState {
+            AnyJsExpression::JsArrowFunctionExpression(_) if !ctx.options().allow_arrow_functions => Some(NoJsxPropsBindState {
                 invalid_kind: InvalidKind::ArrowFunction,
                 attribute_range: expression.range(),
             }),
 
-            AnyJsExpression::JsFunctionExpression(_) => Some(NoJsxPropsBindState {
+            AnyJsExpression::JsFunctionExpression(_) if !ctx.options().allow_functions => Some(NoJsxPropsBindState {
                 invalid_kind: InvalidKind::Function,
                 attribute_range: expression.range(),
             }),

85-102: .bind() detection: support computed/optional members and honour option

Covers only static members and ignores allow_bind. Add optional/chain support and skip non-member calls.

-            AnyJsExpression::JsCallExpression(call) => {
-                // This will still throw a false positive on e.g. window.bind()
-                let is_bind = call
-                    .callee()
-                    .ok()
-                    .and_then(|c| c.as_js_static_member_expression().cloned())
-                    .and_then(|m| m.member().ok())
-                    .and_then(|n| n.value_token().ok())
-                    .map_or(false, |t| t.text() == "bind");
-                if is_bind {
-                    Some(NoJsxPropsBindState {
-                        invalid_kind: InvalidKind::Bind,
-                        attribute_range: expression.range(),
-                    })
-                } else {
-                    None
-                }
-            }
+            AnyJsExpression::JsCallExpression(call) => {
+                if ctx.options().allow_bind {
+                    return None;
+                }
+                use biome_js_syntax::AnyJsMemberExpression;
+
+                // Unwrap optional chaining: (obj.bind)?.(...)
+                let callee = call
+                    .callee()
+                    .ok()
+                    .and_then(|c| if let Some(chain) = c.as_js_chain_expression() { chain.expression().ok() } else { Some(c) });
+
+                let member = callee.as_ref().and_then(|c| c.as_js_member_expression().cloned());
+                let is_bind = member
+                    .and_then(|m: AnyJsMemberExpression| m.member_name().ok())
+                    .and_then(|n| n.value_token().ok())
+                    .map(|t| t.text() == "bind")
+                    .unwrap_or(false);
+
+                if is_bind {
+                    Some(NoJsxPropsBindState {
+                        invalid_kind: InvalidKind::Bind,
+                        // Highlight callee if available (e.g. `obj.bind`)
+                        attribute_range: callee.map(|c| c.range()).unwrap_or_else(|| expression.range()),
+                    })
+                } else {
+                    None
+                }
+            }

Add tests:

  • invalid: onClick={obj["bind"](this)}, onClick={obj.bind?.(this)}
  • valid: onClick={foo()}
🧹 Nitpick comments (5)
crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalid.jsx (3)

1-10: Add edge cases for .bind() and a control for plain calls

Please add:

  • invalid: <Foo onClick={obj["bind"](this)} />, <Foo onClick={obj.bind?.(this)} />
  • valid: <Foo onClick={foo()} /> (should not be flagged)

These will guard against false positives and missed .bind() variants.

Happy to open a quick PR with these fixtures.


12-25: Cover arrow-function components (current logic likely misses them)

Add cases like:

  • invalid: const Foo = () => { const onClick = () => {}; return <Bar onClick={onClick} /> }
  • invalid: const Foo = () => { function onClick() {}; return <Bar onClick={onClick} /> }

The implementation checks only for JS_FUNCTION_DECLARATION ancestors, so arrow/function-expression components are currently slipping through. Tests will expose that.

I can add these and wire snapshots.


12-25: Avoid reusing the same component name thrice

Minor: redefining function Foo() three times is a bit noisy for readers. Consider renaming to FooDecl, FooArrowVar, FooFuncVar.

crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (2)

109-113: Remove leftover debug comments

The dbg! comments can go; they distract in production rule code.

-                // dbg!(binding.syntax().kind());
-                // dbg!(identifier.syntax().kind());
-                // dbg!(declaration.syntax().kind());

154-169: Diagnostic polish: note can suggest useCallback with deps

Minor copy tweak: “Consider extracting the function or wrapping it in useCallback with an appropriate dependency list.” This mirrors React guidance.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e57678 and 826a0e2.

⛔ Files ignored due to path filters (2)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalid.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/valid.jsx.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (3)
  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalid.jsx (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/valid.jsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/valid.jsx
🧰 Additional context used
📓 Path-based instructions (4)
crates/biome_*_{syntax,parser,formatter,analyze,factory,semantic}/**

📄 CodeRabbit inference engine (CLAUDE.md)

Maintain the per-language crate structure: biome_{lang}_{syntax,parser,formatter,analyze,factory,semantic}

Files:

  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalid.jsx
  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
crates/biome_*/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place core crates under /crates/biome_*/

Files:

  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalid.jsx
  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
**/tests/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place test files under a tests/ directory in each crate

Files:

  • crates/biome_js_analyze/tests/specs/nursery/noJsxPropsBind/invalid.jsx
**/*.{rs,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Format Rust and TOML files before committing (use just f/just format).

Files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
🧠 Learnings (10)
📓 Common learnings
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/lib/src/lint/nursery/**/*.rs : Place all new rules in the nursery group under biome_js_analyze/lib/src/lint/nursery
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/**/src/**/*.rs : When deprecating a rule, add deprecated: "<reason>" to declare_lint_rule!

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/lib/src/lint/nursery/**/*.rs : Place all new rules in the nursery group under biome_js_analyze/lib/src/lint/nursery

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/**/src/**/*.rs : Avoid unnecessary string allocations in rules; compare against &str or TokenText instead of calling to_string()

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/**/src/**/*.rs : If a rule provides a code action, declare fix_kind in declare_lint_rule!

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/**/src/**/*.rs : Set the language field in declare_lint_rule! to the most specific applicable language (js/jsx/ts/tsx)

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/**/src/**/*.rs : When porting a lint rule, set sources in declare_lint_rule! (RuleSource::Eslint(...).same() or .inspired())

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_rule_options/lib/**/*.rs : Define per‑rule options in the biome_rule_options crate (one file per rule, e.g., use_my_rule.rs)

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_rule_options/lib/**/*.rs : Derive Serialize, Deserialize, Deserializable (and schemars JsonSchema behind feature) on options types; use serde attributes rename_all="camelCase", deny_unknown_fields, default

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/**/src/**/*.rs : In declare_lint_rule! metadata, set version: "next"

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs

Comment on lines 103 to 149
AnyJsExpression::JsIdentifierExpression(identifier) => {
let model = ctx.model();
let binding: Binding = model.binding(&identifier.name().ok()?)?;

let declaration = binding.tree().declaration()?;

// dbg!(binding.syntax().kind());
// dbg!(identifier.syntax().kind());
// dbg!(declaration.syntax().kind());

match &declaration {
AnyJsBindingDeclaration::JsFunctionDeclaration(_) => {
// Global functions are fine.
// This is probably overly simplistic
// Also I don't understand why I need to skip the first ancestor
// It seems like the first ancestor of a function declaration is itself a
// function declaration??
if !declaration.syntax().ancestors().skip(1).any(|anc| {
anc.kind() == biome_js_syntax::JsSyntaxKind::JS_FUNCTION_DECLARATION
}) {
return None;
}
return Some(NoJsxPropsBindState {
invalid_kind: InvalidKind::Function,
attribute_range: expression.range(),
});
}
AnyJsBindingDeclaration::JsVariableDeclarator(variable_declarator) => {
// dbg!("It's some kind of variable!");
// dbg!(&variable_declarator);
match variable_declarator.initializer()?.expression().ok()? {
AnyJsExpression::JsFunctionExpression(_)
| AnyJsExpression::JsArrowFunctionExpression(_) => {
if !declaration.syntax().ancestors().any(|anc| anc.kind() == biome_js_syntax::JsSyntaxKind::JS_FUNCTION_DECLARATION) {
return None
}
return Some(NoJsxPropsBindState {
invalid_kind: InvalidKind::Function,
attribute_range: expression.range(),
});
}
_ => None,
}
}
_ => None,
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Identifier path: misses arrow/function components; fix function-like ancestor check

The ancestor check looks only for JS_FUNCTION_DECLARATION; arrow/function-expression components won’t be flagged. Also the skip(1) hack is brittle. Check for any function-like ancestor.

-                    AnyJsBindingDeclaration::JsFunctionDeclaration(_) => {
-                        // Global functions are fine.
-                        // This is probably overly simplistic
-                        // Also I don't understand why I need to skip the first ancestor
-                        // It seems like the first ancestor of a function declaration is itself a
-                        // function declaration??
-                        if !declaration.syntax().ancestors().skip(1).any(|anc| {
-                            anc.kind() == biome_js_syntax::JsSyntaxKind::JS_FUNCTION_DECLARATION
-                        }) {
-                            return None;
-                        }
-                        return Some(NoJsxPropsBindState {
-                            invalid_kind: InvalidKind::Function,
-                            attribute_range: expression.range(),
-                        });
-                    }
+                    AnyJsBindingDeclaration::JsFunctionDeclaration(_) => {
+                        use biome_js_syntax::JsSyntaxKind::*;
+                        // Valid only when declared at module scope (no function-like ancestor beyond itself).
+                        let is_nested_in_fn_like = declaration
+                            .syntax()
+                            .ancestors()
+                            .skip(1)
+                            .any(|a| matches!(a.kind(), JS_FUNCTION_DECLARATION | JS_FUNCTION_EXPRESSION | JS_ARROW_FUNCTION_EXPRESSION));
+                        if is_nested_in_fn_like {
+                            Some(NoJsxPropsBindState { invalid_kind: InvalidKind::Function, attribute_range: expression.range() })
+                        } else {
+                            None
+                        }
+                    }
@@
-                    AnyJsBindingDeclaration::JsVariableDeclarator(variable_declarator) => {
+                    AnyJsBindingDeclaration::JsVariableDeclarator(variable_declarator) => {
                         // dbg!("It's some kind of variable!");
                         // dbg!(&variable_declarator);
-                        match variable_declarator.initializer()?.expression().ok()? {
+                        match variable_declarator.initializer()?.expression().ok()? {
                             AnyJsExpression::JsFunctionExpression(_)
                             | AnyJsExpression::JsArrowFunctionExpression(_) => {
-                                if !declaration.syntax().ancestors().any(|anc| anc.kind() == biome_js_syntax::JsSyntaxKind::JS_FUNCTION_DECLARATION) {
-                                    return None
-                                }
-                                return Some(NoJsxPropsBindState {
-                                    invalid_kind: InvalidKind::Function,
-                                    attribute_range: expression.range(),
-                                });
+                                use biome_js_syntax::JsSyntaxKind::*;
+                                let is_inside_fn_like = declaration
+                                    .syntax()
+                                    .ancestors()
+                                    .any(|a| matches!(a.kind(), JS_FUNCTION_DECLARATION | JS_FUNCTION_EXPRESSION | JS_ARROW_FUNCTION_EXPRESSION));
+                                if is_inside_fn_like {
+                                    Some(NoJsxPropsBindState { invalid_kind: InvalidKind::Function, attribute_range: expression.range() })
+                                } else {
+                                    None
+                                }
                             }
                             _ => None,
                         }
                     }

Please also add tests for arrow-function components as noted in the fixture comment.

🤖 Prompt for AI Agents
In crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs around lines
103 to 149, the current ancestor checks only detect JS_FUNCTION_DECLARATION (and
uses a brittle skip(1)) so arrow/function expression components are missed;
change the logic to look for any function-like ancestor
(JS_FUNCTION_DECLARATION, JS_FUNCTION_EXPRESSION, JS_ARROW_FUNCTION_EXPRESSION)
using a single ancestors().any(...) check (remove skip(1) usage) in both the
declaration and variable_declarator branches, and ensure the same
InvalidKind::Function path is returned when any of those ancestors are found;
additionally add unit/fixture tests covering arrow-function components as
indicated by the fixture comment.

@sgarcialaguna
Copy link
Contributor Author

sgarcialaguna commented Sep 7, 2025

Okay, made some progress. The code to check if a variable is global is almost certainly wrong, definitely hacky, I know that much.

Comment on lines 118 to 119
// It seems like the first ancestor of a function declaration is itself a
// function declaration??
Copy link
Contributor

Choose a reason for hiding this comment

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

ancestors() includes the node itself, so this is expected.

Comment on lines 120 to 124
if !declaration.syntax().ancestors().skip(1).any(|anc| {
anc.kind() == biome_js_syntax::JsSyntaxKind::JS_FUNCTION_DECLARATION
}) {
return None;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm assuming this is the globals check? You should be consulting the semantic model for this info. Global references have no bindings that they map to. Take a look at noConsoleLog for a good example.

Copy link
Contributor Author

@sgarcialaguna sgarcialaguna Sep 8, 2025

Choose a reason for hiding this comment

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

Hmm, maybe global isn't quite the correct term here, or I am still missing something. I don't mean to check where anything is defined on window, which it looks likes no_console is doing. I'm just looking if the function was declared outside a component

const onGlobal = function () {};
function Bar() {
    const onLocal = function () {};
    return <Foo onClick={onGlobal} onMouseDown={onLocal} />
}

In this snippet, onGlobal is fine, but onLocal is not.

I tried adapting the code from no_console:

let (reference, name) = global_identifier(&expression)?;
let is_global = model.binding(&reference).is_none();
dbg!(name, is_global);

But this returns false for both onGlobal and onLocal

Copy link
Member

Choose a reason for hiding this comment

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

If you aren't looking for any globals, then you don't need the function global_identifier. Just use model.binding by passing the correct reference.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

let is_global = model
    .binding(&expression.as_js_reference_identifier()?)
    .is_none();
dbg!(is_global);

This fits the types, but also returns false for both cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I fear I'm stuck on this one. Anything I can think of to pass to model.binding is never none. I would need some more help here.

Copy link
Member

@ematipico ematipico Sep 12, 2025

Choose a reason for hiding this comment

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

Then it's fine if we don't cover this use case for now. We can merge it now and come back later. A nursery rule doesn't have to be perfect at the first try. You can keep the original task open and leave a comment saying what's left after we merge this one. What do you think @sgarcialaguna ?

Copy link
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.

Actionable comments posted: 0

♻️ Duplicate comments (6)
crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (6)

38-45: Wire options schema in biome_rule_options (separate file)

Your rule reads NoJsxPropsBindOptions but doesn’t use fields unless they exist. Define them to match eslint flexibility.

Outside this file, add in crates/biome_rule_options/src/no_jsx_props_bind.rs:

use biome_analyze::RuleOptions;
use biome_deserialize_macros::Merge;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Default, Serialize, Deserialize, Merge, RuleOptions)]
#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
pub struct NoJsxPropsBindOptions {
    pub allow_arrow_functions: bool,
    pub allow_functions: bool,
    pub allow_bind: bool,
    pub ignore_refs: bool,
    pub ignore_dom_components: bool,
}

And expose the module in crates/biome_rule_options/src/lib.rs:

pub mod no_jsx_props_bind;

Happy to open a tiny follow‑up PR wiring docs and snapshots.


75-85: Gate arrow/function reports behind options

Honour allow_arrow_functions and allow_functions.

-        match &expression {
-            AnyJsExpression::JsArrowFunctionExpression(_) => Some(NoJsxPropsBindState {
+        match &expression {
+            AnyJsExpression::JsArrowFunctionExpression(_) if !options.allow_arrow_functions => Some(NoJsxPropsBindState {
                 invalid_kind: InvalidKind::ArrowFunction,
                 attribute_range: expression.range(),
             }),
 
-            AnyJsExpression::JsFunctionExpression(_) => Some(NoJsxPropsBindState {
+            AnyJsExpression::JsFunctionExpression(_) if !options.allow_functions => Some(NoJsxPropsBindState {
                 invalid_kind: InvalidKind::Function,
                 attribute_range: expression.range(),
             }),

133-145: Variable declarator path: include arrow/function components, mirror logic

Symmetry with the function‑declaration branch; treat any function‑like ancestor as “local”.

-                        match variable_declarator.initializer()?.expression().ok()? {
+                        match variable_declarator.initializer()?.expression().ok()? {
                             AnyJsExpression::JsFunctionExpression(_)
                             | AnyJsExpression::JsArrowFunctionExpression(_) => {
-                                if !declaration.syntax().ancestors().any(|anc| anc.kind() == biome_js_syntax::JsSyntaxKind::JS_FUNCTION_DECLARATION) {
-                                    return None
-                                }
-                                Some(NoJsxPropsBindState {
-                                    invalid_kind: InvalidKind::Function,
-                                    attribute_range: expression.range(),
-                                })
+                                use biome_js_syntax::JsSyntaxKind::*;
+                                let inside_fn_like = declaration
+                                    .syntax()
+                                    .ancestors()
+                                    .any(|a| matches!(a.kind(), JS_FUNCTION_DECLARATION | JS_FUNCTION_EXPRESSION | JS_ARROW_FUNCTION_EXPRESSION));
+                                if inside_fn_like {
+                                    Some(NoJsxPropsBindState {
+                                        invalid_kind: InvalidKind::Function,
+                                        attribute_range: expression.range(),
+                                    })
+                                } else {
+                                    None
+                                }
                             }
                             _ => None,
                         }

65-75: Respect options early; skip refs and DOM components

Short‑circuit per eslint parity: ignore ref attributes and built‑in DOM components when configured.

-    fn run(ctx: &RuleContext<Self>) -> Self::Signals {
-        let expression: AnyJsExpression = ctx
+    fn run(ctx: &RuleContext<Self>) -> Self::Signals {
+        let options = ctx.options();
+
+        // Ignore ref attributes if configured
+        if options.ignore_refs {
+            if let Ok(name) = ctx.query().name() {
+                if name.text() == "ref" {
+                    return None;
+                }
+            }
+        }
+
+        // Ignore lowercase (built-in) DOM components if configured
+        if options.ignore_dom_components {
+            if let Some(opening) = ctx
+                .query()
+                .syntax()
+                .ancestors()
+                .find_map(JsxOpeningElement::cast)
+            {
+                if let Ok(token) = opening.name().and_then(|n| n.value_token()) {
+                    if token.text().chars().next().is_some_and(|c| c.is_ascii_lowercase()) {
+                        return None;
+                    }
+                }
+            }
+        }
+
+        let expression: AnyJsExpression = ctx
             .query()
             .initializer()?
             .value()
             .ok()?
             .as_jsx_expression_attribute_value()?
             .expression()
             .ok()?;

113-129: Fix “global vs local” check; consider any function‑like ancestor (no skip hack)

This currently only checks JS_FUNCTION_DECLARATION and uses a brittle skip(1). Include arrow/function expressions and drop the hack.

-                    AnyJsBindingDeclaration::JsFunctionDeclaration(_) => {
-                        // Global functions are fine.
-                        // This is probably overly simplistic
-                        // Also I don't understand why I need to skip the first ancestor
-                        // It seems like the first ancestor of a function declaration is itself a
-                        // function declaration??
-                        if !declaration.syntax().ancestors().skip(1).any(|anc| {
-                            anc.kind() == biome_js_syntax::JsSyntaxKind::JS_FUNCTION_DECLARATION
-                        }) {
-                            return None;
-                        }
-                        return Some(NoJsxPropsBindState {
-                            invalid_kind: InvalidKind::Function,
-                            attribute_range: expression.range(),
-                        });
-                    }
+                    AnyJsBindingDeclaration::JsFunctionDeclaration(_) => {
+                        use biome_js_syntax::JsSyntaxKind::*;
+                        // Flag only when declared inside a function-like (component boundary).
+                        let is_nested_in_fn_like = declaration
+                            .syntax()
+                            .ancestors()
+                            .skip(1) // exclude the declaration itself
+                            .any(|a| matches!(a.kind(), JS_FUNCTION_DECLARATION | JS_FUNCTION_EXPRESSION | JS_ARROW_FUNCTION_EXPRESSION));
+                        if is_nested_in_fn_like {
+                            Some(NoJsxPropsBindState {
+                                invalid_kind: InvalidKind::Function,
+                                attribute_range: expression.range(),
+                            })
+                        } else {
+                            None
+                        }
+                    }

85-102: .bind() detection: handle optional/computed members and honour option

Only flag member calls named “bind”; support obj["bind"](…) and optional chaining.

-            AnyJsExpression::JsCallExpression(call) => {
-                // This will still throw a false positive on e.g. window.bind()
-                let is_bind = call
-                    .callee()
-                    .ok()
-                    .and_then(|c| c.as_js_static_member_expression().cloned())
-                    .and_then(|m| m.member().ok())
-                    .and_then(|n| n.value_token().ok())
-                    .map_or(false, |t| t.text() == "bind");
-                if is_bind {
-                    Some(NoJsxPropsBindState {
-                        invalid_kind: InvalidKind::Bind,
-                        attribute_range: expression.range(),
-                    })
-                } else {
-                    None
-                }
-            }
+            AnyJsExpression::JsCallExpression(call) => {
+                if options.allow_bind {
+                    return None;
+                }
+                // Unwrap optional chain: (obj.bind)?.(...)
+                let callee = call
+                    .callee()
+                    .ok()
+                    .and_then(|c| {
+                        if let Some(chain) = c.as_js_chain_expression() {
+                            chain.expression().ok()
+                        } else {
+                            Some(c)
+                        }
+                    });
+
+                let member = callee.as_ref().and_then(|c| c.as_js_member_expression().cloned());
+                let is_bind = member
+                    .and_then(|m: AnyJsMemberExpression| m.member_name().ok())
+                    .and_then(|n| n.value_token().ok())
+                    .map(|t| t.text() == "bind")
+                    .unwrap_or(false);
+
+                if is_bind {
+                    Some(NoJsxPropsBindState {
+                        invalid_kind: InvalidKind::Bind,
+                        // Highlight just the callee when available
+                        attribute_range: callee.map(|c| c.range()).unwrap_or_else(|| expression.range()),
+                    })
+                } else {
+                    None
+                }
+            }

Add tests:

  • invalid: <Foo onClick={obj["bind"](this)} />, <Foo onClick={obj.bind?.(this)} />
  • valid: <Foo onClick={foo()} />
🧹 Nitpick comments (3)
crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (3)

5-5: Import the JSX/member helpers you use in the rule

You reference JSX opening elements and member expressions in suggested logic; import them here.

-use biome_js_syntax::{AnyJsExpression, JsxAttribute, binding_ext::AnyJsBindingDeclaration};
+use biome_js_syntax::{
+    AnyJsExpression,
+    JsxAttribute,
+    JsxOpeningElement,
+    AnyJsMemberExpression,
+    binding_ext::AnyJsBindingDeclaration,
+};

154-169: Diagnostic polish: include the prop name for context

Small DX win: show which prop triggered the diagnostic.

-    fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
+    fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
+        let prop = ctx.query().name().ok().map(|n| n.text()).unwrap_or("prop".into());
         let note = match state.invalid_kind {
             InvalidKind::ArrowFunction => "JSX props should not use arrow functions",
             InvalidKind::Bind => "JSX props should not use .bind()",
             InvalidKind::Function => "JSX props should not use function expressions",
         };
         Some(
             RuleDiagnostic::new(
                 rule_category!(),
                 state.attribute_range,
-                "Pass stable function references as props to avoid unnecessary rerenders.",
+                format!("Pass stable function references in `{prop}` to avoid unnecessary rerenders."),
             )
             .note(note)
             .note("Consider extracting the function or wrapping it in useCallback"),
         )
     }

85-95: Remove misleading comment

“false positive on window.bind()” isn’t actionable here. Let’s drop it to avoid confusion.

-                // This will still throw a false positive on e.g. window.bind()
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 826a0e2 and 36b75cd.

📒 Files selected for processing (1)
  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
crates/biome_*_{syntax,parser,formatter,analyze,factory,semantic}/**

📄 CodeRabbit inference engine (CLAUDE.md)

Maintain the per-language crate structure: biome_{lang}_{syntax,parser,formatter,analyze,factory,semantic}

Files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
crates/biome_*/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place core crates under /crates/biome_*/

Files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
**/*.rs

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Format all Rust source files before committing (just f)

Files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
🧠 Learnings (10)
📓 Common learnings
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/lib/src/lint/nursery/**/*.rs : Place all new rules in the nursery group under biome_js_analyze/lib/src/lint/nursery
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/**/src/**/*.rs : In declare_lint_rule! metadata, set version: "next"

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/**/src/**/*.rs : When deprecating a rule, add deprecated: "<reason>" to declare_lint_rule!

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/**/src/**/*.rs : If a rule provides a code action, declare fix_kind in declare_lint_rule!

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/lib/src/lint/nursery/**/*.rs : Place all new rules in the nursery group under biome_js_analyze/lib/src/lint/nursery

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/**/src/**/*.rs : Set the language field in declare_lint_rule! to the most specific applicable language (js/jsx/ts/tsx)

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/**/src/**/*.rs : Avoid unnecessary string allocations in rules; compare against &str or TokenText instead of calling to_string()

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/**/src/**/*.rs : When porting a lint rule, set sources in declare_lint_rule! (RuleSource::Eslint(...).same() or .inspired())

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_rule_options/lib/**/*.rs : Define per‑rule options in the biome_rule_options crate (one file per rule, e.g., use_my_rule.rs)

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs
📚 Learning: 2025-09-05T09:13:58.901Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-05T09:13:58.901Z
Learning: Applies to crates/biome_analyze/crates/biome_rule_options/lib/**/*.rs : Derive Serialize, Deserialize, Deserializable (and schemars JsonSchema behind feature) on options types; use serde attributes rename_all="camelCase", deny_unknown_fields, default

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_jsx_props_bind.rs

@sgarcialaguna sgarcialaguna requested a review from dyc3 September 12, 2025 15:54
Comment on lines 151 to 164
let note = match state.invalid_kind {
InvalidKind::ArrowFunction => "JSX props should not use arrow functions",
InvalidKind::Bind => "JSX props should not use .bind()",
InvalidKind::Function => "JSX props should not use function expressions",
};
Some(
RuleDiagnostic::new(
rule_category!(),
state.attribute_range,
"Pass stable function references as props to avoid unnecessary rerenders.",
)
.note(note)
.note("Consider extracting the function or wrapping it in useCallback"),
)
Copy link
Contributor

Choose a reason for hiding this comment

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

The message should explain that the flagged code creates a new function every render.

@sgarcialaguna sgarcialaguna requested a review from dyc3 September 15, 2025 08:35
Copy link
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. Nice work!

Copy link
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.

Sorry it's been so long, this one slipped through the cracks. But, this needs a changeset, and a rebase to fix the merge conflicts (you can just run the codegen to fix them). Still interested in this PR?

@sgarcialaguna
Copy link
Contributor Author

No worries, I've been dealing with some real-life stuff. I'm still interested, I'll look to get it done towards the weekend.

@Netail
Copy link
Member

Netail commented Dec 19, 2025

No worries, I've been dealing with some real-life stuff. I'm still interested, I'll look to get it done towards the weekend.

I got you ;)

Copy link
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.

Actionable comments posted: 0

🧹 Nitpick comments (1)
.changeset/many-candies-see.md (1)

5-11: Consider adding a valid example to complete the guidance.

The changeset clearly describes the rule and shows an invalid pattern. However, including a valid example (e.g., extracting the function or using useCallback) would help users understand the recommended approach and provide better contrast.

Suggested enhancement
 **Invalid:**
 
 ```jsx
 <Foo onClick={() => console.log('Hello!')}></Foo>

+Valid:
+
+jsx +const handleClick = () => console.log('Hello!'); +<Foo onClick={handleClick}></Foo> +


Alternatively, if space is a concern, you could keep it brief with just the extracted function approach shown.

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used**: Path: .coderabbit.yaml

**Review profile**: CHILL

**Plan**: Pro

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 72f79c33a3a27fad106a01a636c604d612c5dd67 and 702b11f235f026233482c744e9227192540fe39c.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `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 `**`

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `.changeset/many-candies-see.md` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>📓 Path-based instructions (1)</summary>

<details>
<summary>.changeset/*.md</summary>


**📄 CodeRabbit inference engine (CONTRIBUTING.md)**

> Write changesets that are concise (1-3 sentences), user-focused, use past tense for actions taken and present tense for Biome behavior, include code examples for rules, and end sentences with periods

Files:
- `.changeset/many-candies-see.md`

</details>

</details><details>
<summary>🧠 Learnings (3)</summary>

<details>
<summary>📓 Common learnings</summary>

Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-11-27T23:04:02.022Z
Learning: Applies to crates/biome_analyze//*analyze/src/lint//*.rs : Use the noInvalid prefix for rules that report runtime errors from mistyping (e.g., noInvalidConstructorSuper)


</details>
<details>
<summary>📚 Learning: 2025-11-27T23:04:02.022Z</summary>

Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-11-27T23:04:02.022Z
Learning: Applies to crates/biome_analyze//*analyze/src/lint/nursery//*.rs : New rules must be placed inside the nursery group as an incubation space exempt from semantic versioning, with promotion to appropriate groups done during minor/major releases


**Applied to files:**
- `.changeset/many-candies-see.md`

</details>
<details>
<summary>📚 Learning: 2025-11-27T23:04:02.022Z</summary>

Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-11-27T23:04:02.022Z
Learning: Commit rule changes with message format: feat(biome_<crate>): <ruleName> to follow Biome's conventional commit style


**Applied to files:**
- `.changeset/many-candies-see.md`

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)</summary>

* GitHub Check: autofix
* GitHub Check: Bench (biome_js_formatter)
* GitHub Check: Bench (biome_js_parser)
* GitHub Check: Bench (biome_js_analyze)
* GitHub Check: Test Node.js API
* GitHub Check: Check JS Files
* GitHub Check: Test (depot-ubuntu-24.04-arm-16)
* GitHub Check: Lint project (depot-ubuntu-24.04-arm-16)
* GitHub Check: Documentation
* GitHub Check: Test (depot-windows-2022-16)
* GitHub Check: Lint project (depot-windows-2022)
* GitHub Check: Check Dependencies
* GitHub Check: End-to-end tests
* GitHub Check: Validate rules documentation

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

@Netail
Copy link
Member

Netail commented Dec 19, 2025

The valid.jsx did not seem to have valid syntax, threw 6 parser errors. Fixed those.

@Netail Netail self-requested a review December 19, 2025 12:37
@Netail Netail merged commit ab9af9a into biomejs:main Dec 29, 2025
20 checks passed
@github-actions github-actions bot mentioned this pull request Dec 29, 2025
@Netail
Copy link
Member

Netail commented Dec 29, 2025

Thank you for your contribution

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.

4 participants