Skip to content

Commit ab9af9a

Browse files
sgarcialagunaautofix-ci[bot]Netail
authored
feat: no-jsx-props-bind (#7410)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Maikel <[email protected]>
1 parent cc3e851 commit ab9af9a

15 files changed

Lines changed: 678 additions & 133 deletions

File tree

.changeset/many-candies-see.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the new nursery rule [`noJsxPropsBind`](https://biomejs.dev/linter/rules/no-jsx-props-bind). This rule disallows .bind(), arrow functions, or function expressions in JSX props.
6+
7+
**Invalid:**
8+
9+
```jsx
10+
<Foo onClick={() => console.log('Hello!')}></Foo>
11+
```

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

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

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

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

crates/biome_configuration/src/generated/domain_selector.rs

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

crates/biome_diagnostics_categories/src/categories.rs

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

crates/biome_js_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub mod no_for_in;
1515
pub mod no_import_cycles;
1616
pub mod no_increment_decrement;
1717
pub mod no_jsx_literals;
18+
pub mod no_jsx_props_bind;
1819
pub mod no_leaked_render;
1920
pub mod no_misused_promises;
2021
pub mod no_multi_assign;
@@ -56,4 +57,4 @@ pub mod use_spread;
5657
pub mod use_vue_consistent_define_props_declaration;
5758
pub mod use_vue_define_macros_order;
5859
pub mod use_vue_multi_word_component_names;
59-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_ambiguous_anchor_text :: NoAmbiguousAnchorText , self :: no_before_interactive_script_outside_document :: NoBeforeInteractiveScriptOutsideDocument , self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_duplicated_spread_props :: NoDuplicatedSpreadProps , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , self :: no_multi_assign :: NoMultiAssign , self :: no_multi_str :: NoMultiStr , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_proto :: NoProto , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_return_assign :: NoReturnAssign , self :: no_script_url :: NoScriptUrl , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_undeclared_env_vars :: NoUndeclaredEnvVars , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: no_vue_setup_props_reactivity_loss :: NoVueSetupPropsReactivityLoss , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_await_thenable :: UseAwaitThenable , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_destructuring :: UseDestructuring , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_regexp_exec :: UseRegexpExec , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_consistent_define_props_declaration :: UseVueConsistentDefinePropsDeclaration , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
60+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_ambiguous_anchor_text :: NoAmbiguousAnchorText , self :: no_before_interactive_script_outside_document :: NoBeforeInteractiveScriptOutsideDocument , self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_duplicated_spread_props :: NoDuplicatedSpreadProps , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_jsx_props_bind :: NoJsxPropsBind , self :: no_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , self :: no_multi_assign :: NoMultiAssign , self :: no_multi_str :: NoMultiStr , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_proto :: NoProto , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_return_assign :: NoReturnAssign , self :: no_script_url :: NoScriptUrl , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_undeclared_env_vars :: NoUndeclaredEnvVars , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: no_vue_setup_props_reactivity_loss :: NoVueSetupPropsReactivityLoss , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_await_thenable :: UseAwaitThenable , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_destructuring :: UseDestructuring , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_regexp_exec :: UseRegexpExec , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_consistent_define_props_declaration :: UseVueConsistentDefinePropsDeclaration , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
use biome_analyze::{
2+
Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
3+
};
4+
use biome_js_semantic::Binding;
5+
use biome_js_syntax::{AnyJsExpression, JsxAttribute, binding_ext::AnyJsBindingDeclaration};
6+
use biome_rowan::{AstNode, TextRange};
7+
use biome_rule_options::no_jsx_props_bind::NoJsxPropsBindOptions;
8+
9+
use crate::services::semantic::Semantic;
10+
11+
declare_lint_rule! {
12+
/// Disallow .bind(), arrow functions, or function expressions in JSX props
13+
///
14+
/// Using `.bind()` or creating a function inline in props creates a new function
15+
/// on every render, changing identity and defeating memoisation,
16+
/// which may cause unnecessary rerenders.
17+
///
18+
/// ### Invalid
19+
///
20+
/// ```jsx,expect_diagnostic
21+
/// <Foo onClick={this._handleClick.bind(this)}></Foo>
22+
/// ```
23+
///
24+
/// ```jsx,expect_diagnostic
25+
/// <Foo onClick={() => console.log('Hello!')}></Foo>
26+
/// ```
27+
///
28+
/// ```jsx,expect_diagnostic
29+
/// <Foo onClick={function () { console.log('Hello!'); }}></Foo>
30+
/// ```
31+
///
32+
/// ### Valid
33+
///
34+
/// ```jsx
35+
/// <Foo onClick={this._handleClick}></Foo>
36+
/// ```
37+
38+
pub NoJsxPropsBind {
39+
version: "next",
40+
name: "noJsxPropsBind",
41+
language: "jsx",
42+
recommended: false,
43+
sources: &[RuleSource::EslintReact("jsx-no-bind").inspired()],
44+
domains: &[RuleDomain::React],
45+
}
46+
}
47+
48+
enum InvalidKind {
49+
ArrowFunction,
50+
Function,
51+
Bind,
52+
}
53+
54+
pub struct NoJsxPropsBindState {
55+
invalid_kind: InvalidKind,
56+
attribute_range: TextRange,
57+
}
58+
59+
fn declaration_is_global(declaration: &AnyJsBindingDeclaration) -> bool {
60+
// TODO: This needs some work
61+
!declaration
62+
.syntax()
63+
.ancestors()
64+
.skip(1)
65+
.any(|anc| anc.kind() == biome_js_syntax::JsSyntaxKind::JS_FUNCTION_DECLARATION)
66+
}
67+
68+
impl Rule for NoJsxPropsBind {
69+
type Query = Semantic<JsxAttribute>;
70+
type State = NoJsxPropsBindState;
71+
type Signals = Option<Self::State>;
72+
type Options = NoJsxPropsBindOptions;
73+
74+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
75+
let expression: AnyJsExpression = ctx
76+
.query()
77+
.initializer()?
78+
.value()
79+
.ok()?
80+
.as_jsx_expression_attribute_value()?
81+
.expression()
82+
.ok()?;
83+
84+
match &expression {
85+
AnyJsExpression::JsArrowFunctionExpression(_) => Some(NoJsxPropsBindState {
86+
invalid_kind: InvalidKind::ArrowFunction,
87+
attribute_range: expression.range(),
88+
}),
89+
90+
AnyJsExpression::JsFunctionExpression(_) => Some(NoJsxPropsBindState {
91+
invalid_kind: InvalidKind::Function,
92+
attribute_range: expression.range(),
93+
}),
94+
AnyJsExpression::JsCallExpression(call) => {
95+
// TODO: This will still throw a false positive on e.g. window.bind()
96+
let is_bind = call
97+
.callee()
98+
.ok()
99+
.and_then(|c| c.as_js_static_member_expression().cloned())
100+
.and_then(|m| m.member().ok())
101+
.and_then(|n| n.value_token().ok())
102+
.is_some_and(|t| t.text() == "bind");
103+
if is_bind {
104+
Some(NoJsxPropsBindState {
105+
invalid_kind: InvalidKind::Bind,
106+
attribute_range: expression.range(),
107+
})
108+
} else {
109+
None
110+
}
111+
}
112+
AnyJsExpression::JsIdentifierExpression(identifier) => {
113+
let model = ctx.model();
114+
let binding: Binding = model.binding(&identifier.name().ok()?)?;
115+
116+
let declaration = binding.tree().declaration()?;
117+
118+
match &declaration {
119+
AnyJsBindingDeclaration::JsFunctionDeclaration(_) => {
120+
if declaration_is_global(&declaration) {
121+
return None;
122+
}
123+
Some(NoJsxPropsBindState {
124+
invalid_kind: InvalidKind::Function,
125+
attribute_range: expression.range(),
126+
})
127+
}
128+
AnyJsBindingDeclaration::JsVariableDeclarator(variable_declarator) => {
129+
match variable_declarator.initializer()?.expression().ok()? {
130+
AnyJsExpression::JsFunctionExpression(_)
131+
| AnyJsExpression::JsArrowFunctionExpression(_) => {
132+
if declaration_is_global(&declaration) {
133+
return None;
134+
}
135+
Some(NoJsxPropsBindState {
136+
invalid_kind: InvalidKind::Function,
137+
attribute_range: expression.range(),
138+
})
139+
}
140+
_ => None,
141+
}
142+
}
143+
_ => None,
144+
}
145+
}
146+
_ => None,
147+
}
148+
}
149+
150+
fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
151+
let note = match state.invalid_kind {
152+
InvalidKind::ArrowFunction => "JSX props should not use arrow functions",
153+
InvalidKind::Bind => "JSX props should not use .bind()",
154+
InvalidKind::Function => "JSX props should not use function expressions",
155+
};
156+
Some(
157+
RuleDiagnostic::new(
158+
rule_category!(),
159+
state.attribute_range,
160+
"This function will be recreated on every render. Pass stable function references as props to avoid unnecessary rerenders.",
161+
)
162+
.note(note)
163+
.note("Consider extracting the function or wrapping it in useCallback"),
164+
)
165+
}
166+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<>
2+
<Foo
3+
onClick={function () {
4+
alert("1337");
5+
}}
6+
/>
7+
<Foo onClick={handleClick.bind(this)} />
8+
<Foo onClick={this._handleClick.bind(this)} />
9+
<Foo onClick={() => console.log("Hello!")} />
10+
</>;
11+
12+
function Foo() {
13+
function onClick() {}
14+
return <Bar onClick={onClick}></Bar>;
15+
}
16+
17+
function Foo() {
18+
const onClick = () => {};
19+
return <Bar onClick={onClick}></Bar>;
20+
}
21+
22+
function Foo() {
23+
const onClick = function () {};
24+
return <Bar onClick={onClick}></Bar>;
25+
}

0 commit comments

Comments
 (0)