-
-
Notifications
You must be signed in to change notification settings - Fork 442
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(lint): add rule useComponentExportOnlyModules
#3576
Conversation
CodSpeed Performance ReportMerging #3576 will degrade performances by 7.96%Comparing Summary
Benchmarks breakdown
|
crates/biome_js_analyze/src/lint/nursery/use_components_only_module.rs
Outdated
Show resolved
Hide resolved
/// | ||
/// This is necessary to enable the Fast Refresh feature, which improves development efficiency. | ||
/// The determination of whether something is a component depends on naming conventions. | ||
/// Please write components in PascalCase and regular functions in camelCase. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like useNamingConvention, we can emphasize PascalCase
and camelCase
and I believe that this is valuable for the user.
/// | ||
/// ### allowExportNames | ||
/// | ||
/// If you use a framework that handles HMR of some specific exports, you can use this option to avoid warning for them. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add some information for HMR
like below.
crates/biome_js_analyze/src/lint/nursery/use_components_only_module.rs
Outdated
Show resolved
Hide resolved
} | ||
} | ||
|
||
fn is_default<T: Default + Eq>(value: &T) -> bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you can move the is_default function under the UseComponentsOnlyModuleOptions class to make it clear that this is for the field attribute, IMHO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand that it should be that way. I wrote this based on biome_analyze/CONTRIBUTING.md and use_filenaming_convention.rs as references. Those might need to be revised as well. (For now, I deleted it since it doesn't seem like a crucial item.)
return exported_non_component_ids | ||
.iter() | ||
.filter_map(|id| { | ||
let range = id.identifier.clone().map(|x| x.range())?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's use as_ref to avoid redundant clone().
if !exported_component_ids.is_empty() {
return exported_non_component_ids
.iter()
.filter_map(|id| {
id.identifier.as_ref().map(|identifier| {
UseComponentsOnlyModuleState {
error: ErrorType::ExportedNonComponentWithComponent,
range: identifier.range(),
}
})
})
.collect();
}
if let Some(AnyJsExported::AnyJsExpression(AnyJsExpression::JsCallExpression(f))) = | ||
any_exported_item.exported.clone() | ||
{ | ||
if let Ok(AnyJsExpression::JsIdentifierExpression(funcname)) = f.callee() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is my preference, but I think naming it fn_name or function_name would be better maybe?
markup! { | ||
"Export Non-Component with components are not allowed." | ||
}, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the user's point of view, adding this consideration is more useful, I believe.
.note(markup! {
"Consider separating non-component exports into a new file." // Please update the description
}),
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was originally written in the note
section, but I addressed it by splitting the long sentence for clarity.👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left a preliminary review. Can I ask you to update the PR description and explain the business logic you implemented for the rule? Try to be as much as technical as you want. This will help us to review your code, and provide better suggestions to prevent performance regressions
} | ||
|
||
impl JsExport { | ||
/// Returns the pair of id and entity of the exported object |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When creating public APIs, we usually add doc tests.
biome/crates/biome_js_syntax/src/directive_ext.rs
Lines 6 to 22 in 4880f84
/// Get the inner text of a string not including the quotes | |
/// | |
/// ## Examples | |
/// | |
/// ``` | |
/// use biome_js_factory::syntax::{JsDirective, JsSyntaxKind::*}; | |
/// use biome_js_factory::JsSyntaxTreeBuilder; | |
/// use biome_rowan::AstNode; | |
/// let mut tree_builder = JsSyntaxTreeBuilder::new(); | |
/// tree_builder.start_node(JS_DIRECTIVE); | |
/// tree_builder.token(JS_STRING_LITERAL, "\"use strict\""); | |
/// tree_builder.finish_node(); | |
/// let node = tree_builder.finish(); | |
/// let js_directive = JsDirective::cast(node).unwrap(); | |
/// let text = js_directive.inner_string_text().unwrap(); | |
/// assert_eq!(text, "use strict") | |
/// ``` |
Also, think the doc of this function is incorrect. It doesn't return any pairs, it seems. ExportedItem
has three fields
pub struct ExportedItem { | ||
pub identifier: Option<AnyIdentifier>, | ||
pub exported: Option<AnyJsExported>, | ||
pub is_default: bool, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should document this type and its fields
/// - Static template literals: `foo` | ||
/// - Negative numeric literal: -1 | ||
/// - Parenthesized expression: (1) | ||
pub fn is_literal_expression(&self) -> bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add some examples
use serde::{Deserialize, Serialize}; | ||
|
||
declare_lint_rule! { | ||
/// React components should be separated into different modules. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first line of the docs is usually a small description of the rule. It's usually added in this table https://biomejs.dev/linter/rules/#accessibility
So the phrase could start with something like "Enforce React components ..." Or "Promotes the use of .."
declare_lint_rule! { | ||
/// React components should be separated into different modules. | ||
/// | ||
/// This is necessary to enable the Fast Refresh feature, which improves development efficiency. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should provide a link to Fash Refresh
/// | ||
pub UseComponentsOnlyModule { | ||
version: "1.8.0", | ||
name: "useComponentsOnlyModule", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name is weird, but I don't have better ideas 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about useModuleComponentExportsOnly
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is hard...how about useComponentExportOnlyModules
(This is just a suggestion).
pub struct UseComponentsOnlyModuleOptions { | ||
#[serde(default, skip_serializing_if = "is_default")] | ||
allow_constant_export: bool, | ||
#[serde(default, skip_serializing_if = "is_default")] | ||
allow_export_names: Vec<String>, | ||
#[serde(default, skip_serializing_if = "is_default")] | ||
check_js: bool, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should document these options. These options will be described inside our configuration schema, and users can read its docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a way of reducing the number of options? For example is checkJs
really useful? or even the two ther options?
let file_name = ctx.file_path().file_name().and_then(|x| x.to_str()); | ||
if let Some(file_name2) = file_name { | ||
if !ctx.options().check_js && !JSX_FILE_EXT.iter().any(|ext| file_name2.ends_with(ext)) | ||
{ | ||
return vec![]; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have an API that returns the file source, you can use that:
let file_source = ctx.source_type::<JsFileSource>();
JsFileSource
has methods to tell you which kind of file you're processing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Due to relying on PascalCase for component detection, unrelated .js files that export in PascalCase for some reason may cause malfunction. We implement this exception handling to reduce the impact, but is_jsx()
may return true for general JavaScript as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but is_jsx() may return true for general JavaScript as well.
At the moment, no. Biome doesn't allow JSX inside .js
files.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JS files within the test directory are tested as JSX even if they do not contain JSX syntax. Is this specific to the testing environment?
Lines 15 to 28 in 8343762
invalid_component_and_constant_js_with_option.js:1:14 lint/nursery/useComponentsOnlyModule ━━━━━━━━━━ | |
! Exporting a non-component with components is not allowed. | |
> 1 │ export const CONSTANT = 3 | |
│ ^^^^^^^^ | |
2 │ export const Foo = () => {} | |
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. |
(The above snapshot shows the expected behavior when checkJS
is set to true
and targets JavaScript. Normally, I do not want any errors to be generated.)
I do not want to generate errors like the ones mentioned above for .js
files. But ctx.source_type::<JsFileSource>().is_jsx()
returned true
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have to disregard my previous statement, I am sorry. It seems that our parser enables JSX parsing by default for .js
files :(
So yeah, you better keep this check for now. I believe we should break this behaviour in v2.0
// Function that returns a standard React component | ||
const REACT_HOOKS: [&str; 2] = ["memo", "forwardRef"]; | ||
|
||
fn is_exported_react_component(any_exported_item: &ExportedItem) -> bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since ExportedItem
is a public API, it makes more sense to expose a function called exports_binding()
(just a suggestion, if you have a better name, that's great), and pass the binding name.
crates/biome_js_analyze/src/lint/nursery/use_components_only_module.rs
Outdated
Show resolved
Hide resolved
…odule.rs Co-authored-by: Yusuke Abe <[email protected]>
…odule.rs Co-authored-by: Yusuke Abe <[email protected]>
…odule.rs Co-authored-by: Emanuele Stoppa <[email protected]>
…odule.rs Co-authored-by: Emanuele Stoppa <[email protected]>
…odule.rs Co-authored-by: Emanuele Stoppa <[email protected]>
…odule.rs Co-authored-by: Emanuele Stoppa <[email protected]>
…odule.rs Co-authored-by: Emanuele Stoppa <[email protected]>
@GunseiKPaseri |
/// | ||
pub UseComponentsOnlyModule { | ||
version: "1.8.0", | ||
name: "useComponentsOnlyModule", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is hard...how about useComponentExportOnlyModules
(This is just a suggestion).
crates/biome_js_analyze/src/lint/nursery/use_components_only_module.rs
Outdated
Show resolved
Hide resolved
…odule.rs Co-authored-by: Yusuke Abe <[email protected]>
useComponentsOnlyModule
useComponentExportOnlyModules
Took very long, didn't review. They can follow up if they want
Summary
Implement
useComponentsOnlyModule
( inspired eslint-plugin-react-refresh)Closes #3560
This rule applies only to
.jsx
files. (Since.js
files are also treated as.jsx
, it is necessary to check the file extension.)It scans the declarations in the file and enumerates the following:
The determination of whether something is a component or not is done simply by checking if the function or variable name is in PascalCase. (Exceptionally, cases where standard React functions like
memo
are used are also considered as components.)The enumeration of exports is achieved by searching for
JsExport
and using a newly implemented utility (get_exported_items
).This utility extracts the names of declarations and the expressions being exported within
JsExport
. The declaration names are used to determine whether they are components, and the expressions are used for exception handling specific to React.Based on the count of each of these, it outputs appropriate errors to ensure that the condition "components must always be exported, and only components should be exported" is met.
When attempting to export both components and non-components simultaneously, it instructs to move the export of non-components to a separate file.
If there are components that are not exported, it instructs to move the component to a new file (with a slightly different error message depending on whether there are other exported components).
No error is output in other cases.
Options have been implemented to ignore the export of constants (
allowConstantExport
), to treat specific non-PascalCase exports as components (allowExportNames
),and to enforce the check on..js
files as well (checkJS
)checkJS
has not been implemented.Test Plan