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_cli/src/execute/migrate/eslint_any_rule_to_biome.rs b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs index 29335a6a48ea..7d0dbf512d10 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 @@ -241,6 +241,34 @@ pub(crate) fn migrate_eslint_any_rule( .get_or_insert(Default::default()); rule.set_level(rule.level().max(rule_severity.into())); } + "@html-eslint/no-inline-styles" => { + if !options.include_nursery { + results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery); + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group + .unwrap_group_as_mut() + .no_inline_styles + .get_or_insert(Default::default()); + rule.set_level(rule.level().max(rule_severity.into())); + } + "@html-eslint/no-inline-styles" => { + if !options.include_inspired { + results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Inspired); + return false; + } + if !options.include_nursery { + results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery); + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group + .unwrap_group_as_mut() + .no_inline_styles + .get_or_insert(Default::default()); + rule.set_level(rule.level().max(rule_severity.into())); + } "@html-eslint/no-positive-tabindex" => { let group = rules.a11y.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 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 + /// `