Skip to content

Commit dc4ae2c

Browse files
committed
feat(lint): add noReactDeps
1 parent 5fa96f0 commit dc4ae2c

File tree

13 files changed

+471
-92
lines changed

13 files changed

+471
-92
lines changed

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

+15
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

+105-86
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

+1
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ define_categories! {
171171
"lint/nursery/noPackagePrivateImports": "https://biomejs.dev/linter/rules/no-package-private-imports",
172172
"lint/nursery/noProcessEnv": "https://biomejs.dev/linter/rules/no-process-env",
173173
"lint/nursery/noProcessGlobal": "https://biomejs.dev/linter/rules/no-process-global",
174+
"lint/nursery/noReactDeps": "https://biomejs.dev/linter/rules/no-react-deps",
174175
"lint/nursery/noReactSpecificProps": "https://biomejs.dev/linter/rules/no-react-specific-props",
175176
"lint/nursery/noRestrictedImports": "https://biomejs.dev/linter/rules/no-restricted-imports",
176177
"lint/nursery/noRestrictedTypes": "https://biomejs.dev/linter/rules/no-restricted-types",

crates/biome_js_analyze/src/lib.rs

+6-5
Original file line numberDiff line numberDiff line change
@@ -202,18 +202,19 @@ mod tests {
202202
#[test]
203203
fn quick_test() {
204204
const SOURCE: &str = r#"
205-
let Component = (props) => <ol>{props.data.map(d => <li>{d.text}</li>)}</ol>;
205+
import { createEffect } from 'solid-js';
206+
207+
createEffect(() => {
208+
console.log(signal());
209+
});
206210
"#;
207211

208212
let parsed = parse(SOURCE, JsFileSource::tsx(), JsParserOptions::default());
209213

210214
let mut error_ranges: Vec<TextRange> = Vec::new();
211215
let options = AnalyzerOptions::default();
212-
let rule_filter = RuleFilter::Rule("nursery", "useForComponent");
213-
214-
let mut dependencies = Dependencies::default();
216+
let rule_filter = RuleFilter::Rule("nursery", "noReactDeps");
215217
dependencies.add("buffer", "latest");
216-
217218
let services = JsAnalyzerServices::from((
218219
Default::default(),
219220
project_layout_with_top_level_dependencies(dependencies),

crates/biome_js_analyze/src/lint/nursery.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub mod no_octal_escape;
2626
pub mod no_package_private_imports;
2727
pub mod no_process_env;
2828
pub mod no_process_global;
29+
pub mod no_react_deps;
2930
pub mod no_restricted_imports;
3031
pub mod no_restricted_types;
3132
pub mod no_secrets;
@@ -57,4 +58,4 @@ pub mod use_sorted_classes;
5758
pub mod use_strict_mode;
5859
pub mod use_trim_start_end;
5960
pub mod use_valid_autocomplete;
60-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_await_in_loop :: NoAwaitInLoop , self :: no_common_js :: NoCommonJs , self :: no_constant_binary_expression :: NoConstantBinaryExpression , self :: no_destructured_props :: NoDestructuredProps , self :: no_document_cookie :: NoDocumentCookie , self :: no_document_import_in_page :: NoDocumentImportInPage , self :: no_duplicate_else_if :: NoDuplicateElseIf , self :: no_dynamic_namespace_import_access :: NoDynamicNamespaceImportAccess , self :: no_enum :: NoEnum , self :: no_exported_imports :: NoExportedImports , self :: no_floating_promises :: NoFloatingPromises , self :: no_global_dirname_filename :: NoGlobalDirnameFilename , self :: no_head_element :: NoHeadElement , self :: no_head_import_in_document :: NoHeadImportInDocument , self :: no_img_element :: NoImgElement , self :: no_import_cycles :: NoImportCycles , self :: no_irregular_whitespace :: NoIrregularWhitespace , self :: no_nested_ternary :: NoNestedTernary , self :: no_noninteractive_element_interactions :: NoNoninteractiveElementInteractions , self :: no_octal_escape :: NoOctalEscape , self :: no_package_private_imports :: NoPackagePrivateImports , self :: no_process_env :: NoProcessEnv , self :: no_process_global :: NoProcessGlobal , self :: no_restricted_imports :: NoRestrictedImports , self :: no_restricted_types :: NoRestrictedTypes , self :: no_secrets :: NoSecrets , self :: no_static_element_interactions :: NoStaticElementInteractions , self :: no_substr :: NoSubstr , self :: no_template_curly_in_string :: NoTemplateCurlyInString , self :: no_ts_ignore :: NoTsIgnore , self :: no_unwanted_polyfillio :: NoUnwantedPolyfillio , self :: no_useless_escape_in_regex :: NoUselessEscapeInRegex , self :: no_useless_escape_in_string :: NoUselessEscapeInString , self :: no_useless_string_raw :: NoUselessStringRaw , self :: no_useless_undefined :: NoUselessUndefined , self :: use_adjacent_overload_signatures :: UseAdjacentOverloadSignatures , self :: use_aria_props_supported_by_role :: UseAriaPropsSupportedByRole , self :: use_at_index :: UseAtIndex , self :: use_collapsed_if :: UseCollapsedIf , self :: use_component_export_only_modules :: UseComponentExportOnlyModules , self :: use_consistent_curly_braces :: UseConsistentCurlyBraces , self :: use_consistent_member_accessibility :: UseConsistentMemberAccessibility , self :: use_consistent_object_definition :: UseConsistentObjectDefinition , self :: use_explicit_type :: UseExplicitType , self :: use_exports_last :: UseExportsLast , self :: use_for_component :: UseForComponent , self :: use_google_font_display :: UseGoogleFontDisplay , self :: use_google_font_preconnect :: UseGoogleFontPreconnect , self :: use_guard_for_in :: UseGuardForIn , self :: use_parse_int_radix :: UseParseIntRadix , self :: use_sorted_classes :: UseSortedClasses , self :: use_strict_mode :: UseStrictMode , self :: use_trim_start_end :: UseTrimStartEnd , self :: use_valid_autocomplete :: UseValidAutocomplete ,] } }
61+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_await_in_loop :: NoAwaitInLoop , self :: no_common_js :: NoCommonJs , self :: no_constant_binary_expression :: NoConstantBinaryExpression , self :: no_destructured_props :: NoDestructuredProps , self :: no_document_cookie :: NoDocumentCookie , self :: no_document_import_in_page :: NoDocumentImportInPage , self :: no_duplicate_else_if :: NoDuplicateElseIf , self :: no_dynamic_namespace_import_access :: NoDynamicNamespaceImportAccess , self :: no_enum :: NoEnum , self :: no_exported_imports :: NoExportedImports , self :: no_floating_promises :: NoFloatingPromises , self :: no_global_dirname_filename :: NoGlobalDirnameFilename , self :: no_head_element :: NoHeadElement , self :: no_head_import_in_document :: NoHeadImportInDocument , self :: no_img_element :: NoImgElement , self :: no_import_cycles :: NoImportCycles , self :: no_irregular_whitespace :: NoIrregularWhitespace , self :: no_nested_ternary :: NoNestedTernary , self :: no_noninteractive_element_interactions :: NoNoninteractiveElementInteractions , self :: no_octal_escape :: NoOctalEscape , self :: no_package_private_imports :: NoPackagePrivateImports , self :: no_process_env :: NoProcessEnv , self :: no_process_global :: NoProcessGlobal , self :: no_react_deps :: NoReactDeps , self :: no_restricted_imports :: NoRestrictedImports , self :: no_restricted_types :: NoRestrictedTypes , self :: no_secrets :: NoSecrets , self :: no_static_element_interactions :: NoStaticElementInteractions , self :: no_substr :: NoSubstr , self :: no_template_curly_in_string :: NoTemplateCurlyInString , self :: no_ts_ignore :: NoTsIgnore , self :: no_unwanted_polyfillio :: NoUnwantedPolyfillio , self :: no_useless_escape_in_regex :: NoUselessEscapeInRegex , self :: no_useless_escape_in_string :: NoUselessEscapeInString , self :: no_useless_string_raw :: NoUselessStringRaw , self :: no_useless_undefined :: NoUselessUndefined , self :: use_adjacent_overload_signatures :: UseAdjacentOverloadSignatures , self :: use_aria_props_supported_by_role :: UseAriaPropsSupportedByRole , self :: use_at_index :: UseAtIndex , self :: use_collapsed_if :: UseCollapsedIf , self :: use_component_export_only_modules :: UseComponentExportOnlyModules , self :: use_consistent_curly_braces :: UseConsistentCurlyBraces , self :: use_consistent_member_accessibility :: UseConsistentMemberAccessibility , self :: use_consistent_object_definition :: UseConsistentObjectDefinition , self :: use_explicit_type :: UseExplicitType , self :: use_exports_last :: UseExportsLast , self :: use_for_component :: UseForComponent , self :: use_google_font_display :: UseGoogleFontDisplay , self :: use_google_font_preconnect :: UseGoogleFontPreconnect , self :: use_guard_for_in :: UseGuardForIn , self :: use_parse_int_radix :: UseParseIntRadix , self :: use_sorted_classes :: UseSortedClasses , self :: use_strict_mode :: UseStrictMode , self :: use_trim_start_end :: UseTrimStartEnd , self :: use_valid_autocomplete :: UseValidAutocomplete ,] } }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
use biome_analyze::{
2+
context::RuleContext, declare_lint_rule, Ast, Rule, RuleDiagnostic, RuleDomain, RuleSource,
3+
RuleSourceKind,
4+
};
5+
use biome_console::markup;
6+
use biome_js_syntax::{AnyJsExpression, JsCallExpression};
7+
use biome_rowan::{AstNode, AstSeparatedList, TextRange};
8+
9+
declare_lint_rule! {
10+
/// Disallow usage of dependency arrays in `createEffect` and `createMemo`.
11+
///
12+
/// In Solid, `createEffect` and `createMemo` track dependencies automatically, it's no need to add dependency arrays.
13+
///
14+
/// ## Examples
15+
///
16+
/// ### Invalid
17+
///
18+
/// ```js,expect_diagnostic
19+
/// import { createEffect } from "solid-js";
20+
/// createEffect(() => {
21+
/// console.log(signal());
22+
/// }, [signal()]);
23+
/// ```
24+
///
25+
/// ```js,expect_diagnostic
26+
/// import { createEffect } from "solid-js";
27+
/// createEffect(() => {
28+
/// console.log(signal());
29+
/// }, [signal]);
30+
/// ```
31+
///
32+
/// ```js,expect_diagnostic
33+
/// import { createEffect } from "solid-js";
34+
/// const deps = [signal];
35+
/// createEffect(() => {
36+
/// console.log(signal());
37+
/// }, deps)
38+
/// ```
39+
///
40+
/// ```js,expect_diagnostic
41+
/// import { createMemo } from "solid-js";
42+
/// const value = createMemo(() => computeExpensiveValue(a(), b()), [a(), b()]);
43+
/// ```
44+
///
45+
/// ```js,expect_diagnostic
46+
/// import { createMemo } from "solid-js";
47+
/// const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b]);
48+
/// ```
49+
///
50+
/// ```js,expect_diagnostic
51+
/// import { createMemo } from "solid-js";
52+
/// const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b()]);
53+
/// ```
54+
///
55+
/// ```js,expect_diagnostic
56+
/// import { createMemo } from "solid-js";
57+
/// const deps = [a, b];
58+
/// const value = createMemo(() => computeExpensiveValue(a(), b()), deps);
59+
/// ```
60+
///
61+
/// ```js,expect_diagnostic
62+
/// import { createMemo } from "solid-js";
63+
/// const deps = [a, b];
64+
/// const memoFn = () => computeExpensiveValue(a(), b());
65+
/// const value = createMemo(memoFn, deps);
66+
/// ```
67+
///
68+
/// ### Valid
69+
///
70+
/// ```js
71+
/// import { createEffect } from "solid-js";
72+
/// createEffect(() => {
73+
/// console.log(signal());
74+
/// });
75+
/// ```
76+
///
77+
/// ```js
78+
/// import { createEffect } from "solid-js";
79+
/// createEffect((prev) => {
80+
/// console.log(signal());
81+
/// return prev + 1;
82+
/// }, 0);
83+
/// ```
84+
///
85+
/// ```js
86+
/// import { createEffect } from "solid-js";
87+
/// createEffect((prev) => {
88+
/// console.log(signal());
89+
/// return (prev || 0) + 1;
90+
/// });
91+
/// ```
92+
///
93+
/// ```js
94+
/// import { createEffect } from "solid-js";
95+
/// createEffect((prev) => {
96+
/// console.log(signal());
97+
/// return prev ? prev + 1 : 1;
98+
/// }, undefined);
99+
/// ```
100+
///
101+
/// ```js
102+
/// import { createMemo } from "solid-js";
103+
/// const value = createMemo(() => computeExpensiveValue(a(), b()));
104+
/// ```
105+
///
106+
/// ```js
107+
/// import { createMemo } from "solid-js";
108+
/// const sum = createMemo((prev) => input() + prev, 0);
109+
/// ```
110+
///
111+
/// ```js
112+
/// import { createEffect } from "solid-js";
113+
/// const args = [
114+
/// () => {
115+
/// console.log(signal());
116+
/// },
117+
/// [signal()],
118+
/// ];
119+
/// createEffect(...args);
120+
/// ```
121+
pub NoReactDeps {
122+
version: "next",
123+
name: "noReactDeps",
124+
language: "js",
125+
domains: &[RuleDomain::Solid],
126+
recommended: false,
127+
sources: &[RuleSource::EslintSolid("no-react-deps")],
128+
source_kind: RuleSourceKind::Inspired,
129+
}
130+
}
131+
132+
impl Rule for NoReactDeps {
133+
type Query = Ast<JsCallExpression>;
134+
type State = (String, TextRange);
135+
type Signals = Option<Self::State>;
136+
type Options = ();
137+
138+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
139+
let node = ctx.query();
140+
let callee = node.callee().ok()?;
141+
let ident = callee.as_js_identifier_expression()?.name().ok()?;
142+
let callee_name = ident.value_token().ok()?;
143+
let callee_name = callee_name.text_trimmed();
144+
145+
if callee_name != "createEffect" && callee_name != "createMemo" {
146+
return None;
147+
}
148+
149+
let arguments = node.arguments().ok()?.args();
150+
let len = arguments.len();
151+
let mut iter = arguments.into_iter();
152+
153+
let has_spread = iter.all(|arg| arg.is_ok_and(|arg| arg.as_js_spread().is_some()));
154+
155+
if len == 2 && !has_spread {
156+
let first_argument = iter.next()?.ok()?;
157+
let first_argument = first_argument.as_any_js_expression()?;
158+
159+
let is_first_arg_function_type =
160+
first_argument.as_js_arrow_function_expression().is_some()
161+
|| first_argument.as_js_function_expression().is_some();
162+
163+
let first_arg_parameter_len = match first_argument {
164+
AnyJsExpression::JsArrowFunctionExpression(node) => node.parameters().ok()?.len(),
165+
AnyJsExpression::JsFunctionExpression(node) => {
166+
node.parameters().ok()?.items().len()
167+
}
168+
_ => 0,
169+
};
170+
171+
let second_argument = iter.next()?.ok()?;
172+
let second_argument = second_argument.as_any_js_expression()?;
173+
let is_second_arg_array_type = second_argument.as_js_array_expression().is_some();
174+
175+
if is_first_arg_function_type
176+
&& first_arg_parameter_len == 0
177+
&& is_second_arg_array_type
178+
{
179+
return Some((callee_name.into(), second_argument.range()));
180+
}
181+
}
182+
183+
None
184+
}
185+
186+
fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
187+
let (callee_name, range) = state;
188+
Some(
189+
RuleDiagnostic::new(
190+
rule_category!(),
191+
range,
192+
markup! {
193+
"In Solid, "<Emphasis>{callee_name}</Emphasis>" doesn't accept a dependency array because it automatically tracks its dependencies."
194+
},
195+
)
196+
.note(markup! {
197+
"Please just remove the dependency array parameter here."
198+
})
199+
.note(markup! {
200+
"If you really need to override the list of dependencies, use \
201+
"<Hyperlink href="https://docs.solidjs.com/reference/reactive-utilities/on-util#on">"on"</Hyperlink>"."
202+
}),
203+
)
204+
}
205+
}

crates/biome_js_analyze/src/options.rs

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { createEffect, createMemo } from "solid-js";
2+
3+
createEffect(() => {
4+
console.log(signal());
5+
}, [signal()]);
6+
7+
createEffect(() => {
8+
console.log(signal());
9+
}, [signal]);
10+
11+
const deps = [signal];
12+
createEffect(() => {
13+
console.log(signal());
14+
}, deps);
15+
16+
const value = createMemo(() => computeExpensiveValue(a(), b()), [a(), b()]);
17+
18+
const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b]);
19+
20+
const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b()]);
21+
22+
const deps = [a, b];
23+
const value = createMemo(() => computeExpensiveValue(a(), b()), deps);
24+
25+
const deps = [a, b];
26+
const memoFn = () => computeExpensiveValue(a(), b());
27+
const value = createMemo(memoFn, deps);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: invalid.tsx
4+
snapshot_kind: text
5+
---
6+
# Input
7+
```tsx
8+
import { createEffect, createMemo } from "solid-js";
9+
10+
createEffect(() => {
11+
console.log(signal());
12+
}, [signal()]);
13+
14+
createEffect(() => {
15+
console.log(signal());
16+
}, [signal]);
17+
18+
const deps = [signal];
19+
createEffect(() => {
20+
console.log(signal());
21+
}, deps);
22+
23+
const value = createMemo(() => computeExpensiveValue(a(), b()), [a(), b()]);
24+
25+
const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b]);
26+
27+
const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b()]);
28+
29+
const deps = [a, b];
30+
const value = createMemo(() => computeExpensiveValue(a(), b()), deps);
31+
32+
const deps = [a, b];
33+
const memoFn = () => computeExpensiveValue(a(), b());
34+
const value = createMemo(memoFn, deps);
35+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { createEffect, createMemo } from "solid-js";
2+
3+
createEffect(() => {
4+
console.log(signal());
5+
});
6+
7+
createEffect((prev) => {
8+
console.log(signal());
9+
return prev + 1;
10+
}, 0);
11+
12+
createEffect((prev) => {
13+
console.log(signal());
14+
return (prev || 0) + 1;
15+
});
16+
17+
createEffect((prev) => {
18+
console.log(signal());
19+
return prev ? prev + 1 : 1;
20+
}, undefined);
21+
22+
const value = createMemo(() => computeExpensiveValue(a(), b()));
23+
24+
const sum = createMemo((prev) => input() + prev, 0);
25+
26+
const args = [() => { console.log(signal()); }, [signal()]];
27+
createEffect(...args);

0 commit comments

Comments
 (0)