Skip to content

Commit

Permalink
feat(linter): eslint-plugin-next/no-duplicate-head (#3174)
Browse files Browse the repository at this point in the history
closes #2438
  • Loading branch information
Boshen authored May 6, 2024
1 parent d91b688 commit cb2e651
Show file tree
Hide file tree
Showing 3 changed files with 218 additions and 0 deletions.
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
185 changes: 185 additions & 0 deletions crates/oxc_linter/src/rules/nextjs/no_duplicate_head.rs
Original file line number Diff line number Diff line change
@@ -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();
}
31 changes: 31 additions & 0 deletions crates/oxc_linter/src/snapshots/no_duplicate_head.snap
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit cb2e651

Please sign in to comment.