Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/strong-mirrors-attend.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 12 additions & 0 deletions crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion crates/biome_diagnostics_categories/src/categories.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

154 changes: 154 additions & 0 deletions crates/biome_html_analyze/src/lint/nursery/no_inline_styles.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
use biome_analyze::{
Ast, FixKind, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
};
use biome_console::markup;
use biome_html_syntax::{AnyHtmlElement, HtmlAttribute, HtmlAttributeList, HtmlFileSource};
use biome_rowan::{AstNode, BatchMutationExt, TextRange, TokenText};
use biome_rule_options::no_inline_styles::NoInlineStylesOptions;
use biome_string_case::StrOnlyExtension;

use crate::HtmlRuleAction;

declare_lint_rule! {
/// Disallow the use of inline styles.
///
/// Inline styles via the `style` attribute 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, CSS modules, or a styling library.
///
/// ## Examples
///
/// ### Invalid
///
/// ```html,expect_diagnostic
/// <div style="color: red;"></div>
/// ```
///
/// ```html,expect_diagnostic
/// <p style="font-size: 14px;">Hello</p>
/// ```
///
/// ### Valid
///
/// ```html
/// <div class="text-red"></div>
/// ```
///
/// ```html
/// <p class="body-text">Hello</p>
/// ```
///
/// ## Resources
///
/// - [Content Security Policy: Allowing inline styles](https://content-security-policy.com/examples/allow-inline-style)
///
pub NoInlineStyles {
version: "next",
name: "noInlineStyles",
language: "html",
recommended: false,
sources: &[
RuleSource::HtmlEslint("no-inline-styles").same(),
],
fix_kind: FixKind::Unsafe,
}
}

impl Rule for NoInlineStyles {
type Query = Ast<AnyHtmlElement>;
type State = TextRange;
type Signals = Option<Self::State>;
type Options = NoInlineStylesOptions;

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();

match node {
AnyHtmlElement::HtmlElement(element) => {
let opening = element.opening_element().ok()?;

if let Some(name) = opening.tag_name()
&& is_custom_component(name, ctx)
{
return None;
}

find_style_attribute(opening.attributes()).map(|attribute| attribute.range())
}
AnyHtmlElement::HtmlSelfClosingElement(self_closing_element) => {
if let Some(name) = self_closing_element.tag_name()
&& is_custom_component(name, ctx)
{
return None;
}

find_style_attribute(self_closing_element.attributes())
.map(|attribute| attribute.range())
}
_ => None,
}
}

fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
Some(
RuleDiagnostic::new(
rule_category!(),
state,
markup! {
"Unexpected "<Emphasis>"style"</Emphasis>" attribute."
},
)
.note(markup! {
"Inline styles make code harder to maintain, reduce reusability, and can prevent effective use of a strict Content Security Policy. Use external CSS classes or stylesheets instead."
}),
)
}

fn action(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<HtmlRuleAction> {
let node = ctx.query();
let mut mutation = ctx.root().begin();

match node {
AnyHtmlElement::HtmlElement(element) => {
let opening = element.opening_element().ok()?;
mutation.remove_node(find_style_attribute(opening.attributes()).unwrap())
}
AnyHtmlElement::HtmlSelfClosingElement(self_closing_element) => mutation
.remove_node(find_style_attribute(self_closing_element.attributes()).unwrap()),
_ => {}
}

Some(HtmlRuleAction::new(
ctx.metadata().action_category(ctx.category(), ctx.group()),
ctx.metadata().applicability(),
markup! { "Remove the "<Emphasis>"style"</Emphasis>" attribute." }.to_owned(),
mutation,
))
}
}

fn find_style_attribute(attributes: HtmlAttributeList) -> Option<HtmlAttribute> {
for attribute in attributes {
if let Some(attribute) = attribute.as_html_attribute()
&& let Some(name) = attribute.name().ok()
&& let Some(value_token) = name.value_token().ok()
&& value_token.text_trimmed().to_lowercase_cow() == "style"
{
return Some(attribute.clone());
}
}

None
}

fn is_custom_component(element_name: TokenText, ctx: &RuleContext<NoInlineStyles>) -> bool {
let source_type = ctx.source_type::<HtmlFileSource>();

if source_type.is_html() {
return false;
}

element_name.text() != element_name.to_lowercase_cow()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!-- should generate diagnostics -->

<div style="color: red;"></div>

<p style="font-size: 14px;">Hello</p>

<span style="display: none;">Hidden</span>

<input type="text" style="border: 1px solid black;" />

<a href="#" style="text-decoration: none;">Link</a>
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
---
source: crates/biome_html_analyze/tests/spec_tests.rs
expression: invalid.astro
---
# Input
```html
<!-- should generate diagnostics -->

<div style="color: red;"></div>

<p style="font-size: 14px;">Hello</p>

<span style="display: none;">Hidden</span>

<input type="text" style="border: 1px solid black;" />

<a href="#" style="text-decoration: none;">Link</a>

```

# Diagnostics
```
invalid.astro:3:6 lint/nursery/noInlineStyles FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Unexpected style attribute.

1 │ <!-- should generate diagnostics -->
2 │
> 3 │ <div style="color: red;"></div>
│ ^^^^^^^^^^^^^^^^^^^
4 │
5 │ <p style="font-size: 14px;">Hello</p>

i Inline styles make code harder to maintain, reduce reusability, and can prevent effective use of a strict Content Security Policy. Use external CSS classes or stylesheets instead.

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.

i Unsafe fix: Remove the style attribute.

3 │ <div·style="color:·red;"></div>
│ -------------------

```

```
invalid.astro:5:4 lint/nursery/noInlineStyles FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Unexpected style attribute.

3 │ <div style="color: red;"></div>
4 │
> 5 │ <p style="font-size: 14px;">Hello</p>
│ ^^^^^^^^^^^^^^^^^^^^^^^^
6 │
7 │ <span style="display: none;">Hidden</span>

i Inline styles make code harder to maintain, reduce reusability, and can prevent effective use of a strict Content Security Policy. Use external CSS classes or stylesheets instead.

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.

i Unsafe fix: Remove the style attribute.

5 │ <p·style="font-size:·14px;">Hello</p>
│ ------------------------

```

```
invalid.astro:7:7 lint/nursery/noInlineStyles FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Unexpected style attribute.

5 │ <p style="font-size: 14px;">Hello</p>
6 │
> 7 │ <span style="display: none;">Hidden</span>
│ ^^^^^^^^^^^^^^^^^^^^^^
8 │
9 │ <input type="text" style="border: 1px solid black;" />

i Inline styles make code harder to maintain, reduce reusability, and can prevent effective use of a strict Content Security Policy. Use external CSS classes or stylesheets instead.

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.

i Unsafe fix: Remove the style attribute.

7 │ <span·style="display:·none;">Hidden</span>
│ ----------------------

```

```
invalid.astro:9:20 lint/nursery/noInlineStyles FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Unexpected style attribute.

7 │ <span style="display: none;">Hidden</span>
8 │
> 9 │ <input type="text" style="border: 1px solid black;" />
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
10 │
11 │ <a href="#" style="text-decoration: none;">Link</a>

i Inline styles make code harder to maintain, reduce reusability, and can prevent effective use of a strict Content Security Policy. Use external CSS classes or stylesheets instead.

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.

i Unsafe fix: Remove the style attribute.

9 │ <input·type="text"·style="border:·1px·solid·black;"·/>
│ ---------------------------------

```

```
invalid.astro:11:13 lint/nursery/noInlineStyles FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Unexpected style attribute.

9 │ <input type="text" style="border: 1px solid black;" />
10 │
> 11 │ <a href="#" style="text-decoration: none;">Link</a>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12 │

i Inline styles make code harder to maintain, reduce reusability, and can prevent effective use of a strict Content Security Policy. Use external CSS classes or stylesheets instead.

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.

i Unsafe fix: Remove the style attribute.

11 │ <a·href="#"·style="text-decoration:·none;">Link</a>
│ ------------------------------

```
Loading