From 226e9ce0750c4df10d6ed4eeebf9b96ad980bcb2 Mon Sep 17 00:00:00 2001 From: MCMXC <16797721+mcmxcdev@users.noreply.github.com> Date: Thu, 19 Feb 2026 07:53:45 -0700 Subject: [PATCH 1/2] feat(lint): add NoInlineStyles nursery rule --- .changeset/add-no-inline-styles-rule.md | 5 + .../src/analyzer/linter/rules.rs | 4 + .../src/generated/domain_selector.rs | 1 + .../src/categories.rs | 5 +- .../src/lint/nursery/no_inline_styles.rs | 89 +++++++++ .../specs/nursery/noInlineStyles/invalid.html | 13 ++ .../nursery/noInlineStyles/invalid.html.snap | 147 +++++++++++++++ .../specs/nursery/noInlineStyles/valid.html | 11 ++ .../nursery/noInlineStyles/valid.html.snap | 19 ++ .../src/lint/nursery/no_inline_styles.rs | 149 +++++++++++++++ .../specs/nursery/noInlineStyles/invalid.jsx | 15 ++ .../nursery/noInlineStyles/invalid.jsx.snap | 170 ++++++++++++++++++ .../specs/nursery/noInlineStyles/valid.jsx | 15 ++ .../nursery/noInlineStyles/valid.jsx.snap | 23 +++ crates/biome_rule_options/src/lib.rs | 1 + .../src/no_inline_styles.rs | 6 + 16 files changed, 671 insertions(+), 2 deletions(-) create mode 100644 .changeset/add-no-inline-styles-rule.md create mode 100644 crates/biome_html_analyze/src/lint/nursery/no_inline_styles.rs create mode 100644 crates/biome_html_analyze/tests/specs/nursery/noInlineStyles/invalid.html create mode 100644 crates/biome_html_analyze/tests/specs/nursery/noInlineStyles/invalid.html.snap create mode 100644 crates/biome_html_analyze/tests/specs/nursery/noInlineStyles/valid.html create mode 100644 crates/biome_html_analyze/tests/specs/nursery/noInlineStyles/valid.html.snap create mode 100644 crates/biome_js_analyze/src/lint/nursery/no_inline_styles.rs create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noInlineStyles/invalid.jsx create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noInlineStyles/invalid.jsx.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noInlineStyles/valid.jsx create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noInlineStyles/valid.jsx.snap create mode 100644 crates/biome_rule_options/src/no_inline_styles.rs diff --git a/.changeset/add-no-inline-styles-rule.md b/.changeset/add-no-inline-styles-rule.md new file mode 100644 index 000000000000..6c29c59d5d7d --- /dev/null +++ b/.changeset/add-no-inline-styles-rule.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +Added the nursery rule [`noInlineStyles`](https://biomejs.dev/linter/rules/no-inline-styles/). The rule disallows the use of inline `style` attributes in HTML and the `style` prop in JSX, including `React.createElement` calls. Inline styles make code harder to maintain and can interfere with Content Security Policy. diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs index ae5da59ffdb5..b882a15421c7 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -214,6 +214,7 @@ pub enum RuleName { NoImportantStyles, NoIncrementDecrement, NoInferrableTypes, + NoInlineStyles, NoInnerDeclarations, NoInteractiveElementToNoninteractiveRole, NoInvalidBuiltinInstantiation, @@ -669,6 +670,7 @@ impl RuleName { Self::NoImportantStyles => "noImportantStyles", Self::NoIncrementDecrement => "noIncrementDecrement", Self::NoInferrableTypes => "noInferrableTypes", + Self::NoInlineStyles => "noInlineStyles", Self::NoInnerDeclarations => "noInnerDeclarations", Self::NoInteractiveElementToNoninteractiveRole => { "noInteractiveElementToNoninteractiveRole" @@ -1128,6 +1130,7 @@ impl RuleName { Self::NoImportantStyles => RuleGroup::Complexity, Self::NoIncrementDecrement => RuleGroup::Nursery, Self::NoInferrableTypes => RuleGroup::Style, + Self::NoInlineStyles => RuleGroup::Nursery, Self::NoInnerDeclarations => RuleGroup::Correctness, Self::NoInteractiveElementToNoninteractiveRole => RuleGroup::A11y, Self::NoInvalidBuiltinInstantiation => RuleGroup::Correctness, @@ -1588,6 +1591,7 @@ impl std::str::FromStr for RuleName { "noImportantStyles" => Ok(Self::NoImportantStyles), "noIncrementDecrement" => Ok(Self::NoIncrementDecrement), "noInferrableTypes" => Ok(Self::NoInferrableTypes), + "noInlineStyles" => Ok(Self::NoInlineStyles), "noInnerDeclarations" => Ok(Self::NoInnerDeclarations), "noInteractiveElementToNoninteractiveRole" => { Ok(Self::NoInteractiveElementToNoninteractiveRole) diff --git a/crates/biome_configuration/src/generated/domain_selector.rs b/crates/biome_configuration/src/generated/domain_selector.rs index 4230d0d405d3..3a236efc5f09 100644 --- a/crates/biome_configuration/src/generated/domain_selector.rs +++ b/crates/biome_configuration/src/generated/domain_selector.rs @@ -67,6 +67,7 @@ static REACT_FILTERS: LazyLock>> = LazyLock::new(|| { RuleFilter::Rule("correctness", "useJsxKeyInIterable"), RuleFilter::Rule("correctness", "useUniqueElementIds"), RuleFilter::Rule("nursery", "noDuplicatedSpreadProps"), + RuleFilter::Rule("nursery", "noInlineStyles"), RuleFilter::Rule("nursery", "noJsxPropsBind"), RuleFilter::Rule("nursery", "noLeakedRender"), RuleFilter::Rule("nursery", "noSyncScripts"), diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index cf8d9d13b764..d1e0e45abd4f 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -173,10 +173,10 @@ define_categories! { "lint/correctness/useValidForDirection": "https://biomejs.dev/linter/rules/use-valid-for-direction", "lint/correctness/useValidTypeof": "https://biomejs.dev/linter/rules/use-valid-typeof", "lint/correctness/useYield": "https://biomejs.dev/linter/rules/use-yield", - "lint/nursery/useExpect": "https://biomejs.dev/linter/rules/use-expect", "lint/nursery/noAmbiguousAnchorText": "https://biomejs.dev/linter/rules/no-ambiguous-anchor-text", "lint/nursery/noBeforeInteractiveScriptOutsideDocument": "https://biomejs.dev/linter/rules/no-before-interactive-script-outside-document", "lint/nursery/noColorInvalidHex": "https://biomejs.dev/linter/rules/no-color-invalid-hex", + "lint/nursery/noConditionalExpect": "https://biomejs.dev/linter/rules/no-conditional-expect", "lint/nursery/noContinue": "https://biomejs.dev/linter/rules/no-continue", "lint/nursery/noDeprecatedMediaType": "https://biomejs.dev/linter/rules/no-deprecated-media-type", "lint/nursery/noDivRegex": "https://biomejs.dev/linter/rules/no-div-regex", @@ -198,6 +198,7 @@ define_categories! { "lint/nursery/noHexColors": "https://biomejs.dev/linter/rules/no-hex-colors", "lint/nursery/noImplicitCoercion": "https://biomejs.dev/linter/rules/no-implicit-coercion", "lint/nursery/noIncrementDecrement": "https://biomejs.dev/linter/rules/no-increment-decrement", + "lint/nursery/noInlineStyles": "https://biomejs.dev/linter/rules/no-inline-styles", "lint/nursery/noJsxPropsBind": "https://biomejs.dev/linter/rules/no-jsx-props-bind", "lint/nursery/noLeakedRender": "https://biomejs.dev/linter/rules/no-leaked-render", "lint/nursery/noMissingGenericFamilyKeyword": "https://biomejs.dev/linter/rules/no-missing-generic-family-keyword", @@ -206,7 +207,6 @@ define_categories! { "lint/nursery/noMultiStr": "https://biomejs.dev/linter/rules/no-multi-str", "lint/nursery/noNestedPromises": "https://biomejs.dev/linter/rules/no-nested-promises", "lint/nursery/noParametersOnlyUsedInRecursion": "https://biomejs.dev/linter/rules/no-parameters-only-used-in-recursion", - "lint/nursery/noConditionalExpect": "https://biomejs.dev/linter/rules/no-conditional-expect", "lint/nursery/noPlaywrightElementHandle": "https://biomejs.dev/linter/rules/no-playwright-element-handle", "lint/nursery/noPlaywrightEval": "https://biomejs.dev/linter/rules/no-playwright-eval", "lint/nursery/noPlaywrightForceOption": "https://biomejs.dev/linter/rules/no-playwright-force-option", @@ -244,6 +244,7 @@ define_categories! { "lint/nursery/useDestructuring": "https://biomejs.dev/linter/rules/use-destructuring", "lint/nursery/useErrorCause": "https://biomejs.dev/linter/rules/use-error-cause", "lint/nursery/useExhaustiveSwitchCases": "https://biomejs.dev/linter/rules/use-exhaustive-switch-cases", + "lint/nursery/useExpect": "https://biomejs.dev/linter/rules/use-expect", "lint/nursery/useExplicitFunctionReturnType": "https://biomejs.dev/linter/rules/use-explicit-type", "lint/nursery/useExplicitType": "https://biomejs.dev/linter/rules/use-explicit-type", "lint/nursery/useFind": "https://biomejs.dev/linter/rules/use-find", diff --git a/crates/biome_html_analyze/src/lint/nursery/no_inline_styles.rs b/crates/biome_html_analyze/src/lint/nursery/no_inline_styles.rs new file mode 100644 index 000000000000..22ddb938fa7f --- /dev/null +++ b/crates/biome_html_analyze/src/lint/nursery/no_inline_styles.rs @@ -0,0 +1,89 @@ +use biome_analyze::{ + Ast, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule, +}; +use biome_console::markup; +use biome_html_syntax::HtmlAttribute; +use biome_rowan::AstNode; +use biome_rule_options::no_inline_styles::NoInlineStylesOptions; + +declare_lint_rule! { + /// Disallow the use of inline styles on elements. + /// + /// Inline styles are specified using the `style` attribute directly on an element. + /// They make code harder to maintain and override, prevent reusability of styling, and + /// can be a security concern when implementing a strict Content Security Policy (CSP). + /// + /// Instead of inline styles, use CSS classes defined in external stylesheets or + /// `