From b0db23022fb1464a3564f158b9f469055d5057ff Mon Sep 17 00:00:00 2001 From: kaioduarte Date: Sun, 29 Sep 2024 22:08:15 +0100 Subject: [PATCH 1/8] chore: support nested folders on tests --- crates/biome_test_utils/src/lib.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/crates/biome_test_utils/src/lib.rs b/crates/biome_test_utils/src/lib.rs index 8906f0cf7e06..61c7ea188697 100644 --- a/crates/biome_test_utils/src/lib.rs +++ b/crates/biome_test_utils/src/lib.rs @@ -204,13 +204,25 @@ 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 root_found = false; + let mut group_name = ""; + let mut rule_name = ""; + + for component in file.iter().rev() { + if component == "specs" || component == "suppression" { + root_found = true; + break; + } + + rule_name = group_name; + group_name = component.to_str().unwrap_or_default(); + } - let group_folder = rule_folder.parent().unwrap(); - let group_name = group_folder.file_name().unwrap(); + if !root_found { + panic!("Failed to find group and rule"); + } - (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 From 83c8b61620913df301ec72fdd2a6367a9d713635 Mon Sep 17 00:00:00 2001 From: kaioduarte Date: Mon, 30 Sep 2024 10:37:23 +0100 Subject: [PATCH 2/8] implement `no-head-element` from `eslint-plugin-next` --- crates/biome_analyze/src/rule.rs | 6 + .../migrate/eslint_any_rule_to_biome.rs | 8 ++ .../src/analyzer/linter/rules.rs | 124 ++++++++++-------- .../src/categories.rs | 1 + crates/biome_js_analyze/src/lint/nursery.rs | 2 + .../src/lint/nursery/no_next_head_element.rs | 86 ++++++++++++ crates/biome_js_analyze/src/options.rs | 2 + .../nursery/noNextHeadElement/app/valid.jsx | 3 + .../noNextHeadElement/app/valid.jsx.snap | 12 ++ .../noNextHeadElement/pages/invalid.jsx | 3 + .../noNextHeadElement/pages/invalid.jsx.snap | 26 ++++ .../nursery/noNextHeadElement/pages/valid.jsx | 4 + .../noNextHeadElement/pages/valid.jsx.snap | 13 ++ .../@biomejs/backend-jsonrpc/src/workspace.ts | 5 + .../@biomejs/biome/configuration_schema.json | 7 + 15 files changed, 250 insertions(+), 52 deletions(-) create mode 100644 crates/biome_js_analyze/src/lint/nursery/no_next_head_element.rs create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/app/valid.jsx create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/app/valid.jsx.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/pages/invalid.jsx create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/pages/invalid.jsx.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/pages/valid.jsx create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/pages/valid.jsx.snap 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 16318d7cd703..378ad3e3341e 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_next_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 b7cac6a2ab55..a1fb552f4f99 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -3312,6 +3312,10 @@ pub struct Nursery { #[doc = "Disallow nested ternary expressions."] #[serde(skip_serializing_if = "Option::is_none")] pub no_nested_ternary: Option>, + #[doc = "Prevent usage of \\ element."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_next_head_element: + Option>, #[doc = "Disallow octal escape sequences in string literals"] #[serde(skip_serializing_if = "Option::is_none")] pub no_octal_escape: Option>, @@ -3431,6 +3435,7 @@ impl Nursery { "noIrregularWhitespace", "noMissingVarFunction", "noNestedTernary", + "noNextHeadElement", "noOctalEscape", "noProcessEnv", "noRestrictedImports", @@ -3476,13 +3481,13 @@ impl Nursery { 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[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[27]), + 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[32]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), @@ -3520,6 +3525,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -3591,126 +3597,131 @@ impl Nursery { 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_next_head_element.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_component_export_only_modules.as_ref() { + 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_consistent_curly_braces.as_ref() { + if let Some(rule) = self.use_component_export_only_modules.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26])); } } - 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_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } } - if let Some(rule) = self.use_deprecated_reason.as_ref() { + if let Some(rule) = self.use_consistent_member_accessibility.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - 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_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - 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_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.use_sorted_classes.as_ref() { + if let Some(rule) = self.use_import_restrictions.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.use_strict_mode.as_ref() { + if let Some(rule) = self.use_sorted_classes.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } - if let Some(rule) = self.use_trim_start_end.as_ref() { + if let Some(rule) = self.use_strict_mode.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } - if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if let Some(rule) = self.use_trim_start_end.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } } + if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> FxHashSet> { @@ -3770,126 +3781,131 @@ impl Nursery { 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_next_head_element.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_component_export_only_modules.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_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[26])); } } - 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[27])); } } - 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[28])); } } - 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[29])); } } - 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[30])); } } - 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[31])); } } - 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[32])); } } - 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[33])); } } - if let Some(rule) = self.use_valid_autocomplete.as_ref() { + 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[34])); } } + 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])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -3970,6 +3986,10 @@ impl Nursery { .no_nested_ternary .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "noNextHeadElement" => self + .no_next_head_element + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "noOctalEscape" => self .no_octal_escape .as_ref() diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index c8321d685ad8..c0604ed12bdd 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/noNextHeadElement": "https://biomejs.dev/linter/rules/no-next-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 c808f8122d78..87ad47fb8e3b 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -9,6 +9,7 @@ pub mod no_enum; pub mod no_exported_imports; pub mod no_irregular_whitespace; pub mod no_nested_ternary; +pub mod no_next_head_element; pub mod no_octal_escape; pub mod no_process_env; pub mod no_restricted_imports; @@ -41,6 +42,7 @@ declare_lint_group! { self :: no_exported_imports :: NoExportedImports , self :: no_irregular_whitespace :: NoIrregularWhitespace , self :: no_nested_ternary :: NoNestedTernary , + self :: no_next_head_element :: NoNextHeadElement , self :: no_octal_escape :: NoOctalEscape , self :: no_process_env :: NoProcessEnv , self :: no_restricted_imports :: NoRestrictedImports , diff --git a/crates/biome_js_analyze/src/lint/nursery/no_next_head_element.rs b/crates/biome_js_analyze/src/lint/nursery/no_next_head_element.rs new file mode 100644 index 000000000000..1bce1271401e --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/no_next_head_element.rs @@ -0,0 +1,86 @@ +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. + /// + /// A `` element was used to include page-level metadata, but this can + /// cause unexpected behavior in a Next.js application. Use Next.js' built-in + /// `next/head` component instead. + /// + /// ## Examples + /// + /// ### Invalid + /// ```jsx,expect_diagnostic + /// // /pages/index.jsx + /// function Index() { + /// return ( + /// + /// Invalid + /// + /// ) + /// } + /// ``` + /// + /// ### Valid + /// + /// ```jsx + /// // /pages/index.jsx + /// import Head from 'next/head' + /// + /// function Index() { + /// return ( + /// + /// All good! + /// + /// ) + /// } + /// ``` + pub NoNextHeadElement { + version: "next", + name: "noNextHeadElement", + language: "js", + sources: &[RuleSource::EslintNext("no-head-element")], + source_kind: RuleSourceKind::SameLogic, + recommended: false, + } +} + +impl Rule for NoNextHeadElement { + 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 path = ctx.file_path().as_os_str().to_str()?; + let is_in_app_dir = path.contains(&format!("app{}", std::path::MAIN_SEPARATOR)); + + 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! { + "Do not use """" element. Use """" from ""next/head"" instead." + }, + )); + } +} diff --git a/crates/biome_js_analyze/src/options.rs b/crates/biome_js_analyze/src/options.rs index 8babc8375238..e7b9df602257 100644 --- a/crates/biome_js_analyze/src/options.rs +++ b/crates/biome_js_analyze/src/options.rs @@ -147,6 +147,8 @@ pub type NoNestedTernary = ::Options; pub type NoNewSymbol = ::Options; +pub type NoNextHeadElement = + ::Options; pub type NoNodejsModules = ::Options; pub type NoNonNullAssertion = diff --git a/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/app/valid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/app/valid.jsx new file mode 100644 index 000000000000..c5f12a3b2fb7 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/app/valid.jsx @@ -0,0 +1,3 @@ + + No diagnostic + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/app/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/app/valid.jsx.snap new file mode 100644 index 000000000000..6b213094e0c7 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/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/noNextHeadElement/pages/invalid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/pages/invalid.jsx new file mode 100644 index 000000000000..c426e2e2e3d4 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/pages/invalid.jsx @@ -0,0 +1,3 @@ + + Invalid + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/pages/invalid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/pages/invalid.jsx.snap new file mode 100644 index 000000000000..1dda9a8acc9a --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/pages/invalid.jsx.snap @@ -0,0 +1,26 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: invalid.jsx +--- +# Input +```jsx + + Invalid + + +``` + +# Diagnostics +``` +invalid.jsx:1:1 lint/nursery/noNextHeadElement ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use element. Use from next/head instead. + + > 1 │ + │ ^^^^^^ + 2 │ Invalid + 3 │ + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/pages/valid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/pages/valid.jsx new file mode 100644 index 000000000000..4281d09e26aa --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/pages/valid.jsx @@ -0,0 +1,4 @@ + + Valid + + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/pages/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/pages/valid.jsx.snap new file mode 100644 index 000000000000..9c402b696a51 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/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/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index d10410948d90..206914ac2b27 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1262,6 +1262,10 @@ export interface Nursery { * Disallow nested ternary expressions. */ noNestedTernary?: RuleConfiguration_for_Null; + /** + * Prevent usage of \ element. + */ + noNextHeadElement?: RuleConfiguration_for_Null; /** * Disallow octal escape sequences in string literals */ @@ -2854,6 +2858,7 @@ export type Category = | "lint/nursery/noMissingGenericFamilyKeyword" | "lint/nursery/noMissingVarFunction" | "lint/nursery/noNestedTernary" + | "lint/nursery/noNextHeadElement" | "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 9f725c5c0a2e..283f1f836a64 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -2153,6 +2153,13 @@ { "type": "null" } ] }, + "noNextHeadElement": { + "description": "Prevent usage of \\ element.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noOctalEscape": { "description": "Disallow octal escape sequences in string literals", "anyOf": [ From 4a94e6d943d81aa710e77169b1c9c10de2444777 Mon Sep 17 00:00:00 2001 From: kaioduarte Date: Mon, 30 Sep 2024 11:26:21 +0100 Subject: [PATCH 3/8] rename rule `noNextHeadElement` -> `noHeadElement` --- .../migrate/eslint_any_rule_to_biome.rs | 2 +- .../src/analyzer/linter/rules.rs | 35 +++++++++---------- .../src/categories.rs | 2 +- crates/biome_js_analyze/src/lint/nursery.rs | 4 +-- ...ext_head_element.rs => no_head_element.rs} | 6 ++-- crates/biome_js_analyze/src/options.rs | 4 +-- .../app/valid.jsx | 0 .../app/valid.jsx.snap | 0 .../pages/invalid.jsx | 0 .../pages/invalid.jsx.snap | 2 +- .../pages/valid.jsx | 0 .../pages/valid.jsx.snap | 0 .../@biomejs/backend-jsonrpc/src/workspace.ts | 10 +++--- .../@biomejs/biome/configuration_schema.json | 14 ++++---- 14 files changed, 39 insertions(+), 40 deletions(-) rename crates/biome_js_analyze/src/lint/nursery/{no_next_head_element.rs => no_head_element.rs} (95%) rename crates/biome_js_analyze/tests/specs/nursery/{noNextHeadElement => noHeadElement}/app/valid.jsx (100%) rename crates/biome_js_analyze/tests/specs/nursery/{noNextHeadElement => noHeadElement}/app/valid.jsx.snap (100%) rename crates/biome_js_analyze/tests/specs/nursery/{noNextHeadElement => noHeadElement}/pages/invalid.jsx (100%) rename crates/biome_js_analyze/tests/specs/nursery/{noNextHeadElement => noHeadElement}/pages/invalid.jsx.snap (73%) rename crates/biome_js_analyze/tests/specs/nursery/{noNextHeadElement => noHeadElement}/pages/valid.jsx (100%) rename crates/biome_js_analyze/tests/specs/nursery/{noNextHeadElement => noHeadElement}/pages/valid.jsx.snap (100%) 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 378ad3e3341e..9d2b05410bc3 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 @@ -19,7 +19,7 @@ pub(crate) fn migrate_eslint_any_rule( return false; } let group = rules.nursery.get_or_insert_with(Default::default); - let rule = group.no_next_head_element.get_or_insert(Default::default()); + let rule = group.no_head_element.get_or_insert(Default::default()); rule.set_level(rule_severity.into()); } "@stylistic/jsx-self-closing-comp" => { diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs index a1fb552f4f99..a778dd6e3eea 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."] + #[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: @@ -3312,10 +3315,6 @@ pub struct Nursery { #[doc = "Disallow nested ternary expressions."] #[serde(skip_serializing_if = "Option::is_none")] pub no_nested_ternary: Option>, - #[doc = "Prevent usage of \\ element."] - #[serde(skip_serializing_if = "Option::is_none")] - pub no_next_head_element: - Option>, #[doc = "Disallow octal escape sequences in string literals"] #[serde(skip_serializing_if = "Option::is_none")] pub no_octal_escape: Option>, @@ -3432,10 +3431,10 @@ impl Nursery { "noDynamicNamespaceImportAccess", "noEnum", "noExportedImports", + "noHeadElement", "noIrregularWhitespace", "noMissingVarFunction", "noNestedTernary", - "noNextHeadElement", "noOctalEscape", "noProcessEnv", "noRestrictedImports", @@ -3480,7 +3479,7 @@ 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[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[22]), @@ -3582,22 +3581,22 @@ 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_next_head_element.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])); } @@ -3766,22 +3765,22 @@ 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_next_head_element.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])); } @@ -3974,6 +3973,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() @@ -3986,10 +3989,6 @@ impl Nursery { .no_nested_ternary .as_ref() .map(|conf| (conf.level(), conf.get_options())), - "noNextHeadElement" => self - .no_next_head_element - .as_ref() - .map(|conf| (conf.level(), conf.get_options())), "noOctalEscape" => self .no_octal_escape .as_ref() diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index c0604ed12bdd..adf59f54d2bf 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -153,7 +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/noNextHeadElement": "https://biomejs.dev/linter/rules/no-next-head-element", + "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 87ad47fb8e3b..262787628b06 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -7,9 +7,9 @@ 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_next_head_element; pub mod no_octal_escape; pub mod no_process_env; pub mod no_restricted_imports; @@ -40,9 +40,9 @@ 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_next_head_element :: NoNextHeadElement , self :: no_octal_escape :: NoOctalEscape , self :: no_process_env :: NoProcessEnv , self :: no_restricted_imports :: NoRestrictedImports , diff --git a/crates/biome_js_analyze/src/lint/nursery/no_next_head_element.rs b/crates/biome_js_analyze/src/lint/nursery/no_head_element.rs similarity index 95% rename from crates/biome_js_analyze/src/lint/nursery/no_next_head_element.rs rename to crates/biome_js_analyze/src/lint/nursery/no_head_element.rs index 1bce1271401e..06af42127f18 100644 --- a/crates/biome_js_analyze/src/lint/nursery/no_next_head_element.rs +++ b/crates/biome_js_analyze/src/lint/nursery/no_head_element.rs @@ -42,9 +42,9 @@ declare_lint_rule! { /// ) /// } /// ``` - pub NoNextHeadElement { + pub NoHeadElement { version: "next", - name: "noNextHeadElement", + name: "noHeadElement", language: "js", sources: &[RuleSource::EslintNext("no-head-element")], source_kind: RuleSourceKind::SameLogic, @@ -52,7 +52,7 @@ declare_lint_rule! { } } -impl Rule for NoNextHeadElement { +impl Rule for NoHeadElement { type Query = Ast; type State = TextRange; type Signals = Option; diff --git a/crates/biome_js_analyze/src/options.rs b/crates/biome_js_analyze/src/options.rs index e7b9df602257..0b349a72b4ff 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 = @@ -147,8 +149,6 @@ pub type NoNestedTernary = ::Options; pub type NoNewSymbol = ::Options; -pub type NoNextHeadElement = - ::Options; pub type NoNodejsModules = ::Options; pub type NoNonNullAssertion = diff --git a/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/app/valid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/app/valid.jsx similarity index 100% rename from crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/app/valid.jsx rename to crates/biome_js_analyze/tests/specs/nursery/noHeadElement/app/valid.jsx diff --git a/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/app/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/app/valid.jsx.snap similarity index 100% rename from crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/app/valid.jsx.snap rename to crates/biome_js_analyze/tests/specs/nursery/noHeadElement/app/valid.jsx.snap diff --git a/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/pages/invalid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/invalid.jsx similarity index 100% rename from crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/pages/invalid.jsx rename to crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/invalid.jsx diff --git a/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/pages/invalid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/invalid.jsx.snap similarity index 73% rename from crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/pages/invalid.jsx.snap rename to crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/invalid.jsx.snap index 1dda9a8acc9a..6cc111d32a18 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/pages/invalid.jsx.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/invalid.jsx.snap @@ -13,7 +13,7 @@ expression: invalid.jsx # Diagnostics ``` -invalid.jsx:1:1 lint/nursery/noNextHeadElement ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:1:1 lint/nursery/noHeadElement ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! Do not use element. Use from next/head instead. diff --git a/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/pages/valid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/valid.jsx similarity index 100% rename from crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/pages/valid.jsx rename to crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/valid.jsx diff --git a/crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/pages/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/valid.jsx.snap similarity index 100% rename from crates/biome_js_analyze/tests/specs/nursery/noNextHeadElement/pages/valid.jsx.snap rename to crates/biome_js_analyze/tests/specs/nursery/noHeadElement/pages/valid.jsx.snap diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 206914ac2b27..6c6f86ae050a 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. + */ + noHeadElement?: RuleConfiguration_for_Null; /** * Disallows the use of irregular whitespace characters. */ @@ -1262,10 +1266,6 @@ export interface Nursery { * Disallow nested ternary expressions. */ noNestedTernary?: RuleConfiguration_for_Null; - /** - * Prevent usage of \ element. - */ - noNextHeadElement?: RuleConfiguration_for_Null; /** * Disallow octal escape sequences in string literals */ @@ -2858,7 +2858,7 @@ export type Category = | "lint/nursery/noMissingGenericFamilyKeyword" | "lint/nursery/noMissingVarFunction" | "lint/nursery/noNestedTernary" - | "lint/nursery/noNextHeadElement" + | "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 283f1f836a64..5b23ed52a179 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -2132,6 +2132,13 @@ { "type": "null" } ] }, + "noHeadElement": { + "description": "Prevent usage of \\ element.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noIrregularWhitespace": { "description": "Disallows the use of irregular whitespace characters.", "anyOf": [ @@ -2153,13 +2160,6 @@ { "type": "null" } ] }, - "noNextHeadElement": { - "description": "Prevent usage of \\ element.", - "anyOf": [ - { "$ref": "#/definitions/RuleConfiguration" }, - { "type": "null" } - ] - }, "noOctalEscape": { "description": "Disallow octal escape sequences in string literals", "anyOf": [ From d3837261b8c2af41579ada96cbd5b659f69cec7f Mon Sep 17 00:00:00 2001 From: kaioduarte Date: Mon, 30 Sep 2024 11:40:34 +0100 Subject: [PATCH 4/8] improve app dir check --- crates/biome_js_analyze/src/lint/nursery/no_head_element.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 index 06af42127f18..1942b1a19161 100644 --- a/crates/biome_js_analyze/src/lint/nursery/no_head_element.rs +++ b/crates/biome_js_analyze/src/lint/nursery/no_head_element.rs @@ -63,8 +63,10 @@ impl Rule for NoHeadElement { let name = element.name().ok()?.name_value_token()?; if name.text_trimmed() == "head" { - let path = ctx.file_path().as_os_str().to_str()?; - let is_in_app_dir = path.contains(&format!("app{}", std::path::MAIN_SEPARATOR)); + 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()); From a91c55ad9b6cdb04398c2e70a2301778cbf24690 Mon Sep 17 00:00:00 2001 From: kaioduarte Date: Mon, 30 Sep 2024 12:00:26 +0100 Subject: [PATCH 5/8] remove uncessary panic --- crates/biome_test_utils/src/lib.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crates/biome_test_utils/src/lib.rs b/crates/biome_test_utils/src/lib.rs index 61c7ea188697..6191e2eccc95 100644 --- a/crates/biome_test_utils/src/lib.rs +++ b/crates/biome_test_utils/src/lib.rs @@ -204,13 +204,11 @@ 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 mut root_found = false; let mut group_name = ""; let mut rule_name = ""; for component in file.iter().rev() { if component == "specs" || component == "suppression" { - root_found = true; break; } @@ -218,10 +216,6 @@ pub fn parse_test_path(file: &Path) -> (&str, &str) { group_name = component.to_str().unwrap_or_default(); } - if !root_found { - panic!("Failed to find group and rule"); - } - (group_name, rule_name) } From f49609e451e5668e173a73ab08095f89ebccf7c6 Mon Sep 17 00:00:00 2001 From: kaioduarte Date: Mon, 30 Sep 2024 12:46:41 +0100 Subject: [PATCH 6/8] review documentation and diagnostic --- .../src/analyzer/linter/rules.rs | 2 +- .../src/lint/nursery/no_head_element.rs | 23 ++++++++++--------- .../noHeadElement/pages/invalid.jsx.snap | 4 +++- .../@biomejs/backend-jsonrpc/src/workspace.ts | 2 +- .../@biomejs/biome/configuration_schema.json | 2 +- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs index a778dd6e3eea..44d8d3fcadf2 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -3301,7 +3301,7 @@ pub struct Nursery { #[serde(skip_serializing_if = "Option::is_none")] pub no_exported_imports: Option>, - #[doc = "Prevent usage of \\ element."] + #[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."] 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 index 1942b1a19161..8b7f4fabee0e 100644 --- a/crates/biome_js_analyze/src/lint/nursery/no_head_element.rs +++ b/crates/biome_js_analyze/src/lint/nursery/no_head_element.rs @@ -8,17 +8,19 @@ use biome_rowan::AstNode; use biome_rowan::TextRange; declare_lint_rule! { - /// Prevent usage of `` element. + /// Prevent usage of `` element in a Next.js project. /// - /// A `` element was used to include page-level metadata, but this can - /// cause unexpected behavior in a Next.js application. Use Next.js' built-in - /// `next/head` component instead. + /// 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 outside of the `app/` directory, as it's typically + /// handled differently in Next.js. /// /// ## Examples /// /// ### Invalid /// ```jsx,expect_diagnostic - /// // /pages/index.jsx /// function Index() { /// return ( /// <head> @@ -31,7 +33,6 @@ declare_lint_rule! { /// ### Valid /// /// ```jsx - /// // /pages/index.jsx /// import Head from 'next/head' /// /// function Index() { @@ -45,7 +46,7 @@ declare_lint_rule! { pub NoHeadElement { version: "next", name: "noHeadElement", - language: "js", + language: "jsx", sources: &[RuleSource::EslintNext("no-head-element")], source_kind: RuleSourceKind::SameLogic, recommended: false, @@ -80,9 +81,9 @@ impl Rule for NoHeadElement { return Some(RuleDiagnostic::new( rule_category!(), range, - markup! { - "Do not use "<Emphasis>"<head>"</Emphasis>" element. Use "<Emphasis>"<Head />"</Emphasis>" from "<Emphasis>"next/head"</Emphasis>" instead." - }, - )); + markup! { "Don't use "<Emphasis>"<head>"</Emphasis>" element." }, + ).note(markup! { + "Using "<Emphasis>"<head>"</Emphasis>" element can cause unexpected behavior in a Next.js application. Use "<Emphasis>"<Head />"</Emphasis>" from "<Emphasis>"next/head"</Emphasis>" instead." + })); } } 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 index 6cc111d32a18..e0cf5a73c7ba 100644 --- 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 @@ -15,12 +15,14 @@ expression: invalid.jsx ``` invalid.jsx:1:1 lint/nursery/noHeadElement ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ! Do not use <head> element. Use <Head /> from next/head instead. + ! Don't use <head> element. > 1 │ <head> │ ^^^^^^ 2 │ <title>Invalid 3 │ + i Using element can cause unexpected behavior in a Next.js application. Use from next/head instead. + ``` diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 6c6f86ae050a..cd0308e2a470 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1251,7 +1251,7 @@ export interface Nursery { */ noExportedImports?: RuleConfiguration_for_Null; /** - * Prevent usage of \ element. + * Prevent usage of \ element in a Next.js project. */ noHeadElement?: RuleConfiguration_for_Null; /** diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 5b23ed52a179..310aa553359f 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -2133,7 +2133,7 @@ ] }, "noHeadElement": { - "description": "Prevent usage of \\ element.", + "description": "Prevent usage of \\ element in a Next.js project.", "anyOf": [ { "$ref": "#/definitions/RuleConfiguration" }, { "type": "null" } From 35c62b4c64429d78daaba32db70adb8e29ff63f7 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 1 Oct 2024 11:07:43 +0100 Subject: [PATCH 7/8] Apply suggestions from code review --- crates/biome_js_analyze/src/lint/nursery/no_head_element.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 8b7f4fabee0e..8384ae9f9368 100644 --- a/crates/biome_js_analyze/src/lint/nursery/no_head_element.rs +++ b/crates/biome_js_analyze/src/lint/nursery/no_head_element.rs @@ -14,7 +14,7 @@ declare_lint_rule! { /// the `` tag for optimal server-side rendering, client-side navigation, and /// automatic deduplication of tags such as `` and ``. /// - /// This rule only checks outside of the `app/` directory, as it's typically + /// 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 @@ -83,7 +83,7 @@ impl Rule for NoHeadElement { range, markup! { "Don't use "<Emphasis>"<head>"</Emphasis>" element." }, ).note(markup! { - "Using "<Emphasis>"<head>"</Emphasis>" element can cause unexpected behavior in a Next.js application. Use "<Emphasis>"<Head />"</Emphasis>" from "<Emphasis>"next/head"</Emphasis>" instead." + "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." })); } } From 83b0490849d963022cf2e6912a20263d19063653 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa <my.burning@gmail.com> Date: Tue, 1 Oct 2024 11:33:00 +0100 Subject: [PATCH 8/8] snapshot --- .../tests/specs/nursery/noHeadElement/pages/invalid.jsx.snap | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 index e0cf5a73c7ba..96d3af536e97 100644 --- 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 @@ -1,6 +1,5 @@ --- source: crates/biome_js_analyze/tests/spec_tests.rs -assertion_line: 86 expression: invalid.jsx --- # Input @@ -22,7 +21,7 @@ invalid.jsx:1:1 lint/nursery/noHeadElement ━━━━━━━━━━━━ 2 │ <title>Invalid 3 │ - i Using element can cause unexpected behavior in a Next.js application. Use from next/head instead. + i Using the element can cause unexpected behavior in a Next.js application. Use from next/head instead. ```