Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lint): add no-head-element from eslint-plugin-next #4136

Merged
merged 8 commits into from
Oct 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.