-
-
Notifications
You must be signed in to change notification settings - Fork 500
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(linter) eslint-plugin-next no-async-client-component (#1936)
- Loading branch information
Showing
3 changed files
with
265 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
213 changes: 213 additions & 0 deletions
213
crates/oxc_linter/src/rules/nextjs/no_async_client_component.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
use oxc_ast::{ | ||
ast::{ | ||
BindingPatternKind, ExportDefaultDeclarationKind, Expression, ModuleDeclaration, Statement, | ||
}, | ||
AstKind, | ||
}; | ||
use oxc_diagnostics::{ | ||
miette::{self, Diagnostic}, | ||
thiserror::Error, | ||
}; | ||
use oxc_macros::declare_oxc_lint; | ||
use oxc_span::Span; | ||
|
||
use crate::{ast_util::get_declaration_of_variable, context::LintContext, rule::Rule}; | ||
|
||
#[derive(Debug, Error, Diagnostic)] | ||
#[error("eslint-plugin-next(no-async-client-component): Prevent client components from being async functions.")] | ||
#[diagnostic( | ||
severity(warning), | ||
help("See: https://nextjs.org/docs/messages/no-async-client-component") | ||
)] | ||
struct NoAsyncClientComponentDiagnostic(#[label] pub Span); | ||
|
||
#[derive(Debug, Default, Clone)] | ||
pub struct NoAsyncClientComponent; | ||
|
||
declare_oxc_lint!( | ||
/// ### What it does | ||
/// | ||
/// | ||
/// ### Why is this bad? | ||
/// | ||
/// | ||
/// ### Example | ||
/// ```javascript | ||
/// ``` | ||
NoAsyncClientComponent, | ||
correctness | ||
); | ||
|
||
impl Rule for NoAsyncClientComponent { | ||
fn run_once(&self, ctx: &LintContext) { | ||
let Some(root) = ctx.nodes().iter().next() else { return }; | ||
let AstKind::Program(program) = root.kind() else { return }; | ||
|
||
if program | ||
.directives | ||
.iter() | ||
.any(|directive| directive.expression.value.as_str() == "use client") | ||
{ | ||
for node in &program.body { | ||
let Statement::ModuleDeclaration(mod_decl) = &node else { | ||
continue; | ||
}; | ||
let ModuleDeclaration::ExportDefaultDeclaration(export_default_decl) = &**mod_decl | ||
else { | ||
continue; | ||
}; | ||
|
||
// export default async function MyComponent() {...} | ||
if let ExportDefaultDeclarationKind::FunctionDeclaration(func_decl) = | ||
&export_default_decl.declaration | ||
{ | ||
if func_decl.r#async | ||
&& func_decl | ||
.id | ||
.as_ref() | ||
.is_some_and(|v| v.name.chars().next().unwrap().is_uppercase()) | ||
{ | ||
ctx.diagnostic(NoAsyncClientComponentDiagnostic( | ||
func_decl.id.as_ref().unwrap().span, | ||
)); | ||
} | ||
continue; | ||
} | ||
|
||
// async function MyComponent() {...}; export default MyComponent; | ||
if let ExportDefaultDeclarationKind::Expression(Expression::Identifier( | ||
export_default_id, | ||
)) = &export_default_decl.declaration | ||
{ | ||
let Some(decl) = get_declaration_of_variable(export_default_id, ctx) else { | ||
continue; | ||
}; | ||
|
||
if let AstKind::Function(func) = decl.kind() { | ||
if func.r#async | ||
&& func | ||
.id | ||
.as_ref() | ||
// `func.id.name` MUST be > 0 chars | ||
.is_some_and(|v| v.name.chars().next().unwrap().is_uppercase()) | ||
{ | ||
ctx.diagnostic(NoAsyncClientComponentDiagnostic( | ||
func.id.as_ref().unwrap().span, | ||
)); | ||
} | ||
} | ||
|
||
if let AstKind::VariableDeclarator(var_declarator) = decl.kind() { | ||
if let BindingPatternKind::BindingIdentifier(binding_ident) = | ||
&var_declarator.id.kind | ||
{ | ||
// `binding_ident.name` MUST be > 0 chars | ||
if binding_ident.name.chars().next().unwrap().is_uppercase() { | ||
if let Some(Expression::ArrowExpression(arrow_expr)) = | ||
&var_declarator.init | ||
{ | ||
if arrow_expr.r#async { | ||
ctx.diagnostic(NoAsyncClientComponentDiagnostic( | ||
binding_ident.span, | ||
)); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
#[test] | ||
fn test() { | ||
use crate::tester::Tester; | ||
|
||
let pass = vec![ | ||
r" | ||
export default async function MyComponent() { | ||
return <></> | ||
} | ||
", | ||
r#" | ||
"use client" | ||
export default async function myFunction() { | ||
return '' | ||
} | ||
"#, | ||
r" | ||
async function MyComponent() { | ||
return <></> | ||
} | ||
export default MyComponent | ||
", | ||
r#" | ||
"use client" | ||
async function myFunction() { | ||
return '' | ||
} | ||
export default myFunction | ||
"#, | ||
r#" | ||
"use client" | ||
const myFunction = () => { | ||
return '' | ||
} | ||
export default myFunction | ||
"#, | ||
]; | ||
|
||
let fail = vec![ | ||
r#" | ||
"use client" | ||
export default async function MyComponent() { | ||
return <></> | ||
} | ||
"#, | ||
r#" | ||
"use client" | ||
export default async function MyFunction() { | ||
return '' | ||
} | ||
"#, | ||
r#" | ||
"use client" | ||
async function MyComponent() { | ||
return <></> | ||
} | ||
export default MyComponent | ||
"#, | ||
r#" | ||
"use client" | ||
async function MyFunction() { | ||
return '' | ||
} | ||
export default MyFunction | ||
"#, | ||
r#" | ||
"use client" | ||
const MyFunction = async () => { | ||
return '123' | ||
} | ||
export default MyFunction | ||
"#, | ||
]; | ||
|
||
Tester::new_without_config(NoAsyncClientComponent::NAME, pass, fail).test_and_snapshot(); | ||
} |
50 changes: 50 additions & 0 deletions
50
crates/oxc_linter/src/snapshots/no_async_client_component.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
--- | ||
source: crates/oxc_linter/src/tester.rs | ||
expression: no_async_client_component | ||
--- | ||
⚠ eslint-plugin-next(no-async-client-component): Prevent client components from being async functions. | ||
╭─[no_async_client_component.tsx:3:1] | ||
3 │ | ||
4 │ export default async function MyComponent() { | ||
· ─────────── | ||
5 │ return <></> | ||
╰──── | ||
help: See: https://nextjs.org/docs/messages/no-async-client-component | ||
|
||
⚠ eslint-plugin-next(no-async-client-component): Prevent client components from being async functions. | ||
╭─[no_async_client_component.tsx:3:1] | ||
3 │ | ||
4 │ export default async function MyFunction() { | ||
· ────────── | ||
5 │ return '' | ||
╰──── | ||
help: See: https://nextjs.org/docs/messages/no-async-client-component | ||
|
||
⚠ eslint-plugin-next(no-async-client-component): Prevent client components from being async functions. | ||
╭─[no_async_client_component.tsx:3:1] | ||
3 │ | ||
4 │ async function MyComponent() { | ||
· ─────────── | ||
5 │ return <></> | ||
╰──── | ||
help: See: https://nextjs.org/docs/messages/no-async-client-component | ||
|
||
⚠ eslint-plugin-next(no-async-client-component): Prevent client components from being async functions. | ||
╭─[no_async_client_component.tsx:3:1] | ||
3 │ | ||
4 │ async function MyFunction() { | ||
· ────────── | ||
5 │ return '' | ||
╰──── | ||
help: See: https://nextjs.org/docs/messages/no-async-client-component | ||
|
||
⚠ eslint-plugin-next(no-async-client-component): Prevent client components from being async functions. | ||
╭─[no_async_client_component.tsx:3:1] | ||
3 │ | ||
4 │ const MyFunction = async () => { | ||
· ────────── | ||
5 │ return '123' | ||
╰──── | ||
help: See: https://nextjs.org/docs/messages/no-async-client-component | ||
|
||
|