From 2342984b9223ec0722ce183bd2c12e4ca6ef1da0 Mon Sep 17 00:00:00 2001 From: Kaio Duarte Date: Sun, 13 Oct 2024 20:00:46 +0100 Subject: [PATCH] feat(lint): add `noDocumentImportInPage` rule (#4265) Co-authored-by: unvalley <38400669+unvalley@users.noreply.github.com> --- CHANGELOG.md | 4 + .../migrate/eslint_any_rule_to_biome.rs | 10 + .../src/analyzer/linter/rules.rs | 188 ++++++++++-------- .../src/categories.rs | 1 + crates/biome_js_analyze/src/lint/nursery.rs | 2 + .../nursery/no_document_import_in_page.rs | 95 +++++++++ crates/biome_js_analyze/src/options.rs | 1 + .../noDocumentImportInPage/app/valid.jsx | 1 + .../noDocumentImportInPage/app/valid.jsx.snap | 9 + .../pages/_document.jsx | 1 + .../pages/_document.jsx.snap | 9 + .../noDocumentImportInPage/pages/invalid.jsx | 1 + .../pages/invalid.jsx.snap | 23 +++ .../nursery/noDocumentImportInPage/valid.jsx | 1 + .../noDocumentImportInPage/valid.jsx.snap | 9 + .../@biomejs/backend-jsonrpc/src/workspace.ts | 5 + .../@biomejs/biome/configuration_schema.json | 7 + 17 files changed, 283 insertions(+), 84 deletions(-) create mode 100644 crates/biome_js_analyze/src/lint/nursery/no_document_import_in_page.rs create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/app/valid.jsx create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/app/valid.jsx.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/pages/_document.jsx create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/pages/_document.jsx.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/pages/invalid.jsx create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/pages/invalid.jsx.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/valid.jsx create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/valid.jsx.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index a5c7d9c850c9..1373f21642ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,10 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b #### New features - Add [noDocumentCookie](https://biomejs.dev/linter/rules/no-document-cookie/). Contributed by @tunamaguro +- Add [noDocumentImportInPage](https://biomejs.dev/linter/rules/no-document-import-in-page/). Contributed by @kaioduarte +- Add [noHeadElement](https://biomejs.dev/linter/rules/no-head-element/). Contributed by @kaioduarte +- Add [noHeadImportInDocument](https://biomejs.dev/linter/rules/no-head-import-in-document/). Contributed by @kaioduarte +- Add [noImgElement](https://biomejs.dev/linter/rules/no-img-element/). Contributed by @kaioduarte - Add [guardForIn](https://biomejs.dev/linter/rules/guard-for-in/). Contributed by @fireairforce #### Bug Fixes 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 46f6938063ce..439041656c45 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,16 @@ 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-document-import-in-page" => { + if !options.include_nursery { + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group + .no_document_import_in_page + .get_or_insert(Default::default()); + rule.set_level(rule_severity.into()); + } "@next/no-head-element" => { if !options.include_nursery { return false; diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs index f90a75cf02e2..cce300963082 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -3281,6 +3281,10 @@ pub struct Nursery { #[doc = "Disallow direct assignments to document.cookie."] #[serde(skip_serializing_if = "Option::is_none")] pub no_document_cookie: Option>, + #[doc = "Prevents importing next/document outside of pages/_document.jsx in Next.js projects."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_document_import_in_page: + Option>, #[doc = "Disallow duplicate custom properties within declaration blocks."] #[serde(skip_serializing_if = "Option::is_none")] pub no_duplicate_custom_properties: @@ -3445,6 +3449,7 @@ impl Nursery { "noCommonJs", "noDescendingSpecificity", "noDocumentCookie", + "noDocumentImportInPage", "noDuplicateCustomProperties", "noDuplicateElseIf", "noDuplicatedFields", @@ -3502,18 +3507,18 @@ impl Nursery { ]; const RECOMMENDED_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), - 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[5]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), @@ -3558,6 +3563,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -3589,201 +3595,206 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2])); } } - if let Some(rule) = self.no_duplicate_custom_properties.as_ref() { + if let Some(rule) = self.no_document_import_in_page.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3])); } } - if let Some(rule) = self.no_duplicate_else_if.as_ref() { + if let Some(rule) = self.no_duplicate_custom_properties.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4])); } } - if let Some(rule) = self.no_duplicated_fields.as_ref() { + if let Some(rule) = self.no_duplicate_else_if.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5])); } } - if let Some(rule) = self.no_dynamic_namespace_import_access.as_ref() { + if let Some(rule) = self.no_duplicated_fields.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6])); } } - if let Some(rule) = self.no_enum.as_ref() { + if let Some(rule) = self.no_dynamic_namespace_import_access.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7])); } } - if let Some(rule) = self.no_exported_imports.as_ref() { + if let Some(rule) = self.no_enum.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } } - if let Some(rule) = self.no_head_element.as_ref() { + if let Some(rule) = self.no_exported_imports.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } } - if let Some(rule) = self.no_head_import_in_document.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[10])); } } - if let Some(rule) = self.no_img_element.as_ref() { + if let Some(rule) = self.no_head_import_in_document.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } } - if let Some(rule) = self.no_irregular_whitespace.as_ref() { + if let Some(rule) = self.no_img_element.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } } - 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[13])); } } - 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[14])); } } - 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[15])); } } - 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[16])); } } - 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[17])); } } - 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[18])); } } - 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[19])); } } - 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[20])); } } - 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[21])); } } - 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[22])); } } - 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[23])); } } - 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[24])); } } - if let Some(rule) = self.no_unknown_type_selector.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[25])); } } - if let Some(rule) = self.no_useless_escape_in_regex.as_ref() { + if let Some(rule) = self.no_unknown_type_selector.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26])); } } - 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[27])); } } - 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[28])); } } - 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[29])); } } - if let Some(rule) = self.use_at_index.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[30])); } } - if let Some(rule) = self.use_component_export_only_modules.as_ref() { + if let Some(rule) = self.use_at_index.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - 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[32])); } } - 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[33])); } } - 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[34])); } } - if let Some(rule) = self.use_explicit_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[35])); } } - if let Some(rule) = self.use_guard_for_in.as_ref() { + if let Some(rule) = self.use_explicit_type.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_guard_for_in.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37])); } } - 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[38])); } } - 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[39])); } } - 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[40])); } } - 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[41])); } } + 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[42])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> FxHashSet> { @@ -3803,201 +3814,206 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2])); } } - if let Some(rule) = self.no_duplicate_custom_properties.as_ref() { + if let Some(rule) = self.no_document_import_in_page.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3])); } } - if let Some(rule) = self.no_duplicate_else_if.as_ref() { + if let Some(rule) = self.no_duplicate_custom_properties.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4])); } } - if let Some(rule) = self.no_duplicated_fields.as_ref() { + if let Some(rule) = self.no_duplicate_else_if.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5])); } } - if let Some(rule) = self.no_dynamic_namespace_import_access.as_ref() { + if let Some(rule) = self.no_duplicated_fields.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6])); } } - if let Some(rule) = self.no_enum.as_ref() { + if let Some(rule) = self.no_dynamic_namespace_import_access.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7])); } } - if let Some(rule) = self.no_exported_imports.as_ref() { + if let Some(rule) = self.no_enum.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } } - if let Some(rule) = self.no_head_element.as_ref() { + if let Some(rule) = self.no_exported_imports.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } } - if let Some(rule) = self.no_head_import_in_document.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[10])); } } - if let Some(rule) = self.no_img_element.as_ref() { + if let Some(rule) = self.no_head_import_in_document.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } } - if let Some(rule) = self.no_irregular_whitespace.as_ref() { + if let Some(rule) = self.no_img_element.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } } - 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[13])); } } - 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[14])); } } - 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[15])); } } - 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[16])); } } - 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[17])); } } - 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[18])); } } - 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[19])); } } - 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[20])); } } - 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[21])); } } - 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[22])); } } - 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[23])); } } - 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[24])); } } - if let Some(rule) = self.no_unknown_type_selector.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[25])); } } - if let Some(rule) = self.no_useless_escape_in_regex.as_ref() { + if let Some(rule) = self.no_unknown_type_selector.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26])); } } - 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[27])); } } - 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[28])); } } - 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[29])); } } - if let Some(rule) = self.use_at_index.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[30])); } } - if let Some(rule) = self.use_component_export_only_modules.as_ref() { + if let Some(rule) = self.use_at_index.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - 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[32])); } } - 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[33])); } } - 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[34])); } } - if let Some(rule) = self.use_explicit_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[35])); } } - if let Some(rule) = self.use_guard_for_in.as_ref() { + if let Some(rule) = self.use_explicit_type.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_guard_for_in.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37])); } } - 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[38])); } } - 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[39])); } } - 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[40])); } } - 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[41])); } } + 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[42])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -4046,6 +4062,10 @@ impl Nursery { .no_document_cookie .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "noDocumentImportInPage" => self + .no_document_import_in_page + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "noDuplicateCustomProperties" => self .no_duplicate_custom_properties .as_ref() diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 8e78a861b235..1af118be0cd5 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -138,6 +138,7 @@ define_categories! { "lint/nursery/noConsole": "https://biomejs.dev/linter/rules/no-console", "lint/nursery/noDescendingSpecificity": "https://biomejs.dev/linter/rules/no-descending-specificity", "lint/nursery/noDocumentCookie": "https://biomejs.dev/linter/rules/no-document-cookie", + "lint/nursery/noDocumentImportInPage": "https://biomejs.dev/linter/rules/no-document-import-in-page", "lint/nursery/noDoneCallback": "https://biomejs.dev/linter/rules/no-done-callback", "lint/nursery/noDuplicateAtImportRules": "https://biomejs.dev/linter/rules/no-duplicate-at-import-rules", "lint/nursery/noDuplicateCustomProperties": "https://biomejs.dev/linter/rules/no-duplicate-custom-properties", diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index b427e251a876..2f4de9545914 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -4,6 +4,7 @@ use biome_analyze::declare_lint_group; pub mod no_common_js; pub mod no_document_cookie; +pub mod no_document_import_in_page; pub mod no_duplicate_else_if; pub mod no_dynamic_namespace_import_access; pub mod no_enum; @@ -42,6 +43,7 @@ declare_lint_group! { rules : [ self :: no_common_js :: NoCommonJs , self :: no_document_cookie :: NoDocumentCookie , + self :: no_document_import_in_page :: NoDocumentImportInPage , self :: no_duplicate_else_if :: NoDuplicateElseIf , self :: no_dynamic_namespace_import_access :: NoDynamicNamespaceImportAccess , self :: no_enum :: NoEnum , diff --git a/crates/biome_js_analyze/src/lint/nursery/no_document_import_in_page.rs b/crates/biome_js_analyze/src/lint/nursery/no_document_import_in_page.rs new file mode 100644 index 000000000000..08c02cc656fb --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/no_document_import_in_page.rs @@ -0,0 +1,95 @@ +use biome_analyze::{ + context::RuleContext, declare_lint_rule, Ast, Rule, RuleDiagnostic, RuleSource, RuleSourceKind, +}; +use biome_console::markup; +use biome_js_syntax::{JsFileSource, JsImport}; +use biome_rowan::AstNode; + +declare_lint_rule! { + /// Prevents importing `next/document` outside of `pages/_document.jsx` in Next.js projects. + /// + /// The `next/document` module is intended for customizing the document structure globally in Next.js. + /// Importing it outside of `pages/_document.js` can cause unexpected behavior and break certain features of the framework. + /// + /// ## Examples + /// + /// ### Valid + /// + /// ```jsx + /// import { Document, Html } from 'next/document' + /// + /// export default class MyDocument extends Document { + /// render() { + /// return ( + /// + /// {/* */} + /// + /// ) + /// } + /// } + /// ``` + /// + pub NoDocumentImportInPage { + version: "next", + name: "noDocumentImportInPage", + language: "jsx", + sources: &[RuleSource::EslintNext("no-document-import-in-page")], + source_kind: RuleSourceKind::SameLogic, + recommended: false, + } +} + +impl Rule for NoDocumentImportInPage { + type Query = Ast; + type State = (); + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + if !ctx.source_type::().is_jsx() { + return None; + } + + let import = ctx.query(); + let import_source = import.import_clause().ok()?.source().ok()?; + let module_name = import_source.inner_string_text().ok()?; + + if module_name != "next/document" { + return None; + } + + let path = ctx.file_path(); + + if !path + .ancestors() + .filter_map(|a| a.file_name()) + .any(|f| f == "pages") + { + return None; + } + + let file_name = path.file_stem()?.to_str()?; + let parent_name = path.parent()?.file_stem()?.to_str()?; + + if parent_name == "_document" || file_name == "_document" { + return None; + } + + Some(()) + } + + fn diagnostic(ctx: &RuleContext, _: &Self::State) -> Option { + return Some( + RuleDiagnostic::new( + rule_category!(), + ctx.query().range(), + markup! { + "Don't use ""next/document"" outside of pages/_document.jsx to avoid unexpected behaviors." + }, + ) + .note(markup! { + "Only import ""next/document"" within ""pages/_document.jsx"" to customize the global document structure." + }) + ); + } +} diff --git a/crates/biome_js_analyze/src/options.rs b/crates/biome_js_analyze/src/options.rs index ca357f3e2226..5595abfb35c9 100644 --- a/crates/biome_js_analyze/src/options.rs +++ b/crates/biome_js_analyze/src/options.rs @@ -60,6 +60,7 @@ pub type NoDistractingElements = ::Options; pub type NoDocumentCookie = ::Options; +pub type NoDocumentImportInPage = < lint :: nursery :: no_document_import_in_page :: NoDocumentImportInPage as biome_analyze :: Rule > :: Options ; pub type NoDoneCallback = ::Options; pub type NoDoubleEquals = diff --git a/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/app/valid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/app/valid.jsx new file mode 100644 index 000000000000..6784e03b038d --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/app/valid.jsx @@ -0,0 +1 @@ +import Document from "next/document"; \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/app/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/app/valid.jsx.snap new file mode 100644 index 000000000000..0d8695c11e2d --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/app/valid.jsx.snap @@ -0,0 +1,9 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: valid.jsx +--- +# Input +```jsx +import Document from "next/document"; +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/pages/_document.jsx b/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/pages/_document.jsx new file mode 100644 index 000000000000..6784e03b038d --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/pages/_document.jsx @@ -0,0 +1 @@ +import Document from "next/document"; \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/pages/_document.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/pages/_document.jsx.snap new file mode 100644 index 000000000000..9c125c8034f2 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/pages/_document.jsx.snap @@ -0,0 +1,9 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: _document.jsx +--- +# Input +```jsx +import Document from "next/document"; +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/pages/invalid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/pages/invalid.jsx new file mode 100644 index 000000000000..6784e03b038d --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/pages/invalid.jsx @@ -0,0 +1 @@ +import Document from "next/document"; \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/pages/invalid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/pages/invalid.jsx.snap new file mode 100644 index 000000000000..595fc6ccc9d9 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/pages/invalid.jsx.snap @@ -0,0 +1,23 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: invalid.jsx +--- +# Input +```jsx +import Document from "next/document"; +``` + +# Diagnostics +``` +invalid.jsx:1:1 lint/nursery/noDocumentImportInPage ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't use next/document outside of pages/_document.jsx to avoid unexpected behaviors. + + > 1 │ import Document from "next/document"; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Only import next/document within pages/_document.jsx to customize the global document structure. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/valid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/valid.jsx new file mode 100644 index 000000000000..6784e03b038d --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/valid.jsx @@ -0,0 +1 @@ +import Document from "next/document"; \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/valid.jsx.snap new file mode 100644 index 000000000000..0d8695c11e2d --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noDocumentImportInPage/valid.jsx.snap @@ -0,0 +1,9 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: valid.jsx +--- +# Input +```jsx +import Document from "next/document"; +``` diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index bcd9c6160611..2f51772896a2 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1230,6 +1230,10 @@ export interface Nursery { * Disallow direct assignments to document.cookie. */ noDocumentCookie?: RuleConfiguration_for_Null; + /** + * Prevents importing next/document outside of pages/_document.jsx in Next.js projects. + */ + noDocumentImportInPage?: RuleConfiguration_for_Null; /** * Disallow duplicate custom properties within declaration blocks. */ @@ -2894,6 +2898,7 @@ export type Category = | "lint/nursery/noConsole" | "lint/nursery/noDescendingSpecificity" | "lint/nursery/noDocumentCookie" + | "lint/nursery/noDocumentImportInPage" | "lint/nursery/noDoneCallback" | "lint/nursery/noDuplicateAtImportRules" | "lint/nursery/noDuplicateCustomProperties" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 9402fa335c0f..f470c1cbe1e3 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -2106,6 +2106,13 @@ { "type": "null" } ] }, + "noDocumentImportInPage": { + "description": "Prevents importing next/document outside of pages/_document.jsx in Next.js projects.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noDuplicateCustomProperties": { "description": "Disallow duplicate custom properties within declaration blocks.", "anyOf": [