diff --git a/.changeset/quiet-hounds-shout.md b/.changeset/quiet-hounds-shout.md new file mode 100644 index 000000000000..e5a47f1b31c6 --- /dev/null +++ b/.changeset/quiet-hounds-shout.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +Added the nursery rule [`noHexColors`](https://biomejs.dev/linter/rules/no-hex-colors/), which flags the use of hexadecimal color codes in CSS and suggests using named colors or RGB/RGBA/HSL/HSLA formats instead. diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs index 908d7d8f6ab1..97e6eae3efbe 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -201,6 +201,7 @@ pub enum RuleName { NoHeadElement, NoHeadImportInDocument, NoHeaderScope, + NoHexColors, NoImgElement, NoImplicitAnyLet, NoImplicitBoolean, @@ -634,6 +635,7 @@ impl RuleName { Self::NoHeadElement => "noHeadElement", Self::NoHeadImportInDocument => "noHeadImportInDocument", Self::NoHeaderScope => "noHeaderScope", + Self::NoHexColors => "noHexColors", Self::NoImgElement => "noImgElement", Self::NoImplicitAnyLet => "noImplicitAnyLet", Self::NoImplicitBoolean => "noImplicitBoolean", @@ -1071,6 +1073,7 @@ impl RuleName { Self::NoHeadElement => RuleGroup::Style, Self::NoHeadImportInDocument => RuleGroup::Suspicious, Self::NoHeaderScope => RuleGroup::A11y, + Self::NoHexColors => RuleGroup::Nursery, Self::NoImgElement => RuleGroup::Performance, Self::NoImplicitAnyLet => RuleGroup::Suspicious, Self::NoImplicitBoolean => RuleGroup::Style, @@ -1509,6 +1512,7 @@ impl std::str::FromStr for RuleName { "noHeadElement" => Ok(Self::NoHeadElement), "noHeadImportInDocument" => Ok(Self::NoHeadImportInDocument), "noHeaderScope" => Ok(Self::NoHeaderScope), + "noHexColors" => Ok(Self::NoHexColors), "noImgElement" => Ok(Self::NoImgElement), "noImplicitAnyLet" => Ok(Self::NoImplicitAnyLet), "noImplicitBoolean" => Ok(Self::NoImplicitBoolean), diff --git a/crates/biome_css_analyze/src/lint/nursery.rs b/crates/biome_css_analyze/src/lint/nursery.rs index d0411d86f77d..2812e8aeb012 100644 --- a/crates/biome_css_analyze/src/lint/nursery.rs +++ b/crates/biome_css_analyze/src/lint/nursery.rs @@ -5,4 +5,5 @@ use biome_analyze::declare_lint_group; pub mod no_empty_source; pub mod no_excessive_lines_per_file; -declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_empty_source :: NoEmptySource , self :: no_excessive_lines_per_file :: NoExcessiveLinesPerFile ,] } } +pub mod no_hex_colors; +declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_empty_source :: NoEmptySource , self :: no_excessive_lines_per_file :: NoExcessiveLinesPerFile , self :: no_hex_colors :: NoHexColors ,] } } diff --git a/crates/biome_css_analyze/src/lint/nursery/no_hex_colors.rs b/crates/biome_css_analyze/src/lint/nursery/no_hex_colors.rs new file mode 100644 index 000000000000..475108f47d5d --- /dev/null +++ b/crates/biome_css_analyze/src/lint/nursery/no_hex_colors.rs @@ -0,0 +1,85 @@ +use biome_analyze::{ + Ast, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule, +}; +use biome_console::markup; +use biome_css_syntax::CssColor; +use biome_rowan::AstNode; +use biome_rule_options::no_hex_colors::NoHexColorsOptions; + +declare_lint_rule! { + /// Disallow hex colors. + /// + /// While hex colors are widely supported and compact, they can be less readable + /// and have limitations in terms of color representation compared to color models + /// like HSL or OKLCH. This rule encourages the use of more expressive color formats. + /// + /// This rule is inspired by the Stylelint rule + /// [`color-no-hex`](https://stylelint.io/user-guide/rules/color-no-hex/). + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```css,expect_diagnostic + /// a { color: #000; } + /// ``` + /// + /// ```css,expect_diagnostic + /// a { color: #fff1aa; } + /// ``` + /// + /// ```css,expect_diagnostic + /// a { color: #123456aa; } + /// ``` + /// + /// ### Valid + /// + /// ```css + /// a { color: black; } + /// ``` + /// + /// ```css + /// a { color: rgb(0, 0, 0); } + /// ``` + /// + /// ### References + /// + /// - [MDN Web Docs on CSS color values](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value) + /// + pub NoHexColors { + version: "next", + name: "noHexColors", + language: "css", + sources: &[RuleSource::Stylelint("color-no-hex").same()], + recommended: false, + } +} + +impl Rule for NoHexColors { + type Query = Ast; + type State = (); + type Signals = Option; + type Options = NoHexColorsOptions; + + fn run(_ctx: &RuleContext) -> Self::Signals { + Some(()) + } + + fn diagnostic(ctx: &RuleContext, _state: &Self::State) -> Option { + Some( + RuleDiagnostic::new( + rule_category!(), + ctx.query().range(), + markup! { + "Unexpected hex color." + }, + ) + .note(markup! { + "Hex colors are less readable and have limitations compared to other color models." + }) + .note(markup! { + "Consider using a named color or a color function like rgb(), hsl() or oklch(). See ""MDN Web Docs on CSS color values"" for more information." + }), + ) + } +} diff --git a/crates/biome_css_analyze/tests/specs/nursery/noHexColors/invalid.css b/crates/biome_css_analyze/tests/specs/nursery/noHexColors/invalid.css new file mode 100644 index 000000000000..8e288da12325 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noHexColors/invalid.css @@ -0,0 +1,5 @@ +/* should generate diagnostics */ +a { color: #000; } +a { color: #fff1aa; } +a { color: #123456aa; } +a { color: #0000000000000000; } diff --git a/crates/biome_css_analyze/tests/specs/nursery/noHexColors/invalid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noHexColors/invalid.css.snap new file mode 100644 index 000000000000..2026b2caa823 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noHexColors/invalid.css.snap @@ -0,0 +1,98 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: invalid.css +--- +# Input +```css +/* should generate diagnostics */ +a { color: #000; } +a { color: #fff1aa; } +a { color: #123456aa; } +a { color: #0000000000000000; } + +``` + +_Note: The parser emitted 1 diagnostics which are not shown here._ + +# Diagnostics +``` +invalid.css:2:12 lint/nursery/noHexColors ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Unexpected hex color. + + 1 │ /* should generate diagnostics */ + > 2 │ a { color: #000; } + │ ^^^^ + 3 │ a { color: #fff1aa; } + 4 │ a { color: #123456aa; } + + i Hex colors are less readable and have limitations compared to other color models. + + i Consider using a named color or a color function like rgb(), hsl() or oklch(). See MDN Web Docs on CSS color values for more information. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` + +``` +invalid.css:3:12 lint/nursery/noHexColors ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Unexpected hex color. + + 1 │ /* should generate diagnostics */ + 2 │ a { color: #000; } + > 3 │ a { color: #fff1aa; } + │ ^^^^^^^ + 4 │ a { color: #123456aa; } + 5 │ a { color: #0000000000000000; } + + i Hex colors are less readable and have limitations compared to other color models. + + i Consider using a named color or a color function like rgb(), hsl() or oklch(). See MDN Web Docs on CSS color values for more information. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` + +``` +invalid.css:4:12 lint/nursery/noHexColors ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Unexpected hex color. + + 2 │ a { color: #000; } + 3 │ a { color: #fff1aa; } + > 4 │ a { color: #123456aa; } + │ ^^^^^^^^^ + 5 │ a { color: #0000000000000000; } + 6 │ + + i Hex colors are less readable and have limitations compared to other color models. + + i Consider using a named color or a color function like rgb(), hsl() or oklch(). See MDN Web Docs on CSS color values for more information. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` + +``` +invalid.css:5:12 lint/nursery/noHexColors ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Unexpected hex color. + + 3 │ a { color: #fff1aa; } + 4 │ a { color: #123456aa; } + > 5 │ a { color: #0000000000000000; } + │ ^^^^^^^^^^^^^^^^^ + 6 │ + + i Hex colors are less readable and have limitations compared to other color models. + + i Consider using a named color or a color function like rgb(), hsl() or oklch(). See MDN Web Docs on CSS color values for more information. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` diff --git a/crates/biome_css_analyze/tests/specs/nursery/noHexColors/valid.css b/crates/biome_css_analyze/tests/specs/nursery/noHexColors/valid.css new file mode 100644 index 000000000000..f519fb656776 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noHexColors/valid.css @@ -0,0 +1,7 @@ +/* should not generate diagnostics */ +p { + color: red; +} +a { color: black; } +a { color: rgb(0, 0, 0); } +a { color: rgba(0, 0, 0, 1); } diff --git a/crates/biome_css_analyze/tests/specs/nursery/noHexColors/valid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noHexColors/valid.css.snap new file mode 100644 index 000000000000..fef37b2194cd --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noHexColors/valid.css.snap @@ -0,0 +1,16 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +assertion_line: 111 +expression: valid.css +--- +# Input +```css +/* should not generate diagnostics */ +p { + color: red; +} +a { color: black; } +a { color: rgb(0, 0, 0); } +a { color: rgba(0, 0, 0, 1); } + +``` diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 8ff9c94d2c7e..23fa81b8b0e4 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -185,6 +185,7 @@ define_categories! { "lint/nursery/noFloatingClasses": "https://biomejs.dev/linter/rules/no-floating-classes", "lint/nursery/noFloatingPromises": "https://biomejs.dev/linter/rules/no-floating-promises", "lint/nursery/noForIn": "https://biomejs.dev/linter/rules/no-for-in", + "lint/nursery/noHexColors": "https://biomejs.dev/linter/rules/no-hex-colors", "lint/nursery/noImplicitCoercion": "https://biomejs.dev/linter/rules/no-implicit-coercion", "lint/nursery/noImportCycles": "https://biomejs.dev/linter/rules/no-import-cycles", "lint/nursery/noIncrementDecrement": "https://biomejs.dev/linter/rules/no-increment-decrement", @@ -193,7 +194,6 @@ define_categories! { "lint/nursery/noLeakedRender": "https://biomejs.dev/linter/rules/no-leaked-render", "lint/nursery/noMissingGenericFamilyKeyword": "https://biomejs.dev/linter/rules/no-missing-generic-family-keyword", "lint/nursery/noMisusedPromises": "https://biomejs.dev/linter/rules/no-misused-promises", - "lint/nursery/useConsistentEnumValueType": "https://biomejs.dev/linter/rules/use-consistent-enum-value-type", "lint/nursery/noMultiAssign": "https://biomejs.dev/linter/rules/no-multi-assign", "lint/nursery/noMultiStr": "https://biomejs.dev/linter/rules/no-multi-str", "lint/nursery/noNextAsyncClientComponent": "https://biomejs.dev/linter/rules/no-next-async-client-component", @@ -226,6 +226,7 @@ define_categories! { "lint/nursery/useAwaitThenable": "https://biomejs.dev/linter/rules/use-await-thenable", "lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/linter/rules/use-biome-suppression-comment", "lint/nursery/useConsistentArrowReturn": "https://biomejs.dev/linter/rules/use-consistent-arrow-return", + "lint/nursery/useConsistentEnumValueType": "https://biomejs.dev/linter/rules/use-consistent-enum-value-type", "lint/nursery/useConsistentGraphqlDescriptions": "https://biomejs.dev/linter/rules/use-consistent-graphql-descriptions", "lint/nursery/useConsistentObjectDefinition": "https://biomejs.dev/linter/rules/use-consistent-object-definition", "lint/nursery/useDeprecatedDate": "https://biomejs.dev/linter/rules/use-deprecated-date", @@ -238,9 +239,9 @@ define_categories! { "lint/nursery/useImportRestrictions": "https://biomejs.dev/linter/rules/use-import-restrictions", "lint/nursery/useInlineScriptId": "https://biomejs.dev/linter/rules/use-inline-script-id", "lint/nursery/useJsxCurlyBraceConvention": "https://biomejs.dev/linter/rules/use-jsx-curly-brace-convention", + "lint/nursery/useLoneAnonymousOperation": "https://biomejs.dev/linter/rules/use-lone-anonymous-operation", "lint/nursery/useLoneExecutableDefinition": "https://biomejs.dev/linter/rules/use-lone-executable-definition", "lint/nursery/useMaxParams": "https://biomejs.dev/linter/rules/use-max-params", - "lint/nursery/useLoneAnonymousOperation": "https://biomejs.dev/linter/rules/use-lone-anonymous-operation", "lint/nursery/useQwikMethodUsage": "https://biomejs.dev/linter/rules/use-qwik-method-usage", "lint/nursery/useQwikValidLexicalScope": "https://biomejs.dev/linter/rules/use-qwik-valid-lexical-scope", "lint/nursery/useRegexpExec": "https://biomejs.dev/linter/rules/use-regexp-exec", diff --git a/crates/biome_rule_options/src/lib.rs b/crates/biome_rule_options/src/lib.rs index 13943800a24d..6e8ca6b3c6f8 100644 --- a/crates/biome_rule_options/src/lib.rs +++ b/crates/biome_rule_options/src/lib.rs @@ -114,6 +114,7 @@ pub mod no_global_object_calls; pub mod no_head_element; pub mod no_head_import_in_document; pub mod no_header_scope; +pub mod no_hex_colors; pub mod no_img_element; pub mod no_implicit_any_let; pub mod no_implicit_boolean; diff --git a/crates/biome_rule_options/src/no_hex_colors.rs b/crates/biome_rule_options/src/no_hex_colors.rs new file mode 100644 index 000000000000..1e0d7d6e68cc --- /dev/null +++ b/crates/biome_rule_options/src/no_hex_colors.rs @@ -0,0 +1,6 @@ +use biome_deserialize_macros::{Deserializable, Merge}; +use serde::{Deserialize, Serialize}; +#[derive(Default, Clone, Debug, Deserialize, Deserializable, Merge, Eq, PartialEq, Serialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields, default)] +pub struct NoHexColorsOptions {} diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 460958b73aba..181c8a8b4ea9 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1969,6 +1969,11 @@ See https://biomejs.dev/linter/rules/no-for-in */ noForIn?: NoForInConfiguration; /** + * Disallow hex colors. +See https://biomejs.dev/linter/rules/no-hex-colors + */ + noHexColors?: NoHexColorsConfiguration; + /** * Prevent import cycles. See https://biomejs.dev/linter/rules/no-import-cycles */ @@ -3822,6 +3827,9 @@ export type NoFloatingPromisesConfiguration = export type NoForInConfiguration = | RulePlainConfiguration | RuleWithNoForInOptions; +export type NoHexColorsConfiguration = + | RulePlainConfiguration + | RuleWithNoHexColorsOptions; export type NoImportCyclesConfiguration = | RulePlainConfiguration | RuleWithNoImportCyclesOptions; @@ -5356,6 +5364,10 @@ export interface RuleWithNoForInOptions { level: RulePlainConfiguration; options?: NoForInOptions; } +export interface RuleWithNoHexColorsOptions { + level: RulePlainConfiguration; + options?: NoHexColorsOptions; +} export interface RuleWithNoImportCyclesOptions { level: RulePlainConfiguration; options?: NoImportCyclesOptions; @@ -6804,6 +6816,7 @@ export interface NoExcessiveLinesPerFileOptions { export type NoFloatingClassesOptions = {}; export type NoFloatingPromisesOptions = {}; export type NoForInOptions = {}; +export type NoHexColorsOptions = {}; export interface NoImportCyclesOptions { /** * Ignores type-only imports when finding an import cycle. A type-only import (`import type`) @@ -7745,6 +7758,7 @@ export type Category = | "lint/nursery/noFloatingClasses" | "lint/nursery/noFloatingPromises" | "lint/nursery/noForIn" + | "lint/nursery/noHexColors" | "lint/nursery/noImplicitCoercion" | "lint/nursery/noImportCycles" | "lint/nursery/noIncrementDecrement" @@ -7753,7 +7767,6 @@ export type Category = | "lint/nursery/noLeakedRender" | "lint/nursery/noMissingGenericFamilyKeyword" | "lint/nursery/noMisusedPromises" - | "lint/nursery/useConsistentEnumValueType" | "lint/nursery/noMultiAssign" | "lint/nursery/noMultiStr" | "lint/nursery/noNextAsyncClientComponent" @@ -7786,6 +7799,7 @@ export type Category = | "lint/nursery/useAwaitThenable" | "lint/nursery/useBiomeSuppressionComment" | "lint/nursery/useConsistentArrowReturn" + | "lint/nursery/useConsistentEnumValueType" | "lint/nursery/useConsistentGraphqlDescriptions" | "lint/nursery/useConsistentObjectDefinition" | "lint/nursery/useDeprecatedDate" @@ -7798,9 +7812,9 @@ export type Category = | "lint/nursery/useImportRestrictions" | "lint/nursery/useInlineScriptId" | "lint/nursery/useJsxCurlyBraceConvention" + | "lint/nursery/useLoneAnonymousOperation" | "lint/nursery/useLoneExecutableDefinition" | "lint/nursery/useMaxParams" - | "lint/nursery/useLoneAnonymousOperation" | "lint/nursery/useQwikMethodUsage" | "lint/nursery/useQwikValidLexicalScope" | "lint/nursery/useRegexpExec" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 6b9592e9317f..41604365fe8d 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -3613,6 +3613,13 @@ ] }, "NoHeaderScopeOptions": { "type": "object", "additionalProperties": false }, + "NoHexColorsConfiguration": { + "oneOf": [ + { "$ref": "#/$defs/RulePlainConfiguration" }, + { "$ref": "#/$defs/RuleWithNoHexColorsOptions" } + ] + }, + "NoHexColorsOptions": { "type": "object", "additionalProperties": false }, "NoImgElementConfiguration": { "oneOf": [ { "$ref": "#/$defs/RulePlainConfiguration" }, @@ -5516,6 +5523,13 @@ { "type": "null" } ] }, + "noHexColors": { + "description": "Disallow hex colors.\nSee https://biomejs.dev/linter/rules/no-hex-colors", + "anyOf": [ + { "$ref": "#/$defs/NoHexColorsConfiguration" }, + { "type": "null" } + ] + }, "noImportCycles": { "description": "Prevent import cycles.\nSee https://biomejs.dev/linter/rules/no-import-cycles", "anyOf": [ @@ -7588,6 +7602,15 @@ "additionalProperties": false, "required": ["level"] }, + "RuleWithNoHexColorsOptions": { + "type": "object", + "properties": { + "level": { "$ref": "#/$defs/RulePlainConfiguration" }, + "options": { "$ref": "#/$defs/NoHexColorsOptions" } + }, + "additionalProperties": false, + "required": ["level"] + }, "RuleWithNoImgElementOptions": { "type": "object", "properties": {