feat(linter): eslint-plugin-next/no-duplicate-head (#3174)

closes #2438
This commit is contained in:
Boshen 2024-05-06 19:43:38 +08:00 committed by GitHub
parent d91b688f3a
commit cb2e651eea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 218 additions and 0 deletions

View file

@ -350,6 +350,7 @@ mod nextjs {
pub mod no_before_interactive_script_outside_document;
pub mod no_css_tags;
pub mod no_document_import_in_page;
pub mod no_duplicate_head;
pub mod no_head_element;
pub mod no_head_import_in_document;
pub mod no_img_element;
@ -693,6 +694,7 @@ oxc_macros::declare_all_lint_rules! {
nextjs::no_css_tags,
nextjs::no_head_element,
nextjs::no_head_import_in_document,
nextjs::no_duplicate_head,
nextjs::no_img_element,
nextjs::no_script_component_in_head,
nextjs::no_sync_scripts,

View file

@ -0,0 +1,185 @@
use oxc_ast::AstKind;
use oxc_diagnostics::miette::{miette, LabeledSpan, Severity};
use oxc_macros::declare_oxc_lint;
use oxc_semantic::Reference;
use crate::{context::LintContext, rule::Rule};
#[derive(Debug, Default, Clone)]
pub struct NoDuplicateHead;
declare_oxc_lint!(
/// ### What it does
/// Prevent duplicate usage of <Head> in pages/_document.js.
///
/// ### Why is this bad?
/// This can cause unexpected behavior in your application.
///
/// ### Example
/// ```javascript
/// import Document, { Html, Head, Main, NextScript } from 'next/document'
/// class MyDocument extends Document {
/// static async getInitialProps(ctx) {
/// }
/// render() {
/// return (
/// <Html>
/// <Head />
/// <body>
/// <Main />
/// <NextScript />
/// </body>
/// </Html>
/// )
/// }
///}
///export default MyDocument
/// ```
NoDuplicateHead,
correctness
);
impl Rule for NoDuplicateHead {
fn run_on_symbol(&self, symbol_id: oxc_semantic::SymbolId, ctx: &LintContext<'_>) {
let symbols = ctx.symbols();
let name = symbols.get_name(symbol_id);
if name != "Head" {
return;
}
let flag = symbols.get_flag(symbol_id);
if !flag.is_import_binding() {
return;
}
let scope_id = symbols.get_scope_id(symbol_id);
if scope_id != ctx.scopes().root_scope_id() {
return;
}
let nodes = ctx.nodes();
let labels = symbols
.get_resolved_references(symbol_id)
.filter(|r| r.is_read())
.filter(|r| {
let kind = nodes.ancestors(r.node_id()).nth(2).map(|node_id| nodes.kind(node_id));
matches!(kind, Some(AstKind::JSXOpeningElement(_)))
})
.map(Reference::span)
.map(LabeledSpan::underline)
.collect::<Vec<_>>();
if labels.len() <= 1 {
return;
}
ctx.diagnostic(miette!(
severity = Severity::Warning,
labels = labels,
help = "Only use a single `<Head />` component in your custom document in `pages/_document.js`. See: https://nextjs.org/docs/messages/no-duplicate-head",
"eslint-plugin-next(no-duplicate-head): Do not include multiple instances of `<Head/>`"
));
}
}
#[test]
fn test() {
use crate::tester::Tester;
let pass = vec![
"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![
"
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();
}

View file

@ -0,0 +1,31 @@
---
source: crates/oxc_linter/src/tester.rs
expression: no_duplicate_head
---
⚠ eslint-plugin-next(no-duplicate-head): Do not include multiple instances of `<Head/>`
╭─[no_duplicate_head.tsx:9:19]
8 │ <Html>
9 │ <Head />
· ────
10 │ <Head />
· ────
11 │ <Head />
· ────
12 │ </Html>
╰────
help: Only use a single `<Head />` component in your custom document in `pages/_document.js`. See: https://nextjs.org/docs/messages/no-duplicate-head
⚠ eslint-plugin-next(no-duplicate-head): Do not include multiple instances of `<Head/>`
╭─[no_duplicate_head.tsx:9:19]
8 │ <Html>
9 │ <Head>
· ────
10 │ <meta charSet="utf-8" />
╰────
╭─[no_duplicate_head.tsx:20:19]
19 │ </body>
20 │ <Head>
· ────
21 │ <script
╰────
help: Only use a single `<Head />` component in your custom document in `pages/_document.js`. See: https://nextjs.org/docs/messages/no-duplicate-head