Skip to content

Commit

Permalink
feat(lint): add no-img-element from eslint-plugin-next
Browse files Browse the repository at this point in the history
  • Loading branch information
kaioduarte committed Oct 6, 2024
1 parent 35bb699 commit 4e10665
Show file tree
Hide file tree
Showing 12 changed files with 323 additions and 59 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

137 changes: 78 additions & 59 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ define_categories! {
"lint/nursery/noDynamicNamespaceImportAccess": "https://biomejs.dev/linter/rules/no-dynamic-namespace-import-access",
"lint/nursery/noEnum": "https://biomejs.dev/linter/rules/no-enum",
"lint/nursery/noExportedImports": "https://biomejs.dev/linter/rules/no-exported-imports",
"lint/nursery/noImgElement": "https://biomejs.dev/linter/rules/no-img-element",
"lint/nursery/noImportantInKeyframe": "https://biomejs.dev/linter/rules/no-important-in-keyframe",
"lint/nursery/noInvalidDirectionInLinearGradient": "https://biomejs.dev/linter/rules/no-invalid-direction-in-linear-gradient",
"lint/nursery/noInvalidGridAreas": "https://biomejs.dev/linter/rules/use-consistent-grid-areas",
Expand Down
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod no_dynamic_namespace_import_access;
pub mod no_enum;
pub mod no_exported_imports;
pub mod no_head_element;
pub mod no_img_element;
pub mod no_irregular_whitespace;
pub mod no_nested_ternary;
pub mod no_octal_escape;
Expand Down Expand Up @@ -41,6 +42,7 @@ declare_lint_group! {
self :: no_enum :: NoEnum ,
self :: no_exported_imports :: NoExportedImports ,
self :: no_head_element :: NoHeadElement ,
self :: no_img_element :: NoImgElement ,
self :: no_irregular_whitespace :: NoIrregularWhitespace ,
self :: no_nested_ternary :: NoNestedTernary ,
self :: no_octal_escape :: NoOctalEscape ,
Expand Down
137 changes: 137 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery/no_img_element.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use biome_analyze::RuleSourceKind;
use biome_analyze::{
context::RuleContext, declare_lint_rule, Ast, Rule, RuleDiagnostic, RuleSource,
};
use biome_console::markup;
use biome_js_syntax::{
JsSyntaxToken, JsxAttributeList, JsxChildList, JsxElement, JsxOpeningElement,
JsxSelfClosingElement,
};
use biome_rowan::{declare_node_union, AstNode, AstNodeList, TextRange};

declare_lint_rule! {
/// Prevent usage of `<img>` element in a Next.js project.
///
/// Using the `<img>` element can result in slower Largest Contentful Paint (LCP)
/// and higher bandwidth usage, as it lacks the optimizations provided by the `<Image />`
/// component from `next/image`. Next.js's `<Image />` automatically optimizes images
/// by serving responsive sizes and using modern formats, improving performance and reducing bandwidth.
///
/// ## Examples
///
/// ### Invalid
///
/// ```jsx,expect_diagnostic
/// <img alt="Foo" />
/// ```
///
/// ```jsx,expect_diagnostic
/// <div>
/// <img alt="Foo" />
/// </div>
/// ```
///
/// ### Valid
///
/// ```jsx
/// <img />
/// ```
///
/// ```jsx
/// <Image src="https://example.com/hero.jpg" />
/// ```
///
/// ```jsx
/// <picture>
/// <source srcSet="https://example.com/hero.avif" type="image/avif" />
/// <source srcSet="https://example.com/hero.webp" type="image/webp" />
/// <img src="https://example.com/hero.jpg" />
/// </picture>
/// ```
///
pub NoImgElement {
version: "next",
name: "noImgElement",
language: "jsx",
sources: &[RuleSource::EslintNext("no-img-element")],
source_kind: RuleSourceKind::SameLogic,
recommended: false,
}
}

declare_node_union! {
pub NoImgElementQuery = JsxOpeningElement | JsxSelfClosingElement
}

impl Rule for NoImgElement {
type Query = Ast<NoImgElementQuery>;
type State = TextRange;
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();
node.is_target_valid()?.then(|| node.range())
}

fn diagnostic(_: &RuleContext<Self>, range: &Self::State) -> Option<RuleDiagnostic> {
return Some(
RuleDiagnostic::new(
rule_category!(),
range,
markup! {
"Using "<Emphasis>"<img>"</Emphasis>" could result in slower LCP and higher bandwidth."
},
)
.note(markup! { "Consider using "<Emphasis>"<Image />"</Emphasis>" from "<Emphasis>"next/image"</Emphasis>" to automatically optimize images." })
.note(markup! { "This may incur additional usage or cost from your provider." })
);
}
}

impl NoImgElementQuery {
fn range(&self) -> TextRange {
match self {
NoImgElementQuery::JsxOpeningElement(jsx) => jsx.range(),
NoImgElementQuery::JsxSelfClosingElement(jsx) => jsx.range(),
}
}

fn name(&self) -> Option<JsSyntaxToken> {
match self {
NoImgElementQuery::JsxOpeningElement(jsx) => jsx.name().ok()?.name_value_token(),
NoImgElementQuery::JsxSelfClosingElement(jsx) => jsx.name().ok()?.name_value_token(),
}
}

fn attributes(&self) -> JsxAttributeList {
match self {
NoImgElementQuery::JsxOpeningElement(jsx) => jsx.attributes(),
NoImgElementQuery::JsxSelfClosingElement(jsx) => jsx.attributes(),
}
}

fn get_grandparent(&self) -> Option<JsxOpeningElement> {
if let NoImgElementQuery::JsxSelfClosingElement(jsx) = self {
return jsx
.parent::<JsxChildList>()?
.parent::<JsxElement>()?
.opening_element()
.ok();
}
None
}

fn is_target_valid(&self) -> Option<bool> {
if self.name()?.text_trimmed() != "img" || self.attributes().is_empty() {
return Some(false);
}

if let Some(grandparent) = self.get_grandparent() {
let name = grandparent.name().ok()?.name_value_token();
return Some(name.map_or(true, |name| name.text_trimmed() != "picture"));
}

Some(true)
}
}
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/options.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<>
<img alt="Foo" />

<div>
<img alt="Foo" />
</div>
</>
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
assertion_line: 86
expression: invalid.jsx
---
# Input
```jsx
<>
<img alt="Foo" />

<div>
<img alt="Foo" />
</div>
</>

```

# Diagnostics
```
invalid.jsx:2:2 lint/nursery/noImgElement ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! Using <img> could result in slower LCP and higher bandwidth.
1 │ <>
> 2 │ <img alt="Foo" />
│ ^^^^^^^^^^^^^^^^^
3 │
4 │ <div>
i Consider using <Image /> from next/image to automatically optimize images.
i This may incur additional usage or cost from your provider.
```

```
invalid.jsx:5:3 lint/nursery/noImgElement ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! Using <img> could result in slower LCP and higher bandwidth.
4 │ <div>
> 5 │ <img alt="Foo" />
│ ^^^^^^^^^^^^^^^^^
6 │ </div>
7 │ </>
i Consider using <Image /> from next/image to automatically optimize images.
i This may incur additional usage or cost from your provider.
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<>
<img />

<picture>
<img alt="Foo" />
</picture>
</>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
assertion_line: 86
expression: valid.jsx
---
# Input
```jsx
<>
<img />

<picture>
<img alt="Foo" />
</picture>
</>

```
5 changes: 5 additions & 0 deletions packages/@biomejs/backend-jsonrpc/src/workspace.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions packages/@biomejs/biome/configuration_schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 4e10665

Please sign in to comment.