-
-
Notifications
You must be signed in to change notification settings - Fork 501
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: eslint-plugin-next/no-duplicate-head
- Loading branch information
1 parent
27b2c21
commit 189d551
Showing
5 changed files
with
296 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
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
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
206 changes: 206 additions & 0 deletions
206
crates/oxc_linter/src/rules/nextjs/no_duplicate_head.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,206 @@ | ||
use oxc_ast::{ | ||
ast::{Expression, ImportDeclarationSpecifier::ImportDefaultSpecifier}, | ||
AstKind, | ||
}; | ||
use oxc_diagnostics::{ | ||
miette::{self, Diagnostic}, | ||
thiserror::{self, Error}, | ||
}; | ||
use oxc_macros::declare_oxc_lint; | ||
use oxc_span::{GetSpan, Span}; | ||
|
||
use crate::{context::LintContext, rule::Rule, AstNode}; | ||
|
||
#[derive(Debug, Error, Diagnostic)] | ||
#[error("eslint-plugin-next(no-duplicate-head):")] | ||
#[diagnostic(severity(warning), help("Do not include multiple instances of `<Head/>`. See: https://nextjs.org/docs/messages/no-duplicate-head"))] | ||
struct NoDuplicateHeadDiagnostic(#[label] pub Span); | ||
|
||
#[derive(Debug, Default, Clone)] | ||
pub struct NoDuplicateHead; | ||
|
||
declare_oxc_lint!( | ||
/// ### What it does | ||
/// | ||
/// | ||
/// ### Why is this bad? | ||
/// | ||
/// | ||
/// ### Example | ||
/// ```javascript | ||
/// ``` | ||
NoDuplicateHead, | ||
correctness | ||
); | ||
|
||
impl Rule for NoDuplicateHead { | ||
fn run_once(&self, ctx: &LintContext) { | ||
let mut document_import_name = None; | ||
let nodes = ctx.semantic().nodes(); | ||
for node in nodes.iter() { | ||
match node.kind() { | ||
oxc_ast::AstKind::ImportDeclaration(decl) => { | ||
if decl.source.value == "next/document" { | ||
let document_import = decl.specifiers.as_ref().and_then(|specifiers| { | ||
specifiers.iter().find(|spec| matches!(spec, ImportDefaultSpecifier(_))) | ||
}); | ||
if let Some(ImportDefaultSpecifier(document_improt_specifier)) = | ||
document_import | ||
{ | ||
document_import_name = | ||
Some(document_improt_specifier.local.name.clone()); | ||
} | ||
} | ||
} | ||
oxc_ast::AstKind::ReturnStatement(stmt) => { | ||
let document_class_id = nodes.ancestors(node.id()).find(|node_id| match nodes | ||
.get_node(*node_id) | ||
.kind() | ||
{ | ||
AstKind::Class(class) | ||
if class | ||
.super_class | ||
.as_ref() | ||
.and_then(|sc| sc.as_identifier()) | ||
.map(|id| &id.name) | ||
== document_import_name.as_ref() => | ||
{ | ||
true | ||
} | ||
_ => false, | ||
}); | ||
let Some(document_class) = document_class_id.map(|id| nodes.get_node(id)) | ||
else { | ||
continue; | ||
}; | ||
|
||
let Some(Expression::JSXElement(ref element)) = | ||
stmt.argument.as_ref().map(|arg| arg.get_inner_expression()) | ||
else { | ||
continue; | ||
}; | ||
let head_components = element | ||
.children | ||
.iter() | ||
.filter(|child| { | ||
child | ||
.as_element() | ||
.and_then(|e| e.opening_element.name.as_identifier()) | ||
.map(|id| id.name == "Head") | ||
.unwrap_or_default() | ||
}) | ||
.collect::<Vec<_>>(); | ||
dbg!(&head_components); | ||
if head_components.len() > 1 { | ||
for component in head_components { | ||
ctx.diagnostic(NoDuplicateHeadDiagnostic(component.span())); | ||
} | ||
} | ||
} | ||
_ => continue, | ||
} | ||
} | ||
} | ||
} | ||
|
||
#[test] | ||
fn test() { | ||
use crate::tester::Tester; | ||
|
||
let pass = vec![ | ||
r#"import Document, { Html, Head, Main, NextScript } from 'next/document' | ||
class MyDocument extends Document { | ||
static async getInitialProps(ctx) { | ||
//... | ||
} | ||
render() { | ||
return ( | ||
<Html> | ||
<Head/> | ||
</Html> | ||
) | ||
} | ||
} | ||
export default MyDocument | ||
"#, | ||
r#"import Document, { Html, Head, Main, NextScript } from 'next/document' | ||
class MyDocument extends Document { | ||
render() { | ||
return ( | ||
<Html> | ||
<Head> | ||
<meta charSet="utf-8" /> | ||
<link | ||
href="https://fonts.googleapis.com/css2?family=Sarabun:ital,wght@0,400;0,700;1,400;1,700&display=swap" | ||
rel="stylesheet" | ||
/> | ||
</Head> | ||
</Html> | ||
) | ||
} | ||
} | ||
export default MyDocument | ||
"#, | ||
]; | ||
|
||
let fail = vec![ | ||
r#" | ||
import Document, { Html, Main, NextScript } from 'next/document' | ||
import Head from 'next/head' | ||
class MyDocument extends Document { | ||
render() { | ||
return ( | ||
<Html> | ||
<Head /> | ||
<Head /> | ||
<Head /> | ||
</Html> | ||
) | ||
} | ||
} | ||
export default MyDocument | ||
"#, | ||
r#" | ||
import Document, { Html, Main, NextScript } from 'next/document' | ||
import Head from 'next/head' | ||
class MyDocument extends Document { | ||
render() { | ||
return ( | ||
<Html> | ||
<Head> | ||
<meta charSet='utf-8' /> | ||
<link | ||
href='https://fonts.googleapis.com/css2?family=Sarabun:ital,wght@0,400;0,700;1,400;1,700&display=swap' | ||
rel='stylesheet' | ||
/> | ||
</Head> | ||
<body> | ||
<Main /> | ||
<NextScript /> | ||
</body> | ||
<Head> | ||
<script | ||
dangerouslySetInnerHTML={{ | ||
__html: '', | ||
}} | ||
/> | ||
</Head> | ||
</Html> | ||
) | ||
} | ||
} | ||
export default MyDocument | ||
"#, | ||
]; | ||
|
||
Tester::new(NoDuplicateHead::NAME, pass, fail).test_and_snapshot(); | ||
} |
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,60 @@ | ||
--- | ||
source: crates/oxc_linter/src/tester.rs | ||
expression: no_duplicate_head | ||
--- | ||
|
||
⚠ eslint-plugin-next(no-duplicate-head): | ||
╭─[no_duplicate_head.tsx:9:27] | ||
8 │ <Html> | ||
9 │ <Head /> | ||
· ──────── | ||
10 │ <Head /> | ||
╰──── | ||
help: Do not include multiple instances of `<Head/>`. See: https://nextjs.org/docs/messages/no-duplicate-head | ||
|
||
⚠ eslint-plugin-next(no-duplicate-head): | ||
╭─[no_duplicate_head.tsx:10:27] | ||
9 │ <Head /> | ||
10 │ <Head /> | ||
· ──────── | ||
11 │ <Head /> | ||
╰──── | ||
help: Do not include multiple instances of `<Head/>`. See: https://nextjs.org/docs/messages/no-duplicate-head | ||
|
||
⚠ eslint-plugin-next(no-duplicate-head): | ||
╭─[no_duplicate_head.tsx:11:27] | ||
10 │ <Head /> | ||
11 │ <Head /> | ||
· ──────── | ||
12 │ </Html> | ||
╰──── | ||
help: Do not include multiple instances of `<Head/>`. See: https://nextjs.org/docs/messages/no-duplicate-head | ||
|
||
⚠ eslint-plugin-next(no-duplicate-head): | ||
╭─[no_duplicate_head.tsx:9:27] | ||
8 │ <Html> | ||
9 │ ╭─▶ <Head> | ||
10 │ │ <meta charSet='utf-8' /> | ||
11 │ │ <link | ||
12 │ │ href='https://fonts.googleapis.com/css2?family=Sarabun:ital,wght@0,400;0,700;1,400;1,700&display=swap' | ||
13 │ │ rel='stylesheet' | ||
14 │ │ /> | ||
15 │ ╰─▶ </Head> | ||
16 │ <body> | ||
╰──── | ||
help: Do not include multiple instances of `<Head/>`. See: https://nextjs.org/docs/messages/no-duplicate-head | ||
|
||
⚠ eslint-plugin-next(no-duplicate-head): | ||
╭─[no_duplicate_head.tsx:20:27] | ||
19 │ </body> | ||
20 │ ╭─▶ <Head> | ||
21 │ │ <script | ||
22 │ │ dangerouslySetInnerHTML={{ | ||
23 │ │ __html: '', | ||
24 │ │ }} | ||
25 │ │ /> | ||
26 │ ╰─▶ </Head> | ||
27 │ </Html> | ||
╰──── | ||
help: Do not include multiple instances of `<Head/>`. See: https://nextjs.org/docs/messages/no-duplicate-head | ||
|