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 ``" + )); + } +} + +#[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 ( + + + + + + +
+ + + +