From cb2e651eeac56f30689588756aab52f6c12af335 Mon Sep 17 00:00:00 2001
From: Boshen
Date: Mon, 6 May 2024 19:43:38 +0800
Subject: [PATCH] feat(linter): eslint-plugin-next/no-duplicate-head (#3174)
closes #2438
---
crates/oxc_linter/src/rules.rs | 2 +
.../src/rules/nextjs/no_duplicate_head.rs | 185 ++++++++++++++++++
.../src/snapshots/no_duplicate_head.snap | 31 +++
3 files changed, 218 insertions(+)
create mode 100644 crates/oxc_linter/src/rules/nextjs/no_duplicate_head.rs
create mode 100644 crates/oxc_linter/src/snapshots/no_duplicate_head.snap
diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs
index fb5ab9e32a4fd..980303009e10b 100644
--- a/crates/oxc_linter/src/rules.rs
+++ b/crates/oxc_linter/src/rules.rs
@@ -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,
diff --git a/crates/oxc_linter/src/rules/nextjs/no_duplicate_head.rs b/crates/oxc_linter/src/rules/nextjs/no_duplicate_head.rs
new file mode 100644
index 0000000000000..b8224b874905a
--- /dev/null
+++ b/crates/oxc_linter/src/rules/nextjs/no_duplicate_head.rs
@@ -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 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 (
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// )
+ /// }
+ ///}
+ ///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::>();
+
+ if labels.len() <= 1 {
+ return;
+ }
+
+ ctx.diagnostic(miette!(
+ severity = Severity::Warning,
+ labels = labels,
+ help = "Only use a single ` ` 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 `
+
+
+
+
+
+
+
+ )
+ }
+ }
+
+ export default MyDocument
+ "#,
+ ];
+
+ Tester::new(NoDuplicateHead::NAME, pass, fail).test_and_snapshot();
+}
diff --git a/crates/oxc_linter/src/snapshots/no_duplicate_head.snap b/crates/oxc_linter/src/snapshots/no_duplicate_head.snap
new file mode 100644
index 0000000000000..c7f63ecdf0d43
--- /dev/null
+++ b/crates/oxc_linter/src/snapshots/no_duplicate_head.snap
@@ -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 ``"
+ ));
+ }
+}
+
+#[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 (
+
+
+
+ )
+ }
+ }
+
+ export default MyDocument
+ ",
+ r#"import Document, { Html, Head, Main, NextScript } from 'next/document'
+
+ class MyDocument extends Document {
+ render() {
+ return (
+
+
+
+
+
+
+ )
+ }
+ }
+
+ 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 (
+
+
+
+
+
+ )
+ }
+ }
+
+ export default MyDocument
+ ",
+ r#"
+ import Document, { Html, Main, NextScript } from 'next/document'
+ import Head from 'next/head'
+
+ class MyDocument extends Document {
+ render() {
+ return (
+
+
+
+
+
+
`
+ ╭─[no_duplicate_head.tsx:9:19]
+ 8 │
+ 9 │
+ · ────
+ 10 │
+ · ────
+ 11 │
+ · ────
+ 12 │
+ ╰────
+ help: Only use a single `
` 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 `
`
+ ╭─[no_duplicate_head.tsx:9:19]
+ 8 │
+ 9 │
+ · ────
+ 10 │
+ ╰────
+ ╭─[no_duplicate_head.tsx:20:19]
+ 19 │
+ 20 │
+ · ────
+ 21 │ ` component in your custom document in `pages/_document.js`. See: https://nextjs.org/docs/messages/no-duplicate-head