diff --git a/crates/biome_js_analyze/src/lint/nursery/use_component_export_only_modules.rs b/crates/biome_js_analyze/src/lint/nursery/use_component_export_only_modules.rs index c32ae1532b99..54d3520eed0a 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_component_export_only_modules.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_component_export_only_modules.rs @@ -5,7 +5,7 @@ use biome_console::markup; use biome_deserialize_macros::Deserializable; use biome_js_syntax::{ export_ext::{AnyJsExported, ExportedItem}, - AnyJsBindingPattern, AnyJsCallArgument, AnyJsExpression, AnyJsModuleItem, AnyJsStatement, + AnyJsBindingPattern, AnyJsExpression, AnyJsModuleItem, AnyJsStatement, JsCallExpression, JsModule, }; use biome_rowan::{AstNode, TextRange}; @@ -61,7 +61,7 @@ declare_lint_rule! { /// /// ```jsx /// import { memo } from 'react'; - /// const Component = () => <> + /// export const Component = () => <> /// export default memo(Component); /// ``` /// @@ -289,42 +289,48 @@ impl Rule for UseComponentExportOnlyModules { // Function that returns a standard React component const REACT_HOOKS: [&str; 2] = ["memo", "forwardRef"]; +/// Check if the function is a React Hook +fn is_hooked_component(f: &JsCallExpression) -> Option { + let fn_name = match f.callee().ok()? { + AnyJsExpression::JsIdentifierExpression(fn_name) => fn_name.text(), + AnyJsExpression::JsStaticMemberExpression(member) => member.member().ok()?.text(), + _ => return None, + }; + if !REACT_HOOKS.contains(&fn_name.as_str()) { + return None; + } + let args = f.arguments().ok()?; + let itr = args + .args() + .into_iter() + .filter_map(Result::ok) + .collect::>(); + if itr.len() != 1 { + return None; + } + match &itr[0].as_any_js_expression()? { + AnyJsExpression::JsArrowFunctionExpression(_) => Some(true), + AnyJsExpression::JsFunctionExpression(_) => Some(true), + AnyJsExpression::JsIdentifierExpression(arg) => { + Some(Case::identify(&arg.name().ok()?.text(), false) == Case::Pascal) + } + _ => None, + } +} + +/// Check if the exported item is a React component fn is_exported_react_component(any_exported_item: &ExportedItem) -> bool { - if let Some(AnyJsExported::AnyJsExpression(AnyJsExpression::JsCallExpression(f))) = + if let Some(exported_item_id) = any_exported_item.identifier.clone() { + Case::identify(&exported_item_id.text(), false) == Case::Pascal + && match any_exported_item.exported.clone() { + Some(exported) => !matches!(exported, AnyJsExported::TsEnumDeclaration(_)), + None => true, + } + } else if let Some(AnyJsExported::AnyJsExpression(AnyJsExpression::JsCallExpression(f))) = any_exported_item.exported.clone() { - if let Ok(AnyJsExpression::JsIdentifierExpression(fn_name)) = f.callee() { - if !REACT_HOOKS.contains(&fn_name.text().as_str()) { - return false; - } - let Ok(args) = f.arguments() else { - return false; - }; - let itr = args - .args() - .into_iter() - .filter_map(Result::ok) - .collect::>(); - if itr.len() != 1 { - return false; - } - let AnyJsCallArgument::AnyJsExpression(AnyJsExpression::JsIdentifierExpression(arg)) = - &itr[0] - else { - return false; - }; - let Ok(arg_name) = arg.name() else { - return false; - }; - return Case::identify(&arg_name.text(), false) == Case::Pascal; - } + is_hooked_component(&f).unwrap_or(false) + } else { + false } - let Some(exported_item_id) = any_exported_item.identifier.clone() else { - return false; - }; - Case::identify(&exported_item_id.text(), false) == Case::Pascal - && match any_exported_item.exported.clone() { - Some(exported) => !matches!(exported, AnyJsExported::TsEnumDeclaration(_)), - None => true, - } } diff --git a/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_default_wrapped_component.jsx b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_default_wrapped_component.jsx new file mode 100644 index 000000000000..145d48f2b797 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_default_wrapped_component.jsx @@ -0,0 +1,3 @@ +const Component = () => <> +const func = () => {} +export default func(Component) diff --git a/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_default_wrapped_component.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_default_wrapped_component.jsx.snap new file mode 100644 index 000000000000..0cdcf8e86f5f --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_default_wrapped_component.jsx.snap @@ -0,0 +1,32 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: invalid_default_wrapped_component.jsx +--- +# Input +```jsx +const Component = () => <> +const func = () => {} +export default func(Component) + +``` + +# Diagnostics +``` +invalid_default_wrapped_component.jsx:1:7 lint/nursery/useComponentExportOnlyModules ━━━━━━━━━━━━━━━ + + ! Components should be exported. + + > 1 │ const Component = () => <> + │ ^^^^^^^^^ + 2 │ const func = () => {} + 3 │ export default func(Component) + + i Fast Refresh only works when a file only exports components. + + i Consider separating component exports into a new file. + + i If it is not a component, it may not be following the variable naming conventions. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_hooked_component.jsx b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_hooked_component.jsx deleted file mode 100644 index dcdeedc646c0..000000000000 --- a/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_hooked_component.jsx +++ /dev/null @@ -1,2 +0,0 @@ -const Fuga = () => <> -export default hoge(Fuga) diff --git a/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_hooked_component.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_hooked_component.jsx.snap deleted file mode 100644 index 4273934988c6..000000000000 --- a/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_hooked_component.jsx.snap +++ /dev/null @@ -1,30 +0,0 @@ ---- -source: crates/biome_js_analyze/tests/spec_tests.rs -expression: invalid_hooked_component.jsx ---- -# Input -```jsx -const Fuga = () => <> -export default hoge(Fuga) - -``` - -# Diagnostics -``` -invalid_hooked_component.jsx:1:7 lint/nursery/useComponentExportOnlyModules ━━━━━━━━━━━━━━━━━━━━━━━━ - - ! Components should be exported. - - > 1 │ const Fuga = () => <> - │ ^^^^ - 2 │ export default hoge(Fuga) - 3 │ - - i Fast Refresh only works when a file only exports components. - - i Consider separating component exports into a new file. - - i If it is not a component, it may not be following the variable naming conventions. - - -``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_hooked_default_component_with_non_component.jsx b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_hooked_default_component_with_non_component.jsx new file mode 100644 index 000000000000..6d169fcf5c09 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_hooked_default_component_with_non_component.jsx @@ -0,0 +1,2 @@ +export const func = () => {}; +export default memo(() => <>); diff --git a/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_hooked_default_component_with_non_component.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_hooked_default_component_with_non_component.jsx.snap new file mode 100644 index 000000000000..c16d0edce210 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_hooked_default_component_with_non_component.jsx.snap @@ -0,0 +1,31 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: invalid_hooked_default_component_with_non_component.jsx +--- +# Input +```jsx +export const func = () => {}; +export default memo(() => <>); + +``` + +# Diagnostics +``` +invalid_hooked_default_component_with_non_component.jsx:1:14 lint/nursery/useComponentExportOnlyModules ━━━━━━━━━━ + + ! Exporting a non-component with components is not allowed. + + > 1 │ export const func = () => {}; + │ ^^^^ + 2 │ export default memo(() => <>); + 3 │ + + i Fast Refresh only works when a file only exports components. + + i Consider separating non-component exports into a new file. + + i If it is a component, it may not be following the variable naming conventions. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_hooked_non_component.jsx b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_hooked_non_component.jsx index 8199c3aca09c..af1d0ae861b3 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_hooked_non_component.jsx +++ b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_hooked_non_component.jsx @@ -1,3 +1,3 @@ -export const Hoge = () => {} +export const Component = () => {} const func = () => {} export default memo(func) diff --git a/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_hooked_non_component.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_hooked_non_component.jsx.snap index 59d3465bf240..ddb09a7d8da9 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_hooked_non_component.jsx.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_hooked_non_component.jsx.snap @@ -1,10 +1,11 @@ --- source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 expression: invalid_hooked_non_component.jsx --- # Input ```jsx -export const Hoge = () => {} +export const Component = () => {} const func = () => {} export default memo(func) @@ -16,7 +17,7 @@ invalid_hooked_non_component.jsx:3:16 lint/nursery/useComponentExportOnlyModules ! Exporting a non-component with components is not allowed. - 1 │ export const Hoge = () => {} + 1 │ export const Component = () => {} 2 │ const func = () => {} > 3 │ export default memo(func) │ ^^^^^^^^^^ diff --git a/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_sub_hooked_default_component_with_non_component.jsx b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_sub_hooked_default_component_with_non_component.jsx new file mode 100644 index 000000000000..29a211b2be81 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_sub_hooked_default_component_with_non_component.jsx @@ -0,0 +1,2 @@ +export const func = () => {}; +export default React.memo(() => <>); diff --git a/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_sub_hooked_default_component_with_non_component.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_sub_hooked_default_component_with_non_component.jsx.snap new file mode 100644 index 000000000000..c20e4feefeea --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/invalid_sub_hooked_default_component_with_non_component.jsx.snap @@ -0,0 +1,31 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: invalid_sub_hooked_default_component_with_non_component.jsx +--- +# Input +```jsx +export const func = () => {}; +export default React.memo(() => <>); + +``` + +# Diagnostics +``` +invalid_sub_hooked_default_component_with_non_component.jsx:1:14 lint/nursery/useComponentExportOnlyModules ━━━━━━━━━━ + + ! Exporting a non-component with components is not allowed. + + > 1 │ export const func = () => {}; + │ ^^^^ + 2 │ export default React.memo(() => <>); + 3 │ + + i Fast Refresh only works when a file only exports components. + + i Consider separating non-component exports into a new file. + + i If it is a component, it may not be following the variable naming conventions. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/valid_hooked_component.jsx b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/valid_default_hooked_component.jsx similarity index 63% rename from crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/valid_hooked_component.jsx rename to crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/valid_default_hooked_component.jsx index 00b4630aa137..c19d0fcae519 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/valid_hooked_component.jsx +++ b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/valid_default_hooked_component.jsx @@ -1,5 +1,5 @@ import { memo } from 'react'; -const Component = () => <> +export const Component = () => <> export default memo(Component); \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/valid_hooked_component.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/valid_default_hooked_component.jsx.snap similarity index 58% rename from crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/valid_hooked_component.jsx.snap rename to crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/valid_default_hooked_component.jsx.snap index da962bc64a34..15add6e624dc 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/valid_hooked_component.jsx.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/valid_default_hooked_component.jsx.snap @@ -1,13 +1,13 @@ --- source: crates/biome_js_analyze/tests/spec_tests.rs -assertion_line: 84 -expression: valid_hooked_component.jsx +assertion_line: 86 +expression: valid_default_hooked_component.jsx --- # Input ```jsx import { memo } from 'react'; -const Component = () => <> +export const Component = () => <> export default memo(Component); ``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/valid_non_components_only.jsx b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/valid_non_components_only.jsx index cb084ab73fb8..66796dadfd28 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/valid_non_components_only.jsx +++ b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/valid_non_components_only.jsx @@ -1,4 +1,8 @@ export const sampleConst = 100 -export function hoge () { +export function foo () { return 100 } + +export const nonComponent = foo(sampleConst) + +export default Foo.bar(()=>{}) diff --git a/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/valid_non_components_only.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/valid_non_components_only.jsx.snap index 4f10745d032f..bc10c90cbcdf 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/valid_non_components_only.jsx.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/useComponentExportOnlyModules/valid_non_components_only.jsx.snap @@ -1,13 +1,17 @@ --- source: crates/biome_js_analyze/tests/spec_tests.rs -assertion_line: 84 +assertion_line: 86 expression: valid_non_components_only.jsx --- # Input ```jsx export const sampleConst = 100 -export function hoge () { +export function foo () { return 100 } +export const nonComponent = foo(sampleConst) + +export default Foo.bar(()=>{}) + ```