diff --git a/crates/biome_analyze/src/rule.rs b/crates/biome_analyze/src/rule.rs index 4e7b3cc83588..c857205b5bdc 100644 --- a/crates/biome_analyze/src/rule.rs +++ b/crates/biome_analyze/src/rule.rs @@ -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), } @@ -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"), } } @@ -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, } } @@ -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}"), } } @@ -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"), } } diff --git a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs index 2cb9df5c0c4f..b2443a178978 100644 --- a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs +++ b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs @@ -14,6 +14,14 @@ pub(crate) fn migrate_eslint_any_rule( let rule = group.no_this_in_static.get_or_insert(Default::default()); rule.set_level(rule_severity.into()); } + "@next/no-head-element" => { + if !options.include_nursery { + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group.no_head_element.get_or_insert(Default::default()); + rule.set_level(rule_severity.into()); + } "@stylistic/jsx-self-closing-comp" => { let group = rules.style.get_or_insert_with(Default::default); let rule = group diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs index cfd65435b7f2..a50a2224d423 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -3301,6 +3301,9 @@ pub struct Nursery { #[serde(skip_serializing_if = "Option::is_none")] pub no_exported_imports: Option>, + #[doc = "Prevent usage of \\ element in a Next.js project."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_head_element: Option>, #[doc = "Disallows the use of irregular whitespace characters."] #[serde(skip_serializing_if = "Option::is_none")] pub no_irregular_whitespace: @@ -3431,6 +3434,7 @@ impl Nursery { "noDynamicNamespaceImportAccess", "noEnum", "noExportedImports", + "noHeadElement", "noIrregularWhitespace", "noMissingVarFunction", "noNestedTernary", @@ -3479,11 +3483,11 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33]), @@ -3581,91 +3585,96 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7])); } } - if let Some(rule) = self.no_irregular_whitespace.as_ref() { + if let Some(rule) = self.no_head_element.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } } - if let Some(rule) = self.no_missing_var_function.as_ref() { + if let Some(rule) = self.no_irregular_whitespace.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } } - if let Some(rule) = self.no_nested_ternary.as_ref() { + if let Some(rule) = self.no_missing_var_function.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } } - if let Some(rule) = self.no_octal_escape.as_ref() { + if let Some(rule) = self.no_nested_ternary.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } } - if let Some(rule) = self.no_process_env.as_ref() { + if let Some(rule) = self.no_octal_escape.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } } - if let Some(rule) = self.no_restricted_imports.as_ref() { + if let Some(rule) = self.no_process_env.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } } - if let Some(rule) = self.no_restricted_types.as_ref() { + if let Some(rule) = self.no_restricted_imports.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } } - if let Some(rule) = self.no_secrets.as_ref() { + if let Some(rule) = self.no_restricted_types.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } } - if let Some(rule) = self.no_static_element_interactions.as_ref() { + if let Some(rule) = self.no_secrets.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - if let Some(rule) = self.no_substr.as_ref() { + if let Some(rule) = self.no_static_element_interactions.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } - if let Some(rule) = self.no_template_curly_in_string.as_ref() { + if let Some(rule) = self.no_substr.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - if let Some(rule) = self.no_unknown_pseudo_class.as_ref() { + if let Some(rule) = self.no_template_curly_in_string.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } } - if let Some(rule) = self.no_unknown_pseudo_element.as_ref() { + if let Some(rule) = self.no_unknown_pseudo_class.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.no_useless_escape_in_regex.as_ref() { + if let Some(rule) = self.no_unknown_pseudo_element.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } } - if let Some(rule) = self.no_value_at_rule.as_ref() { + if let Some(rule) = self.no_useless_escape_in_regex.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() { + if let Some(rule) = self.no_value_at_rule.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } } - if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() { + if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } } + if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); + } + } if let Some(rule) = self.use_collapsed_if.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); @@ -3765,141 +3774,146 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7])); } } - if let Some(rule) = self.no_irregular_whitespace.as_ref() { + if let Some(rule) = self.no_head_element.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } } - if let Some(rule) = self.no_missing_var_function.as_ref() { + if let Some(rule) = self.no_irregular_whitespace.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } } - if let Some(rule) = self.no_nested_ternary.as_ref() { + if let Some(rule) = self.no_missing_var_function.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } } - if let Some(rule) = self.no_octal_escape.as_ref() { + if let Some(rule) = self.no_nested_ternary.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } } - if let Some(rule) = self.no_process_env.as_ref() { + if let Some(rule) = self.no_octal_escape.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } } - if let Some(rule) = self.no_restricted_imports.as_ref() { + if let Some(rule) = self.no_process_env.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } } - if let Some(rule) = self.no_restricted_types.as_ref() { + if let Some(rule) = self.no_restricted_imports.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } } - if let Some(rule) = self.no_secrets.as_ref() { + if let Some(rule) = self.no_restricted_types.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } } - if let Some(rule) = self.no_static_element_interactions.as_ref() { + if let Some(rule) = self.no_secrets.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - if let Some(rule) = self.no_substr.as_ref() { + if let Some(rule) = self.no_static_element_interactions.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } - if let Some(rule) = self.no_template_curly_in_string.as_ref() { + if let Some(rule) = self.no_substr.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - if let Some(rule) = self.no_unknown_pseudo_class.as_ref() { + if let Some(rule) = self.no_template_curly_in_string.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } } - if let Some(rule) = self.no_unknown_pseudo_element.as_ref() { + if let Some(rule) = self.no_unknown_pseudo_class.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.no_useless_escape_in_regex.as_ref() { + if let Some(rule) = self.no_unknown_pseudo_element.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } } - if let Some(rule) = self.no_value_at_rule.as_ref() { + if let Some(rule) = self.no_useless_escape_in_regex.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() { + if let Some(rule) = self.no_value_at_rule.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } } - if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() { + if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } } - if let Some(rule) = self.use_collapsed_if.as_ref() { + if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); } } - if let Some(rule) = self.use_component_export_only_modules.as_ref() { + if let Some(rule) = self.use_collapsed_if.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26])); } } - if let Some(rule) = self.use_consistent_curly_braces.as_ref() { + if let Some(rule) = self.use_component_export_only_modules.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } } - if let Some(rule) = self.use_consistent_member_accessibility.as_ref() { + if let Some(rule) = self.use_consistent_curly_braces.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - if let Some(rule) = self.use_deprecated_reason.as_ref() { + if let Some(rule) = self.use_consistent_member_accessibility.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.use_explicit_function_return_type.as_ref() { + if let Some(rule) = self.use_deprecated_reason.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_explicit_function_return_type.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.use_sorted_classes.as_ref() { + if let Some(rule) = self.use_import_restrictions.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } - if let Some(rule) = self.use_strict_mode.as_ref() { + if let Some(rule) = self.use_sorted_classes.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } - if let Some(rule) = self.use_trim_start_end.as_ref() { + if let Some(rule) = self.use_strict_mode.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } } + if let Some(rule) = self.use_trim_start_end.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); + } + } if let Some(rule) = self.use_valid_autocomplete.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); @@ -3973,6 +3987,10 @@ impl Nursery { .no_exported_imports .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "noHeadElement" => self + .no_head_element + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "noIrregularWhitespace" => self .no_irregular_whitespace .as_ref() diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index b4c7949e1aea..3a458503a58f 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -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", diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index 0ede4f7956eb..da2ecb61731d 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -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; @@ -40,6 +41,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 , diff --git a/crates/biome_js_analyze/src/lint/nursery/no_head_element.rs b/crates/biome_js_analyze/src/lint/nursery/no_head_element.rs new file mode 100644 index 000000000000..8384ae9f9368 --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/no_head_element.rs @@ -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 `` element in a Next.js project. + /// + /// Next.js provides a specialized `` component from `next/head` that manages + /// the `` tag for optimal server-side rendering, client-side navigation, and + /// automatic deduplication of tags such as `` and ``. + /// + /// 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 + /// + /// ) + /// } + /// ``` + /// + /// ### Valid + /// + /// ```jsx + /// import Head from 'next/head' + /// + /// function Index() { + /// return ( + /// + /// All good! + /// + /// ) + /// } + /// ``` + 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; + type State = TextRange; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> 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, range: &Self::State) -> Option { + return Some(RuleDiagnostic::new( + rule_category!(), + range, + markup! { "Don't use """" element." }, + ).note(markup! { + "Using the """" element can cause unexpected behavior in a Next.js application. Use """" from ""next/head"" instead." + })); + } +} diff --git a/crates/biome_js_analyze/src/options.rs b/crates/biome_js_analyze/src/options.rs index e3bd17a0a61c..1c9b0b4ecb16 100644 --- a/crates/biome_js_analyze/src/options.rs +++ b/crates/biome_js_analyze/src/options.rs @@ -111,6 +111,8 @@ pub type NoGlobalIsFinite = pub type NoGlobalIsNan = ::Options; pub type NoGlobalObjectCalls = < lint :: correctness :: no_global_object_calls :: NoGlobalObjectCalls as biome_analyze :: Rule > :: Options ; +pub type NoHeadElement = + ::Options; pub type NoHeaderScope = ::Options; pub type NoImplicitAnyLet = diff --git a/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/app/valid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/app/valid.jsx new file mode 100644 index 000000000000..c5f12a3b2fb7 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/app/valid.jsx @@ -0,0 +1,3 @@ + + No diagnostic + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/app/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/app/valid.jsx.snap new file mode 100644 index 000000000000..6b213094e0c7 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/app/valid.jsx.snap @@ -0,0 +1,12 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: valid.jsx +--- +# Input +```jsx + + No diagnostic + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/invalid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/invalid.jsx new file mode 100644 index 000000000000..c426e2e2e3d4 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/invalid.jsx @@ -0,0 +1,3 @@ + + Invalid + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/invalid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/invalid.jsx.snap new file mode 100644 index 000000000000..96d3af536e97 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/invalid.jsx.snap @@ -0,0 +1,27 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.jsx +--- +# Input +```jsx + + Invalid + + +``` + +# Diagnostics +``` +invalid.jsx:1:1 lint/nursery/noHeadElement ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't use element. + + > 1 │ + │ ^^^^^^ + 2 │ Invalid + 3 │ + + i Using the element can cause unexpected behavior in a Next.js application. Use from next/head instead. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/valid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/valid.jsx new file mode 100644 index 000000000000..4281d09e26aa --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/valid.jsx @@ -0,0 +1,4 @@ + + Valid + + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/valid.jsx.snap new file mode 100644 index 000000000000..9c402b696a51 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/valid.jsx.snap @@ -0,0 +1,13 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: valid.jsx +--- +# Input +```jsx + + Valid + + + +``` diff --git a/crates/biome_test_utils/src/lib.rs b/crates/biome_test_utils/src/lib.rs index 8906f0cf7e06..6191e2eccc95 100644 --- a/crates/biome_test_utils/src/lib.rs +++ b/crates/biome_test_utils/src/lib.rs @@ -204,13 +204,19 @@ pub fn code_fix_to_string(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 diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 03b155b667ac..6bb6b4523977 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1250,6 +1250,10 @@ export interface Nursery { * Disallow exporting an imported variable. */ noExportedImports?: RuleConfiguration_for_Null; + /** + * Prevent usage of \ element in a Next.js project. + */ + noHeadElement?: RuleConfiguration_for_Null; /** * Disallows the use of irregular whitespace characters. */ @@ -2866,6 +2870,7 @@ export type Category = | "lint/nursery/noMissingGenericFamilyKeyword" | "lint/nursery/noMissingVarFunction" | "lint/nursery/noNestedTernary" + | "lint/nursery/noHeadElement" | "lint/nursery/noOctalEscape" | "lint/nursery/noProcessEnv" | "lint/nursery/noReactSpecificProps" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 5ec2d179290b..7b77d43f9a9b 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -2123,6 +2123,13 @@ { "type": "null" } ] }, + "noHeadElement": { + "description": "Prevent usage of \\ element in a Next.js project.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noIrregularWhitespace": { "description": "Disallows the use of irregular whitespace characters.", "anyOf": [