diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 97138c553..a40d45b9e 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -276,6 +276,7 @@ mod nextjs { pub mod no_assign_module_variable; pub mod no_async_client_component; pub mod no_css_tags; + pub mod no_img_element; } oxc_macros::declare_all_lint_rules! { @@ -521,4 +522,5 @@ oxc_macros::declare_all_lint_rules! { nextjs::no_assign_module_variable, nextjs::no_async_client_component, nextjs::no_css_tags, + nextjs::no_img_element, } diff --git a/crates/oxc_linter/src/rules/nextjs/no_img_element.rs b/crates/oxc_linter/src/rules/nextjs/no_img_element.rs new file mode 100644 index 000000000..934224773 --- /dev/null +++ b/crates/oxc_linter/src/rules/nextjs/no_img_element.rs @@ -0,0 +1,147 @@ +use oxc_ast::{ast::JSXElementName, AstKind}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint-plugin-next(no-img-element): Prevent usage of `` element due to slower LCP and higher bandwidth.")] +#[diagnostic(severity(warning), help("See https://nextjs.org/docs/messages/no-img-element"))] +struct NoImgElementDiagnostic(#[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct NoImgElement; + +declare_oxc_lint!( + /// ### What it does + /// + /// + /// ### Why is this bad? + /// + /// + /// ### Example + /// ```javascript + /// ``` + NoImgElement, + correctness +); + +impl Rule for NoImgElement { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::JSXOpeningElement(jsx_opening_element) = node.kind() else { return }; + + let JSXElementName::Identifier(jsx_opening_element_name) = &jsx_opening_element.name else { + return; + }; + + if jsx_opening_element_name.name.as_str() != "img" { + return; + } + + let Some(parent) = ctx.nodes().parent_node(node.id()) else { return }; + let Some(parent) = ctx.nodes().parent_node(parent.id()) else { return }; + + if let AstKind::JSXElement(maybe_picture_jsx_elem) = parent.kind() { + if let JSXElementName::Identifier(jsx_opening_element_name) = + &maybe_picture_jsx_elem.opening_element.name + { + if jsx_opening_element_name.name.as_str() == "picture" { + return; + } + } + } + + ctx.diagnostic(NoImgElementDiagnostic(jsx_opening_element_name.span)); + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + r#"import { Image } from 'next/image'; + + export class MyComponent { + render() { + return ( +
+ Test picture +
+ ); + } + }"#, + r#"export class MyComponent { + render() { + return ( + + Test picture + + ); + } + }"#, + r#"export class MyComponent { + render() { + return ( +
+ + + Test picture + +
+ ); + } + }"#, + ]; + + let fail = vec![ + r#" + export class MyComponent { + render() { + return ( +
+ Test picture +
+ ); + } + }"#, + r#" + export class MyComponent { + render() { + return ( + Test picture + ); + } + }"#, + ]; + + Tester::new_without_config(NoImgElement::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_img_element.snap b/crates/oxc_linter/src/snapshots/no_img_element.snap new file mode 100644 index 000000000..d6c9d3d12 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_img_element.snap @@ -0,0 +1,23 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: no_img_element +--- + ⚠ eslint-plugin-next(no-img-element): Prevent usage of `` element due to slower LCP and higher bandwidth. + ╭─[no_img_element.tsx:5:1] + 5 │
+ 6 │ ` element due to slower LCP and higher bandwidth. + ╭─[no_img_element.tsx:4:1] + 4 │ return ( + 5 │