From 0d8cc8fd7e154aebee76579418490c72b2c76934 Mon Sep 17 00:00:00 2001 From: Kaio Duarte Date: Mon, 7 Oct 2024 06:22:51 +0100 Subject: [PATCH] feat(lint): add `noImgElement` rule (#4155) --- .../migrate/eslint_any_rule_to_biome.rs | 8 + .../src/analyzer/linter/rules.rs | 137 ++++++++++-------- .../src/categories.rs | 1 + crates/biome_js_analyze/src/lint/nursery.rs | 2 + .../src/lint/nursery/no_img_element.rs | 113 +++++++++++++++ crates/biome_js_analyze/src/options.rs | 2 + .../specs/nursery/noImgElement/invalid.jsx | 7 + .../nursery/noImgElement/invalid.jsx.snap | 49 +++++++ .../specs/nursery/noImgElement/valid.jsx | 7 + .../specs/nursery/noImgElement/valid.jsx.snap | 16 ++ .../@biomejs/backend-jsonrpc/src/workspace.ts | 5 + .../@biomejs/biome/configuration_schema.json | 7 + 12 files changed, 295 insertions(+), 59 deletions(-) create mode 100644 crates/biome_js_analyze/src/lint/nursery/no_img_element.rs create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noImgElement/invalid.jsx create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noImgElement/invalid.jsx.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noImgElement/valid.jsx create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noImgElement/valid.jsx.snap 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 9d2b05410bc3..a9fd1797e7bb 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 @@ -22,6 +22,14 @@ pub(crate) fn migrate_eslint_any_rule( let rule = group.no_head_element.get_or_insert(Default::default()); rule.set_level(rule_severity.into()); } + "@next/no-img-element" => { + if !options.include_nursery { + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group.no_img_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 44d8d3fcadf2..dc9e88e26bd8 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -3304,6 +3304,9 @@ pub struct Nursery { #[doc = "Prevent usage of \\ element in a Next.js project."] #[serde(skip_serializing_if = "Option::is_none")] pub no_head_element: Option>, + #[doc = "Prevent usage of \\ element in a Next.js project."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_img_element: Option>, #[doc = "Disallows the use of irregular whitespace characters."] #[serde(skip_serializing_if = "Option::is_none")] pub no_irregular_whitespace: @@ -3432,6 +3435,7 @@ impl Nursery { "noEnum", "noExportedImports", "noHeadElement", + "noImgElement", "noIrregularWhitespace", "noMissingVarFunction", "noNestedTernary", @@ -3479,14 +3483,14 @@ 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[10]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21]), 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[23]), + 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[30]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), @@ -3525,6 +3529,7 @@ impl Nursery { 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]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -3586,141 +3591,146 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } } - 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[9])); } } - 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[10])); } } - 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[11])); } } - 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[12])); } } - 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[13])); } } - 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[14])); } } - 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[15])); } } - 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[16])); } } - 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[17])); } } - 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[18])); } } - 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[19])); } } - 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[20])); } } - 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[21])); } } - 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[22])); } } - 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[23])); } } - 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[24])); } } - 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[25])); } } - 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[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_enabled() { 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_enabled() { 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_enabled() { 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_enabled() { 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_enabled() { 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_enabled() { 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_enabled() { 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_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } } - 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[35])); } } + 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[36])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> FxHashSet> { @@ -3770,141 +3780,146 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } } - 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[9])); } } - 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[10])); } } - 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[11])); } } - 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[12])); } } - 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[13])); } } - 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[14])); } } - 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[15])); } } - 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[16])); } } - 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[17])); } } - 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[18])); } } - 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[19])); } } - 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[20])); } } - 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[21])); } } - 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[22])); } } - 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[23])); } } - 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[24])); } } - 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[25])); } } - 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[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_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[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[36])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -3977,6 +3992,10 @@ impl Nursery { .no_head_element .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "noImgElement" => self + .no_img_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 adf59f54d2bf..b01e9bbca275 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -145,6 +145,7 @@ define_categories! { "lint/nursery/noDynamicNamespaceImportAccess": "https://biomejs.dev/linter/rules/no-dynamic-namespace-import-access", "lint/nursery/noEnum": "https://biomejs.dev/linter/rules/no-enum", "lint/nursery/noExportedImports": "https://biomejs.dev/linter/rules/no-exported-imports", + "lint/nursery/noImgElement": "https://biomejs.dev/linter/rules/no-img-element", "lint/nursery/noImportantInKeyframe": "https://biomejs.dev/linter/rules/no-important-in-keyframe", "lint/nursery/noInvalidDirectionInLinearGradient": "https://biomejs.dev/linter/rules/no-invalid-direction-in-linear-gradient", "lint/nursery/noInvalidGridAreas": "https://biomejs.dev/linter/rules/use-consistent-grid-areas", diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index 262787628b06..2e22183f6a81 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -8,6 +8,7 @@ pub mod no_dynamic_namespace_import_access; pub mod no_enum; pub mod no_exported_imports; pub mod no_head_element; +pub mod no_img_element; pub mod no_irregular_whitespace; pub mod no_nested_ternary; pub mod no_octal_escape; @@ -41,6 +42,7 @@ declare_lint_group! { self :: no_enum :: NoEnum , self :: no_exported_imports :: NoExportedImports , self :: no_head_element :: NoHeadElement , + self :: no_img_element :: NoImgElement , 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_img_element.rs b/crates/biome_js_analyze/src/lint/nursery/no_img_element.rs new file mode 100644 index 000000000000..6164b9409f98 --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/no_img_element.rs @@ -0,0 +1,113 @@ +use biome_analyze::RuleSourceKind; +use biome_analyze::{ + context::RuleContext, declare_lint_rule, Ast, Rule, RuleDiagnostic, RuleSource, +}; +use biome_console::markup; +use biome_js_syntax::jsx_ext::AnyJsxElement; +use biome_js_syntax::{JsxChildList, JsxElement}; +use biome_rowan::{AstNode, AstNodeList}; + +declare_lint_rule! { + /// Prevent usage of `` element in a Next.js project. + /// + /// Using the `` element can result in slower Largest Contentful Paint (LCP) + /// and higher bandwidth usage, as it lacks the optimizations provided by the `` + /// component from `next/image`. Next.js's `` automatically optimizes images + /// by serving responsive sizes and using modern formats, improving performance and reducing bandwidth. + /// + /// If your project is self-hosted, ensure that you have sufficient storage and have + /// installed the `sharp` package to support optimized images. When deploying to managed + /// hosting providers, be aware of potential additional costs or usage. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```jsx,expect_diagnostic + /// Foo + /// ``` + /// + /// ```jsx,expect_diagnostic + ///
+ /// Foo + ///
+ /// ``` + /// + /// ### Valid + /// + /// ```jsx + /// + /// ``` + /// + /// ```jsx + /// + /// ``` + /// + /// ```jsx + /// + /// + /// + /// + /// + /// ``` + /// + pub NoImgElement { + version: "next", + name: "noImgElement", + language: "jsx", + sources: &[RuleSource::EslintNext("no-img-element")], + source_kind: RuleSourceKind::SameLogic, + recommended: false, + } +} + +impl Rule for NoImgElement { + type Query = Ast; + type State = (); + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + + if node.name().ok()?.name_value_token()?.text_trimmed() != "img" + || node.attributes().is_empty() + { + return None; + } + + if let AnyJsxElement::JsxSelfClosingElement(jsx) = &node { + let Some(parent) = jsx.parent::() else { + return Some(()); + }; + let Some(parent) = parent.parent::() else { + return Some(()); + }; + let Some(opening_element) = parent.opening_element().ok() else { + return Some(()); + }; + let name = opening_element.name().ok()?.name_value_token()?; + + if name.text_trimmed() == "picture" { + return None; + } + } + + Some(()) + } + + fn diagnostic(ctx: &RuleContext, _: &Self::State) -> Option { + return Some( + RuleDiagnostic::new( + rule_category!(), + ctx.query().range(), + markup! { + "Don't use """" element." + }, + ) + .note(markup! { + "Using the """" can lead to slower LCP and higher bandwidth. Consider using """" from ""next/image"" to automatically optimize images." + }) + ); + } +} diff --git a/crates/biome_js_analyze/src/options.rs b/crates/biome_js_analyze/src/options.rs index 0b349a72b4ff..b64432f70019 100644 --- a/crates/biome_js_analyze/src/options.rs +++ b/crates/biome_js_analyze/src/options.rs @@ -115,6 +115,8 @@ pub type NoHeadElement = ::Options; pub type NoHeaderScope = ::Options; +pub type NoImgElement = + ::Options; pub type NoImplicitAnyLet = ::Options; pub type NoImplicitBoolean = diff --git a/crates/biome_js_analyze/tests/specs/nursery/noImgElement/invalid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noImgElement/invalid.jsx new file mode 100644 index 000000000000..c4ed80a56d64 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noImgElement/invalid.jsx @@ -0,0 +1,7 @@ +<> + Foo + +
+ Foo +
+ diff --git a/crates/biome_js_analyze/tests/specs/nursery/noImgElement/invalid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noImgElement/invalid.jsx.snap new file mode 100644 index 000000000000..3140dd7edddc --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noImgElement/invalid.jsx.snap @@ -0,0 +1,49 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: invalid.jsx +--- +# Input +```jsx +<> + Foo + +
+ Foo +
+ + +``` + +# Diagnostics +``` +invalid.jsx:2:2 lint/nursery/noImgElement ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't use element. + + 1 │ <> + > 2 │ Foo + │ ^^^^^^^^^^^^^^^^^ + 3 │ + 4 │
+ + i Using the can lead to slower LCP and higher bandwidth. Consider using from next/image to automatically optimize images. + + +``` + +``` +invalid.jsx:5:3 lint/nursery/noImgElement ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't use element. + + 4 │
+ > 5 │ Foo + │ ^^^^^^^^^^^^^^^^^ + 6 │
+ 7 │ + + i Using the can lead to slower LCP and higher bandwidth. Consider using from next/image to automatically optimize images. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noImgElement/valid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noImgElement/valid.jsx new file mode 100644 index 000000000000..f47d1e4ada55 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noImgElement/valid.jsx @@ -0,0 +1,7 @@ +<> + + + + Foo + + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noImgElement/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noImgElement/valid.jsx.snap new file mode 100644 index 000000000000..2fb934209539 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noImgElement/valid.jsx.snap @@ -0,0 +1,16 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: valid.jsx +--- +# Input +```jsx +<> + + + + Foo + + + +``` diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 98ac3194ef66..5f7d48fdcc6c 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1254,6 +1254,10 @@ export interface Nursery { * Prevent usage of \ element in a Next.js project. */ noHeadElement?: RuleConfiguration_for_Null; + /** + * Prevent usage of \ element in a Next.js project. + */ + noImgElement?: RuleConfiguration_for_Null; /** * Disallows the use of irregular whitespace characters. */ @@ -2858,6 +2862,7 @@ export type Category = | "lint/nursery/noDynamicNamespaceImportAccess" | "lint/nursery/noEnum" | "lint/nursery/noExportedImports" + | "lint/nursery/noImgElement" | "lint/nursery/noImportantInKeyframe" | "lint/nursery/noInvalidDirectionInLinearGradient" | "lint/nursery/noInvalidGridAreas" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 0fc294995c75..5d47964d280e 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -2130,6 +2130,13 @@ { "type": "null" } ] }, + "noImgElement": { + "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": [