Skip to content

Commit

Permalink
feat(lint): add no-head-element from eslint-plugin-next (#4136)
Browse files Browse the repository at this point in the history
Co-authored-by: Emanuele Stoppa <[email protected]>
  • Loading branch information
kaioduarte and ematipico authored Oct 5, 2024
1 parent 5b6e55b commit 35bb699
Show file tree
Hide file tree
Showing 16 changed files with 271 additions and 64 deletions.
6 changes: 6 additions & 0 deletions crates/biome_analyze/src/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ pub enum RuleSource {
EslintBarrelFiles(&'static str),
/// Rules from [Eslint Plugin N](https://github.com/eslint-community/eslint-plugin-n)
EslintN(&'static str),
/// Rules from [Eslint Plugin Next](https://github.com/vercel/next.js/tree/canary/packages/eslint-plugin-next)
EslintNext(&'static str),
/// Rules from [Stylelint](https://github.com/stylelint/stylelint)
Stylelint(&'static str),
}
Expand Down Expand Up @@ -158,6 +160,7 @@ impl std::fmt::Display for RuleSource {
Self::EslintMysticatea(_) => write!(f, "@mysticatea/eslint-plugin"),
Self::EslintBarrelFiles(_) => write!(f, "eslint-plugin-barrel-files"),
Self::EslintN(_) => write!(f, "eslint-plugin-n"),
Self::EslintNext(_) => write!(f, "@next/eslint-plugin-next"),
Self::Stylelint(_) => write!(f, "Stylelint"),
}
}
Expand Down Expand Up @@ -207,6 +210,7 @@ impl RuleSource {
| Self::EslintMysticatea(rule_name)
| Self::EslintBarrelFiles(rule_name)
| Self::EslintN(rule_name)
| Self::EslintNext(rule_name)
| Self::Stylelint(rule_name) => rule_name,
}
}
Expand All @@ -231,6 +235,7 @@ impl RuleSource {
Self::EslintMysticatea(rule_name) => format!("@mysticatea/{rule_name}"),
Self::EslintBarrelFiles(rule_name) => format!("barrel-files/{rule_name}"),
Self::EslintN(rule_name) => format!("n/{rule_name}"),
Self::EslintNext(rule_name) => format!("@next/{rule_name}"),
Self::Stylelint(rule_name) => format!("stylelint/{rule_name}"),
}
}
Expand All @@ -256,6 +261,7 @@ impl RuleSource {
Self::EslintMysticatea(rule_name) => format!("https://github.com/mysticatea/eslint-plugin/blob/master/docs/rules/{rule_name}.md"),
Self::EslintBarrelFiles(rule_name) => format!("https://github.com/thepassle/eslint-plugin-barrel-files/blob/main/docs/rules/{rule_name}.md"),
Self::EslintN(rule_name) => format!("https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/{rule_name}.md"),
Self::EslintNext(rule_name) => format!("https://nextjs.org/docs/messages/{rule_name}"),
Self::Stylelint(rule_name) => format!("https://github.com/stylelint/stylelint/blob/main/lib/rules/{rule_name}/README.md"),
}
}
Expand Down

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 @@ -153,6 +153,7 @@ define_categories! {
"lint/nursery/noMissingGenericFamilyKeyword": "https://biomejs.dev/linter/rules/no-missing-generic-family-keyword",
"lint/nursery/noMissingVarFunction": "https://biomejs.dev/linter/rules/no-missing-var-function",
"lint/nursery/noNestedTernary": "https://biomejs.dev/linter/rules/no-nested-ternary",
"lint/nursery/noHeadElement": "https://biomejs.dev/linter/rules/no-head-element",
"lint/nursery/noOctalEscape": "https://biomejs.dev/linter/rules/no-octal-escape",
"lint/nursery/noProcessEnv": "https://biomejs.dev/linter/rules/no-process-env",
"lint/nursery/noReactSpecificProps": "https://biomejs.dev/linter/rules/no-react-specific-props",
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 @@ -7,6 +7,7 @@ pub mod no_duplicate_else_if;
pub mod no_dynamic_namespace_import_access;
pub mod no_enum;
pub mod no_exported_imports;
pub mod no_head_element;
pub mod no_irregular_whitespace;
pub mod no_nested_ternary;
pub mod no_octal_escape;
Expand Down Expand Up @@ -39,6 +40,7 @@ declare_lint_group! {
self :: no_dynamic_namespace_import_access :: NoDynamicNamespaceImportAccess ,
self :: no_enum :: NoEnum ,
self :: no_exported_imports :: NoExportedImports ,
self :: no_head_element :: NoHeadElement ,
self :: no_irregular_whitespace :: NoIrregularWhitespace ,
self :: no_nested_ternary :: NoNestedTernary ,
self :: no_octal_escape :: NoOctalEscape ,
Expand Down
89 changes: 89 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery/no_head_element.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use biome_analyze::RuleSourceKind;
use biome_analyze::{
context::RuleContext, declare_lint_rule, Ast, Rule, RuleDiagnostic, RuleSource,
};
use biome_console::markup;
use biome_js_syntax::JsxOpeningElement;
use biome_rowan::AstNode;
use biome_rowan::TextRange;

declare_lint_rule! {
/// Prevent usage of `<head>` element in a Next.js project.
///
/// Next.js provides a specialized `<Head />` component from `next/head` that manages
/// the `<head>` tag for optimal server-side rendering, client-side navigation, and
/// automatic deduplication of tags such as `<meta>` and `<title>`.
///
/// This rule only checks files that are outside of the [`app/` directory](https://nextjs.org/docs/app), as it's typically
/// handled differently in Next.js.
///
/// ## Examples
///
/// ### Invalid
/// ```jsx,expect_diagnostic
/// function Index() {
/// return (
/// <head>
/// <title>Invalid</title>
/// </head>
/// )
/// }
/// ```
///
/// ### Valid
///
/// ```jsx
/// import Head from 'next/head'
///
/// function Index() {
/// return (
/// <Head>
/// <title>All good!</title>
/// </Head>
/// )
/// }
/// ```
pub NoHeadElement {
version: "next",
name: "noHeadElement",
language: "jsx",
sources: &[RuleSource::EslintNext("no-head-element")],
source_kind: RuleSourceKind::SameLogic,
recommended: false,
}
}

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

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let element = ctx.query();
let name = element.name().ok()?.name_value_token()?;

if name.text_trimmed() == "head" {
let is_in_app_dir = ctx
.file_path()
.ancestors()
.any(|a| a.file_name().map_or(false, |f| f == "app" && a.is_dir()));

if !is_in_app_dir {
return Some(element.syntax().text_range());
}
}

None
}

fn diagnostic(_: &RuleContext<Self>, range: &Self::State) -> Option<RuleDiagnostic> {
return Some(RuleDiagnostic::new(
rule_category!(),
range,
markup! { "Don't use "<Emphasis>"<head>"</Emphasis>" element." },
).note(markup! {
"Using the "<Emphasis>"<head>"</Emphasis>" element can cause unexpected behavior in a Next.js application. Use "<Emphasis>"<Head />"</Emphasis>" from "<Emphasis>"next/head"</Emphasis>" instead."
}));
}
}
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,3 @@
<head>
<title>No diagnostic</title>
</head>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
assertion_line: 86
expression: valid.jsx
---
# Input
```jsx
<head>
<title>No diagnostic</title>
</head>

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<head>
<title>Invalid</title>
</head>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: invalid.jsx
---
# Input
```jsx
<head>
<title>Invalid</title>
</head>

```

# Diagnostics
```
invalid.jsx:1:1 lint/nursery/noHeadElement ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! Don't use <head> element.
> 1 │ <head>
│ ^^^^^^
2 │ <title>Invalid</title>
3 │ </head>
i Using the <head> element can cause unexpected behavior in a Next.js application. Use <Head /> from next/head instead.
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<Head>
<title>Valid</title>
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
</Head>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
assertion_line: 86
expression: valid.jsx
---
# Input
```jsx
<Head>
<title>Valid</title>
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
</Head>

```
16 changes: 11 additions & 5 deletions crates/biome_test_utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +204,19 @@ pub fn code_fix_to_string<L: ServiceLanguage>(source: &str, action: AnalyzerActi
/// corresponding to the directory name. E.g., `style/useWhile/test.js`
/// will be analyzed with just the `style/useWhile` rule.
pub fn parse_test_path(file: &Path) -> (&str, &str) {
let rule_folder = file.parent().unwrap();
let rule_name = rule_folder.file_name().unwrap();
let mut group_name = "";
let mut rule_name = "";

let group_folder = rule_folder.parent().unwrap();
let group_name = group_folder.file_name().unwrap();
for component in file.iter().rev() {
if component == "specs" || component == "suppression" {
break;
}

rule_name = group_name;
group_name = component.to_str().unwrap_or_default();
}

(group_name.to_str().unwrap(), rule_name.to_str().unwrap())
(group_name, rule_name)
}

/// This check is used in the parser test to ensure it doesn't emit
Expand Down
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 35bb699

Please sign in to comment.