From a3754e20ca4743631274dc234380e1de3195e198 Mon Sep 17 00:00:00 2001 From: Cameron Date: Tue, 9 Jan 2024 03:16:42 +0000 Subject: [PATCH] feat(linter) eslint-plugin-next no-title-in-document-head (#1952) --- crates/oxc_linter/src/rules.rs | 2 + .../rules/nextjs/no_title_in_document_head.rs | 146 ++++++++++++++++++ .../snapshots/no_title_in_document_head.snap | 14 ++ 3 files changed, 162 insertions(+) create mode 100644 crates/oxc_linter/src/rules/nextjs/no_title_in_document_head.rs create mode 100644 crates/oxc_linter/src/snapshots/no_title_in_document_head.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index a40d45b9e..2512fa553 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -277,6 +277,7 @@ mod nextjs { pub mod no_async_client_component; pub mod no_css_tags; pub mod no_img_element; + pub mod no_title_in_document_head; } oxc_macros::declare_all_lint_rules! { @@ -523,4 +524,5 @@ oxc_macros::declare_all_lint_rules! { nextjs::no_async_client_component, nextjs::no_css_tags, nextjs::no_img_element, + nextjs::no_title_in_document_head, } diff --git a/crates/oxc_linter/src/rules/nextjs/no_title_in_document_head.rs b/crates/oxc_linter/src/rules/nextjs/no_title_in_document_head.rs new file mode 100644 index 000000000..e95edc8aa --- /dev/null +++ b/crates/oxc_linter/src/rules/nextjs/no_title_in_document_head.rs @@ -0,0 +1,146 @@ +use oxc_ast::{ + ast::{ImportDeclarationSpecifier, JSXChild, JSXElementName, ModuleDeclaration}, + AstKind, +}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::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-title-in-document-head): Prevent usage of `` with `Head` component from `next/document`.")] +#[diagnostic( + severity(warning), + help("See https://nextjs.org/docs/messages/no-title-in-document-head") +)] +struct NoTitleInDocumentHeadDiagnostic(#[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct NoTitleInDocumentHead; + +declare_oxc_lint!( + /// ### What it does + /// + /// + /// ### Why is this bad? + /// + /// + /// ### Example + /// ```javascript + /// ``` + NoTitleInDocumentHead, + correctness +); + +impl Rule for NoTitleInDocumentHead { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::ModuleDeclaration(ModuleDeclaration::ImportDeclaration(import_decl)) = + node.kind() + else { + return; + }; + + if import_decl.source.value.as_str() != "next/document" { + return; + } + + let Some(import_specifiers) = &import_decl.specifiers else { return }; + + let Some(default_import) = import_specifiers.iter().find_map(|import_specifier| { + let ImportDeclarationSpecifier::ImportSpecifier(import_default_specifier) = + import_specifier + else { + return None; + }; + + Some(import_default_specifier) + }) else { + return; + }; + + for reference in + ctx.semantic().symbol_references(default_import.local.symbol_id.get().unwrap()) + { + let node = ctx.nodes().get_node(reference.node_id()); + + let AstKind::JSXElementName(_) = node.kind() else { continue }; + let parent_node = ctx.nodes().parent_node(node.id()).unwrap(); + let AstKind::JSXOpeningElement(jsx_opening_element) = parent_node.kind() else { + continue; + }; + let Some(AstKind::JSXElement(jsx_element)) = ctx.nodes().parent_kind(parent_node.id()) + else { + continue; + }; + + for child in &jsx_element.children { + if let JSXChild::Element(child_element) = child { + if let JSXElementName::Identifier(child_element_name) = + &child_element.opening_element.name + { + if child_element_name.name.as_str() == "title" { + ctx.diagnostic(NoTitleInDocumentHeadDiagnostic( + jsx_opening_element.name.span(), + )); + } + } + } + } + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + r#"import Head from "next/head"; + + class Test { + render() { + return ( + <Head> + <title>My page title + + ); + } + }"#, + r#"import Document, { Html, Head } from "next/document"; + + class MyDocument extends Document { + render() { + return ( + + + + + ); + } + } + + export default MyDocument; + "#, + ]; + + let fail = vec![ + r#" + import { Head } from "next/document"; + + class Test { + render() { + return ( + + My page title + + ); + } + }"#, + ]; + + Tester::new_without_config(NoTitleInDocumentHead::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_title_in_document_head.snap b/crates/oxc_linter/src/snapshots/no_title_in_document_head.snap new file mode 100644 index 000000000..a044a26ac --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_title_in_document_head.snap @@ -0,0 +1,14 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: no_title_in_document_head +--- + ⚠ eslint-plugin-next(no-title-in-document-head): Prevent usage of `` with `Head` component from `next/document`. + ╭─[no_title_in_document_head.tsx:6:1] + 6 │ return ( + 7 │ <Head> + · ──── + 8 │ <title>My page title + ╰──── + help: See https://nextjs.org/docs/messages/no-title-in-document-head + +