Skip to content

Commit

Permalink
feat(lint): add noReactDeps
Browse files Browse the repository at this point in the history
  • Loading branch information
fireairforce committed Mar 3, 2025
1 parent 5fa96f0 commit dc4ae2c
Show file tree
Hide file tree
Showing 13 changed files with 471 additions and 92 deletions.
15 changes: 15 additions & 0 deletions crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

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

191 changes: 105 additions & 86 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ define_categories! {
"lint/nursery/noPackagePrivateImports": "https://biomejs.dev/linter/rules/no-package-private-imports",
"lint/nursery/noProcessEnv": "https://biomejs.dev/linter/rules/no-process-env",
"lint/nursery/noProcessGlobal": "https://biomejs.dev/linter/rules/no-process-global",
"lint/nursery/noReactDeps": "https://biomejs.dev/linter/rules/no-react-deps",
"lint/nursery/noReactSpecificProps": "https://biomejs.dev/linter/rules/no-react-specific-props",
"lint/nursery/noRestrictedImports": "https://biomejs.dev/linter/rules/no-restricted-imports",
"lint/nursery/noRestrictedTypes": "https://biomejs.dev/linter/rules/no-restricted-types",
Expand Down
11 changes: 6 additions & 5 deletions crates/biome_js_analyze/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,18 +202,19 @@ mod tests {
#[test]
fn quick_test() {
const SOURCE: &str = r#"
let Component = (props) => <ol>{props.data.map(d => <li>{d.text}</li>)}</ol>;
import { createEffect } from 'solid-js';
createEffect(() => {
console.log(signal());
});
"#;

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

let mut error_ranges: Vec<TextRange> = Vec::new();
let options = AnalyzerOptions::default();
let rule_filter = RuleFilter::Rule("nursery", "useForComponent");

let mut dependencies = Dependencies::default();
let rule_filter = RuleFilter::Rule("nursery", "noReactDeps");
dependencies.add("buffer", "latest");

let services = JsAnalyzerServices::from((
Default::default(),
project_layout_with_top_level_dependencies(dependencies),
Expand Down
3 changes: 2 additions & 1 deletion crates/biome_js_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub mod no_octal_escape;
pub mod no_package_private_imports;
pub mod no_process_env;
pub mod no_process_global;
pub mod no_react_deps;
pub mod no_restricted_imports;
pub mod no_restricted_types;
pub mod no_secrets;
Expand Down Expand Up @@ -57,4 +58,4 @@ pub mod use_sorted_classes;
pub mod use_strict_mode;
pub mod use_trim_start_end;
pub mod use_valid_autocomplete;
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 ,] } }
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 ,] } }
205 changes: 205 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery/no_react_deps.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
use biome_analyze::{
context::RuleContext, declare_lint_rule, Ast, Rule, RuleDiagnostic, RuleDomain, RuleSource,
RuleSourceKind,
};
use biome_console::markup;
use biome_js_syntax::{AnyJsExpression, JsCallExpression};
use biome_rowan::{AstNode, AstSeparatedList, TextRange};

declare_lint_rule! {
/// Disallow usage of dependency arrays in `createEffect` and `createMemo`.
///
/// In Solid, `createEffect` and `createMemo` track dependencies automatically, it's no need to add dependency arrays.
///
/// ## Examples
///
/// ### Invalid
///
/// ```js,expect_diagnostic
/// import { createEffect } from "solid-js";
/// createEffect(() => {
/// console.log(signal());
/// }, [signal()]);
/// ```
///
/// ```js,expect_diagnostic
/// import { createEffect } from "solid-js";
/// createEffect(() => {
/// console.log(signal());
/// }, [signal]);
/// ```
///
/// ```js,expect_diagnostic
/// import { createEffect } from "solid-js";
/// const deps = [signal];
/// createEffect(() => {
/// console.log(signal());
/// }, deps)
/// ```
///
/// ```js,expect_diagnostic
/// import { createMemo } from "solid-js";
/// const value = createMemo(() => computeExpensiveValue(a(), b()), [a(), b()]);
/// ```
///
/// ```js,expect_diagnostic
/// import { createMemo } from "solid-js";
/// const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b]);
/// ```
///
/// ```js,expect_diagnostic
/// import { createMemo } from "solid-js";
/// const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b()]);
/// ```
///
/// ```js,expect_diagnostic
/// import { createMemo } from "solid-js";
/// const deps = [a, b];
/// const value = createMemo(() => computeExpensiveValue(a(), b()), deps);
/// ```
///
/// ```js,expect_diagnostic
/// import { createMemo } from "solid-js";
/// const deps = [a, b];
/// const memoFn = () => computeExpensiveValue(a(), b());
/// const value = createMemo(memoFn, deps);
/// ```
///
/// ### Valid
///
/// ```js
/// import { createEffect } from "solid-js";
/// createEffect(() => {
/// console.log(signal());
/// });
/// ```
///
/// ```js
/// import { createEffect } from "solid-js";
/// createEffect((prev) => {
/// console.log(signal());
/// return prev + 1;
/// }, 0);
/// ```
///
/// ```js
/// import { createEffect } from "solid-js";
/// createEffect((prev) => {
/// console.log(signal());
/// return (prev || 0) + 1;
/// });
/// ```
///
/// ```js
/// import { createEffect } from "solid-js";
/// createEffect((prev) => {
/// console.log(signal());
/// return prev ? prev + 1 : 1;
/// }, undefined);
/// ```
///
/// ```js
/// import { createMemo } from "solid-js";
/// const value = createMemo(() => computeExpensiveValue(a(), b()));
/// ```
///
/// ```js
/// import { createMemo } from "solid-js";
/// const sum = createMemo((prev) => input() + prev, 0);
/// ```
///
/// ```js
/// import { createEffect } from "solid-js";
/// const args = [
/// () => {
/// console.log(signal());
/// },
/// [signal()],
/// ];
/// createEffect(...args);
/// ```
pub NoReactDeps {
version: "next",
name: "noReactDeps",
language: "js",
domains: &[RuleDomain::Solid],
recommended: false,
sources: &[RuleSource::EslintSolid("no-react-deps")],
source_kind: RuleSourceKind::Inspired,
}
}

impl Rule for NoReactDeps {
type Query = Ast<JsCallExpression>;
type State = (String, TextRange);
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();
let callee = node.callee().ok()?;
let ident = callee.as_js_identifier_expression()?.name().ok()?;
let callee_name = ident.value_token().ok()?;
let callee_name = callee_name.text_trimmed();

if callee_name != "createEffect" && callee_name != "createMemo" {
return None;
}

let arguments = node.arguments().ok()?.args();
let len = arguments.len();
let mut iter = arguments.into_iter();

let has_spread = iter.all(|arg| arg.is_ok_and(|arg| arg.as_js_spread().is_some()));

if len == 2 && !has_spread {
let first_argument = iter.next()?.ok()?;
let first_argument = first_argument.as_any_js_expression()?;

let is_first_arg_function_type =
first_argument.as_js_arrow_function_expression().is_some()
|| first_argument.as_js_function_expression().is_some();

let first_arg_parameter_len = match first_argument {
AnyJsExpression::JsArrowFunctionExpression(node) => node.parameters().ok()?.len(),
AnyJsExpression::JsFunctionExpression(node) => {
node.parameters().ok()?.items().len()
}
_ => 0,
};

let second_argument = iter.next()?.ok()?;
let second_argument = second_argument.as_any_js_expression()?;
let is_second_arg_array_type = second_argument.as_js_array_expression().is_some();

if is_first_arg_function_type
&& first_arg_parameter_len == 0
&& is_second_arg_array_type
{
return Some((callee_name.into(), second_argument.range()));
}
}

None
}

fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
let (callee_name, range) = state;
Some(
RuleDiagnostic::new(
rule_category!(),
range,
markup! {
"In Solid, "<Emphasis>{callee_name}</Emphasis>" doesn't accept a dependency array because it automatically tracks its dependencies."
},
)
.note(markup! {
"Please just remove the dependency array parameter here."
})
.note(markup! {
"If you really need to override the list of dependencies, use \
"<Hyperlink href="https://docs.solidjs.com/reference/reactive-utilities/on-util#on">"on"</Hyperlink>"."
}),
)
}
}
1 change: 1 addition & 0 deletions crates/biome_js_analyze/src/options.rs

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { createEffect, createMemo } from "solid-js";

createEffect(() => {
console.log(signal());
}, [signal()]);

createEffect(() => {
console.log(signal());
}, [signal]);

const deps = [signal];
createEffect(() => {
console.log(signal());
}, deps);

const value = createMemo(() => computeExpensiveValue(a(), b()), [a(), b()]);

const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b]);

const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b()]);

const deps = [a, b];
const value = createMemo(() => computeExpensiveValue(a(), b()), deps);

const deps = [a, b];
const memoFn = () => computeExpensiveValue(a(), b());
const value = createMemo(memoFn, deps);
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: invalid.tsx
snapshot_kind: text
---
# Input
```tsx
import { createEffect, createMemo } from "solid-js";

createEffect(() => {
console.log(signal());
}, [signal()]);

createEffect(() => {
console.log(signal());
}, [signal]);

const deps = [signal];
createEffect(() => {
console.log(signal());
}, deps);

const value = createMemo(() => computeExpensiveValue(a(), b()), [a(), b()]);

const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b]);

const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b()]);

const deps = [a, b];
const value = createMemo(() => computeExpensiveValue(a(), b()), deps);

const deps = [a, b];
const memoFn = () => computeExpensiveValue(a(), b());
const value = createMemo(memoFn, deps);
```
27 changes: 27 additions & 0 deletions crates/biome_js_analyze/tests/specs/nursery/noReactDeps/valid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { createEffect, createMemo } from "solid-js";

createEffect(() => {
console.log(signal());
});

createEffect((prev) => {
console.log(signal());
return prev + 1;
}, 0);

createEffect((prev) => {
console.log(signal());
return (prev || 0) + 1;
});

createEffect((prev) => {
console.log(signal());
return prev ? prev + 1 : 1;
}, undefined);

const value = createMemo(() => computeExpensiveValue(a(), b()));

const sum = createMemo((prev) => input() + prev, 0);

const args = [() => { console.log(signal()); }, [signal()]];
createEffect(...args);
Loading

0 comments on commit dc4ae2c

Please sign in to comment.