From 2a05cd47fd401e7b660f480673f55924236289ca Mon Sep 17 00:00:00 2001 From: Kaio Duarte Date: Thu, 17 Oct 2024 13:57:23 +0100 Subject: [PATCH] feat(lint): add `useGoogleFontDisplay` rule (#4264) Co-authored-by: togami <62130798+togami2864@users.noreply.github.com> --- CHANGELOG.md | 3 +- .../migrate/eslint_any_rule_to_biome.rs | 10 + .../src/analyzer/linter/rules.rs | 46 +++- .../src/categories.rs | 2 + crates/biome_js_analyze/src/lint/nursery.rs | 2 + .../lint/nursery/use_google_font_display.rs | 125 ++++++++++ crates/biome_js_analyze/src/options.rs | 2 + .../nursery/useGoogleFontDisplay/invalid.jsx | 16 ++ .../useGoogleFontDisplay/invalid.jsx.snap | 227 ++++++++++++++++++ .../nursery/useGoogleFontDisplay/valid.jsx | 18 ++ .../useGoogleFontDisplay/valid.jsx.snap | 26 ++ .../@biomejs/backend-jsonrpc/src/workspace.ts | 6 + .../@biomejs/biome/configuration_schema.json | 7 + 13 files changed, 476 insertions(+), 14 deletions(-) create mode 100644 crates/biome_js_analyze/src/lint/nursery/use_google_font_display.rs create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useGoogleFontDisplay/invalid.jsx create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useGoogleFontDisplay/invalid.jsx.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useGoogleFontDisplay/valid.jsx create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useGoogleFontDisplay/valid.jsx.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index d229393bcb84..2f1de7112031 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b - 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 - Add [noUselessStringRaw](https://github.com/biomejs/biome/pull/4263). Contributed by @fireairforce +- Add [useGoogleFontDisplay](https://biomejs.dev/linter/rules/use-google-font-display/). Contributed by @kaioduarte #### Bug Fixes @@ -186,7 +187,7 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b #### New features - Add support for parsing the defer attribute in import statements ([#4215](https://github.com/biomejs/biome/issues/4215)). - + ```js import defer * as myModule from "my-module"; ``` 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 b5c5dd51781f..0a8e2f2a7ec0 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/google-font-display" => { + if !options.include_nursery { + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group + .use_google_font_display + .get_or_insert(Default::default()); + rule.set_level(rule_severity.into()); + } "@next/no-document-import-in-page" => { 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 76de7ff26e0a..2bfe89e761e4 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -3417,6 +3417,10 @@ pub struct Nursery { #[doc = "Require explicit return types on functions and class methods."] #[serde(skip_serializing_if = "Option::is_none")] pub use_explicit_type: Option>, + #[doc = "Enforces the use of a recommended display strategy with Google Fonts."] + #[serde(skip_serializing_if = "Option::is_none")] + pub use_google_font_display: + Option>, #[doc = "Require for-in loops to include an if statement."] #[serde(skip_serializing_if = "Option::is_none")] pub use_guard_for_in: Option>, @@ -3497,6 +3501,7 @@ impl Nursery { "useConsistentMemberAccessibility", "useDeprecatedReason", "useExplicitType", + "useGoogleFontDisplay", "useGuardForIn", "useImportRestrictions", "useSortedClasses", @@ -3534,7 +3539,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), @@ -3583,6 +3588,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[46]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -3799,36 +3805,41 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); } } - if let Some(rule) = self.use_guard_for_in.as_ref() { + if let Some(rule) = self.use_google_font_display.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40])); } } - 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[41])); } } - 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[42])); } } - 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[43])); } } - 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[44])); } } - 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[45])); } } + 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[46])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> FxHashSet> { @@ -4033,36 +4044,41 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); } } - if let Some(rule) = self.use_guard_for_in.as_ref() { + if let Some(rule) = self.use_google_font_display.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40])); } } - 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[41])); } } - 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[42])); } } - 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[43])); } } - 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[44])); } } - 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[45])); } } + 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[46])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -4259,6 +4275,10 @@ impl Nursery { .use_explicit_type .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "useGoogleFontDisplay" => self + .use_google_font_display + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "useGuardForIn" => self .use_guard_for_in .as_ref() diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 7a5f6e5341dc..44caa650241c 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -193,7 +193,9 @@ define_categories! { "lint/nursery/useConsistentCurlyBraces": "https://biomejs.dev/linter/rules/use-consistent-curly-braces", "lint/nursery/useConsistentMemberAccessibility": "https://biomejs.dev/linter/rules/use-consistent-member-accessibility", "lint/nursery/useDeprecatedReason": "https://biomejs.dev/linter/rules/use-deprecated-reason", + "lint/nursery/useExplicitFunctionReturnType": "https://biomejs.dev/linter/rules/use-explicit-function-return-type", "lint/nursery/useExplicitType": "https://biomejs.dev/linter/rules/use-explicit-function-return-type", + "lint/nursery/useGoogleFontDisplay": "https://biomejs.dev/linter/rules/use-google-font-display", "lint/nursery/useGuardForIn": "https://biomejs.dev/linter/rules/use-guard-for-in", "lint/nursery/useImportRestrictions": "https://biomejs.dev/linter/rules/use-import-restrictions", "lint/nursery/useJsxCurlyBraceConvention": "https://biomejs.dev/linter/rules/use-jsx-curly-brace-convention", diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index 4660799ace74..d75078d2e484 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -32,6 +32,7 @@ pub mod use_component_export_only_modules; pub mod use_consistent_curly_braces; pub mod use_consistent_member_accessibility; pub mod use_explicit_type; +pub mod use_google_font_display; pub mod use_guard_for_in; pub mod use_import_restrictions; pub mod use_sorted_classes; @@ -73,6 +74,7 @@ declare_lint_group! { self :: use_consistent_curly_braces :: UseConsistentCurlyBraces , self :: use_consistent_member_accessibility :: UseConsistentMemberAccessibility , self :: use_explicit_type :: UseExplicitType , + self :: use_google_font_display :: UseGoogleFontDisplay , self :: use_guard_for_in :: UseGuardForIn , self :: use_import_restrictions :: UseImportRestrictions , self :: use_sorted_classes :: UseSortedClasses , diff --git a/crates/biome_js_analyze/src/lint/nursery/use_google_font_display.rs b/crates/biome_js_analyze/src/lint/nursery/use_google_font_display.rs new file mode 100644 index 000000000000..d790a3237d88 --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/use_google_font_display.rs @@ -0,0 +1,125 @@ +use biome_analyze::{ + context::RuleContext, declare_lint_rule, Ast, Rule, RuleDiagnostic, RuleSource, RuleSourceKind, +}; +use biome_console::markup; +use biome_js_syntax::jsx_ext::AnyJsxElement; +use biome_rowan::TextRange; + +declare_lint_rule! { + /// Enforces the use of a recommended `display` strategy with Google Fonts. + /// + /// The `display` property controls how a font is displayed while it is loading. When using Google Fonts, + /// it's important to specify an appropriate value for this property to ensure good user experience and prevent layout shifts. + /// + /// This rule flags the absence of the `display` parameter, or the usage of less optimal values such as `auto`, `block`, or `fallback`. + /// Using `&display=optional` is generally recommended as it minimizes the risk of invisible text or layout shifts. + /// In cases where swapping to the custom font after it has loaded is important, consider using `&display=swap`. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```jsx,expect_diagnostic + /// + /// ``` + /// + /// ```jsx,expect_diagnostic + /// + /// ``` + /// + /// ```jsx,expect_diagnostic + /// + /// ``` + /// + /// ```jsx,expect_diagnostic + /// + /// ``` + /// + /// ### Valid + /// + /// ```jsx + /// + /// ``` + /// + /// ```jsx + /// + /// ``` + /// + /// ```jsx + /// + /// ``` + pub UseGoogleFontDisplay { + version: "next", + name: "useGoogleFontDisplay", + language: "jsx", + sources: &[RuleSource::EslintNext("google-font-display")], + source_kind: RuleSourceKind::SameLogic, + recommended: false, + } +} + +const FORBIDDEN_VALUES: [&str; 3] = ["auto", "block", "fallback"]; + +pub enum FontDisplayIssue { + MissingDisplayParam, + ForbiddenValue, +} + +impl Rule for UseGoogleFontDisplay { + type Query = Ast; + type State = (FontDisplayIssue, TextRange); + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let element = ctx.query(); + + if element.name().ok()?.name_value_token()?.text_trimmed() != "link" { + return None; + } + + let href = element.find_attribute_by_name("href")?; + let initializer = href.initializer()?.value().ok()?.as_static_value()?; + let href_text = initializer.as_string_constant()?; + + if !href_text.starts_with("https://fonts.googleapis.com/css") { + return None; + } + + let display_param = href_text + .split('?') + .last()? + .split('&') + .find(|p| p.starts_with("display=")); + let range = initializer.range(); + + if let Some(display_param) = display_param { + for forbidden_value in FORBIDDEN_VALUES { + if display_param.ends_with(forbidden_value) { + return Some((FontDisplayIssue::ForbiddenValue, range)); + } + } + } else { + return Some((FontDisplayIssue::MissingDisplayParam, range)); + } + + None + } + + fn diagnostic(_: &RuleContext, (issue, range): &Self::State) -> Option { + let title = match issue { + FontDisplayIssue::MissingDisplayParam => markup! { + "The Google Font link is missing the ""display"" parameter." + }, + FontDisplayIssue::ForbiddenValue => markup! { + "The Google Font link has a non-recommended ""display"" value." + }, + }; + + Some(RuleDiagnostic::new(rule_category!(), range, title).note( + markup! { + "Use ""&display=optional"" to prevent invisible text and layout shifts. If font swapping is important, use ""&display=swap""." + } + )) + } +} diff --git a/crates/biome_js_analyze/src/options.rs b/crates/biome_js_analyze/src/options.rs index fc0bd18366de..eba192f87e62 100644 --- a/crates/biome_js_analyze/src/options.rs +++ b/crates/biome_js_analyze/src/options.rs @@ -333,6 +333,8 @@ pub type UseFragmentSyntax = ::Options; pub type UseGetterReturn = ::Options; +pub type UseGoogleFontDisplay = + ::Options; pub type UseGuardForIn = ::Options; pub type UseHeadingContent = diff --git a/crates/biome_js_analyze/tests/specs/nursery/useGoogleFontDisplay/invalid.jsx b/crates/biome_js_analyze/tests/specs/nursery/useGoogleFontDisplay/invalid.jsx new file mode 100644 index 000000000000..559485b08005 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useGoogleFontDisplay/invalid.jsx @@ -0,0 +1,16 @@ +<> + + + + + + + + + + + + + + + diff --git a/crates/biome_js_analyze/tests/specs/nursery/useGoogleFontDisplay/invalid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/useGoogleFontDisplay/invalid.jsx.snap new file mode 100644 index 000000000000..35775db11ad3 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useGoogleFontDisplay/invalid.jsx.snap @@ -0,0 +1,227 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: invalid.jsx +--- +# Input +```jsx +<> + + + + + + + + + + + + + + + + +``` + +# Diagnostics +``` +invalid.jsx:2:13 lint/nursery/useGoogleFontDisplay ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The Google Font link is missing the display parameter. + + 1 │ <> + > 2 │ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 3 │ + 4 │ + + i Use &display=optional to prevent invisible text and layout shifts. If font swapping is important, use &display=swap. + + +``` + +``` +invalid.jsx:3:13 lint/nursery/useGoogleFontDisplay ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The Google Font link has a non-recommended display value. + + 1 │ <> + 2 │ + > 3 │ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 4 │ + 5 │ + + i Use &display=optional to prevent invisible text and layout shifts. If font swapping is important, use &display=swap. + + +``` + +``` +invalid.jsx:4:13 lint/nursery/useGoogleFontDisplay ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The Google Font link has a non-recommended display value. + + 2 │ + 3 │ + > 4 │ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 5 │ + 6 │ + + i Use &display=optional to prevent invisible text and layout shifts. If font swapping is important, use &display=swap. + + +``` + +``` +invalid.jsx:5:13 lint/nursery/useGoogleFontDisplay ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The Google Font link has a non-recommended display value. + + 3 │ + 4 │ + > 5 │ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 6 │ + 7 │ + + i Use &display=optional to prevent invisible text and layout shifts. If font swapping is important, use &display=swap. + + +``` + +``` +invalid.jsx:7:14 lint/nursery/useGoogleFontDisplay ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The Google Font link is missing the display parameter. + + 5 │ + 6 │ + > 7 │ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 8 │ + 9 │ + + i Use &display=optional to prevent invisible text and layout shifts. If font swapping is important, use &display=swap. + + +``` + +``` +invalid.jsx:8:14 lint/nursery/useGoogleFontDisplay ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The Google Font link has a non-recommended display value. + + 7 │ + > 8 │ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 9 │ + 10 │ + + i Use &display=optional to prevent invisible text and layout shifts. If font swapping is important, use &display=swap. + + +``` + +``` +invalid.jsx:9:14 lint/nursery/useGoogleFontDisplay ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The Google Font link has a non-recommended display value. + + 7 │ + 8 │ + > 9 │ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 10 │ + 11 │ + + i Use &display=optional to prevent invisible text and layout shifts. If font swapping is important, use &display=swap. + + +``` + +``` +invalid.jsx:10:14 lint/nursery/useGoogleFontDisplay ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The Google Font link has a non-recommended display value. + + 8 │ + 9 │ + > 10 │ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 11 │ + 12 │ + + i Use &display=optional to prevent invisible text and layout shifts. If font swapping is important, use &display=swap. + + +``` + +``` +invalid.jsx:12:15 lint/nursery/useGoogleFontDisplay ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The Google Font link is missing the display parameter. + + 10 │ + 11 │ + > 12 │ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 13 │ + 14 │ + + i Use &display=optional to prevent invisible text and layout shifts. If font swapping is important, use &display=swap. + + +``` + +``` +invalid.jsx:13:15 lint/nursery/useGoogleFontDisplay ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The Google Font link has a non-recommended display value. + + 12 │ + > 13 │ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 14 │ + 15 │ + + i Use &display=optional to prevent invisible text and layout shifts. If font swapping is important, use &display=swap. + + +``` + +``` +invalid.jsx:14:15 lint/nursery/useGoogleFontDisplay ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The Google Font link has a non-recommended display value. + + 12 │ + 13 │ + > 14 │ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 15 │ + 16 │ + + i Use &display=optional to prevent invisible text and layout shifts. If font swapping is important, use &display=swap. + + +``` + +``` +invalid.jsx:15:15 lint/nursery/useGoogleFontDisplay ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The Google Font link has a non-recommended display value. + + 13 │ + 14 │ + > 15 │ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 16 │ + 17 │ + + i Use &display=optional to prevent invisible text and layout shifts. If font swapping is important, use &display=swap. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useGoogleFontDisplay/valid.jsx b/crates/biome_js_analyze/tests/specs/nursery/useGoogleFontDisplay/valid.jsx new file mode 100644 index 000000000000..02fa31fa32a9 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useGoogleFontDisplay/valid.jsx @@ -0,0 +1,18 @@ +<> + + + + + + + + \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/useGoogleFontDisplay/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/useGoogleFontDisplay/valid.jsx.snap new file mode 100644 index 000000000000..36c3f953aed7 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useGoogleFontDisplay/valid.jsx.snap @@ -0,0 +1,26 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: valid.jsx +--- +# Input +```jsx +<> + + + + + + + + +``` diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index a5030b84d2d0..b4688cfefe0a 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1382,6 +1382,10 @@ export interface Nursery { * Require explicit return types on functions and class methods. */ useExplicitType?: RuleConfiguration_for_Null; + /** + * Enforces the use of a recommended display strategy with Google Fonts. + */ + useGoogleFontDisplay?: RuleConfiguration_for_Null; /** * Require for-in loops to include an if statement. */ @@ -2965,7 +2969,9 @@ export type Category = | "lint/nursery/useConsistentCurlyBraces" | "lint/nursery/useConsistentMemberAccessibility" | "lint/nursery/useDeprecatedReason" + | "lint/nursery/useExplicitFunctionReturnType" | "lint/nursery/useExplicitType" + | "lint/nursery/useGoogleFontDisplay" | "lint/nursery/useGuardForIn" | "lint/nursery/useImportRestrictions" | "lint/nursery/useJsxCurlyBraceConvention" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 419e88edbaa9..a6865f922768 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -2373,6 +2373,13 @@ { "type": "null" } ] }, + "useGoogleFontDisplay": { + "description": "Enforces the use of a recommended display strategy with Google Fonts.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "useGuardForIn": { "description": "Require for-in loops to include an if statement.", "anyOf": [