Skip to content

feat(linter): implement noInlineStyles HTML lint rule#9319

Closed
Beltranss wants to merge 1 commit intobiomejs:mainfrom
Beltranss:feat/no-inline-styles
Closed

feat(linter): implement noInlineStyles HTML lint rule#9319
Beltranss wants to merge 1 commit intobiomejs:mainfrom
Beltranss:feat/no-inline-styles

Conversation

@Beltranss
Copy link
Copy Markdown

Summary

Closes #9062.

Implements the noInlineStyles nursery lint rule for HTML that disallows the use of the style attribute on HTML elements. This is a port of the html-eslint no-inline-styles rule.

Inline styles are discouraged because they:

  • Mix content with presentation, making code harder to read and maintain
  • Reduce reusability
  • Can prevent effective use of a strict Content Security Policy

The rule provides an unsafe auto-fix that removes the style attribute.

Example

<!-- Invalid -->
<div style="color: red;"></div>

<!-- Valid -->
<div class="text-red"></div>

AI Assistance Disclosure

This PR was written primarily with the assistance of Claude Code (Claude Opus). The implementation, tests, documentation, and commit message were all generated with AI assistance, reviewed and approved by the contributor.

Test plan

  • Added invalid.html test with 5 cases covering various elements with inline styles
  • Added valid.html test with cases using classes, IDs, and <style> blocks
  • All snapshot tests pass (cargo test -p biome_html_analyze)
  • Code generation completed successfully (just gen-analyzer)

🤖 Generated with Claude Code

Closes biomejs#9062.

Added a new nursery lint rule `noInlineStyles` that disallows the use of
the `style` attribute on HTML elements. Inline styles are discouraged
because they mix content with presentation, reduce maintainability, and
can prevent effective use of a strict Content Security Policy.

The rule provides an unsafe auto-fix that removes the `style` attribute.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 3, 2026

⚠️ No Changeset found

Latest commit: 3737c6d

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions github-actions Bot added A-CLI Area: CLI A-Project Area: project A-Linter Area: linter A-Diagnostic Area: diagnostocis L-HTML Language: HTML and super languages labels Mar 3, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 3, 2026

Walkthrough

This pull request introduces a new HTML linter rule NoInlineStyles to detect and remove inline style attributes from HTML elements. The implementation includes the rule declaration in the nursery lint module, a new options structure for configuration, and comprehensive test cases covering both valid (CSS classes) and invalid (inline styles) patterns. The rule integrates with Biome's existing lint framework and provides diagnostic messages alongside automatic fix capabilities.

Suggested labels

A-Linter, L-HTML, A-Diagnostic

Suggested reviewers

  • ematipico
  • dyc3
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: implementing the noInlineStyles HTML lint rule.
Description check ✅ Passed The description clearly explains the feature, references the closed issue, provides reasoning, examples, and test coverage.
Linked Issues check ✅ Passed The PR fully implements the objective from #9062: a rule that disallows inline styles on HTML elements with reasoning around maintainability and CSP.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the noInlineStyles rule: the lint implementation, test cases, and rule options.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/biome_rule_options/src/no_inline_styles.rs (1)

3-6: Add rustdoc to the options type.

Tiny one, but this struct is public and should be documented like other rule options.

Proposed tweak
 #[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)]
+/// Options for the `noInlineStyles` rule.
+///
+/// This rule currently has no configurable options.
 pub struct NoInlineStylesOptions {}
As per coding guidelines, "Use inline rustdoc documentation for rules, assists, and their options".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_rule_options/src/no_inline_styles.rs` around lines 3 - 6, Add a
one- or two-line rustdoc comment to the public options struct
NoInlineStylesOptions explaining its purpose and usage (e.g., what the option
controls and any default behavior) so it matches the project's inline
documentation standard; place the /// doc comment immediately above the pub
struct NoInlineStylesOptions declaration and keep the wording concise and
descriptive consistent with other rule options' docs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_html_analyze/src/lint/nursery/no_inline_styles.rs`:
- Around line 103-107: The is_style_attribute function currently compares the
attribute name with a case-sensitive "style", which misses variants like "STYLE"
or "Style"; update the comparison inside is_style_attribute (working with
HtmlAttribute and name.value_token().text_trimmed()) to use a case-insensitive
comparison (e.g., call eq_ignore_ascii_case on the trimmed text) so the rule
correctly detects style attributes regardless of case, matching the approach
used in no_autofocus.rs and use_scoped_styles.rs.

---

Nitpick comments:
In `@crates/biome_rule_options/src/no_inline_styles.rs`:
- Around line 3-6: Add a one- or two-line rustdoc comment to the public options
struct NoInlineStylesOptions explaining its purpose and usage (e.g., what the
option controls and any default behavior) so it matches the project's inline
documentation standard; place the /// doc comment immediately above the pub
struct NoInlineStylesOptions declaration and keep the wording concise and
descriptive consistent with other rule options' docs.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1022662 and 3737c6d.

⛔ Files ignored due to path filters (7)
  • crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs is excluded by !**/migrate/eslint_any_rule_to_biome.rs and included by **
  • crates/biome_configuration/src/analyzer/linter/rules.rs is excluded by !**/rules.rs and included by **
  • crates/biome_diagnostics_categories/src/categories.rs is excluded by !**/categories.rs and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noInlineStyles/invalid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noInlineStyles/valid.html.snap is excluded by !**/*.snap and included by **
  • packages/@biomejs/backend-jsonrpc/src/workspace.ts is excluded by !**/backend-jsonrpc/src/workspace.ts and included by **
  • packages/@biomejs/biome/configuration_schema.json is excluded by !**/configuration_schema.json and included by **
📒 Files selected for processing (5)
  • crates/biome_html_analyze/src/lint/nursery/no_inline_styles.rs
  • crates/biome_html_analyze/tests/specs/nursery/noInlineStyles/invalid.html
  • crates/biome_html_analyze/tests/specs/nursery/noInlineStyles/valid.html
  • crates/biome_rule_options/src/lib.rs
  • crates/biome_rule_options/src/no_inline_styles.rs

Comment on lines +103 to +107
fn is_style_attribute(node: &HtmlAttribute) -> bool {
node.name().is_ok_and(|name| {
name.value_token()
.is_ok_and(|value_token| value_token.text_trimmed() == "style")
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Read the source file to see the actual code
cat -n crates/biome_html_analyze/src/lint/nursery/no_inline_styles.rs | head -120 | tail -25

Repository: biomejs/biome

Length of output: 1128


🏁 Script executed:

# Check test fixtures for coverage of uppercase variants
ls -la crates/biome_html_analyze/tests/specs/nursery/noInlineStyles/ 2>/dev/null || echo "Path not found"

Repository: biomejs/biome

Length of output: 428


🏁 Script executed:

# Search for test fixtures
find crates/biome_html_analyze/tests -name "*noInlineStyles*" -o -name "*no_inline_styles*" 2>/dev/null

Repository: biomejs/biome

Length of output: 117


🏁 Script executed:

# Read the test fixtures to see what cases are covered
cat crates/biome_html_analyze/tests/specs/nursery/noInlineStyles/invalid.html

Repository: biomejs/biome

Length of output: 318


🏁 Script executed:

# Read the valid fixtures too
cat crates/biome_html_analyze/tests/specs/nursery/noInlineStyles/valid.html

Repository: biomejs/biome

Length of output: 335


🏁 Script executed:

# Check the rule metadata to see if fix_kind is present
head -80 crates/biome_html_analyze/src/lint/nursery/no_inline_styles.rs

Repository: biomejs/biome

Length of output: 2682


🏁 Script executed:

# Check if HTML attribute names are case-sensitive in HTML spec
# Also check if the parser normalizes them
rg -n "HtmlAttribute|lowercase|case" crates/biome_html_analyze/src/lint/nursery/no_inline_styles.rs -A2 -B2

Repository: biomejs/biome

Length of output: 565


🏁 Script executed:

# Check how the HTML parser tokenizes attribute names
# Look for any case normalization in the HTML syntax/parsing layer
rg -n "text_trimmed|lowercase|to_lowercase|eq_ignore" crates/biome_html_syntax/ -i --type rs | head -20

Repository: biomejs/biome

Length of output: 83


🏁 Script executed:

# Check if there's any documentation or tests about attribute name handling
rg -n "style.*attribute|attribute.*case" crates/biome_html_analyze/tests/ -i -A2 -B2

Repository: biomejs/biome

Length of output: 29043


🏁 Script executed:

# Search for how HtmlAttribute name is extracted and used elsewhere
rg -n "HtmlAttribute.*name\(\)|\.name\(\)" crates/biome_html_analyze/src/ -A3 | head -40

Repository: biomejs/biome

Length of output: 3546


🏁 Script executed:

# Check the HTML spec or parser behavior by looking at other attribute checks
rg -n "value_token\(\).*text_trimmed" crates/biome_html_analyze/src/ -B3 -A3

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

# Verify how the HTML parser/tokenizer handles attribute names
# Check if there's any normalization happening before reaching is_style_attribute
find crates/biome_html -name "*.rs" -type f | xargs rg -l "HtmlAttribute" | head -5

Repository: biomejs/biome

Length of output: 313


🏁 Script executed:

# Check if there are any comments or documentation about case sensitivity
rg -n "case.sensitive|case.insensitive|uppercase|lowercase" crates/biome_html_analyze/src/lint/nursery/no_inline_styles.rs

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

# Create a test to verify the actual behaviour
cat > /tmp/test_case_sensitivity.html << 'EOF'
<!-- lowercase - should be detected -->
<div style="color: red;"></div>

<!-- uppercase - will this be detected? -->
<div STYLE="color: red;"></div>

<!-- mixed case -->
<div Style="color: red;"></div>
EOF

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

# Find the HTML syntax crate
find crates -name "*html*" -type d | head -10

Repository: biomejs/biome

Length of output: 502


🏁 Script executed:

# Check how attribute names are parsed - look at the actual parser
rg -n "HtmlAttribute\|attribute.*name" crates/biome_html_analyze/src/ -l | head -5

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

# Look at other rules that check attribute names to see if they use case-insensitive comparison
rg -n "text_trimmed.*==" crates/biome_html_analyze/src/lint/ -B2 -A2 | head -40

Repository: biomejs/biome

Length of output: 3932


🏁 Script executed:

# Check if HTML spec defines attributes as case-insensitive
# Let's look at how autofocus and other attributes are checked
rg -n "eq_ignore_ascii_case" crates/biome_html_analyze/src/lint/ -B3 -A1

Repository: biomejs/biome

Length of output: 13461


🏁 Script executed:

# Quick confirmation: check that use_scoped_styles also handles style attribute/element names with case-insensitivity
rg -n "use_scoped_styles" crates/biome_html_analyze/src/lint/nursery/ -l

Repository: biomejs/biome

Length of output: 120


🏁 Script executed:

# Verify no_duplicate_classes uses case-insensitive check (earlier results showed it uses ==)
head -80 crates/biome_html_analyze/src/assist/source/no_duplicate_classes.rs | tail -15

Repository: biomejs/biome

Length of output: 569


Use case-insensitive comparison for style attribute.

Line 106 uses strict equality to "style", which misses uppercase or mixed-case variants like STYLE or Style. HTML attributes are case-insensitive, so the rule can be bypassed. Align with the pattern used elsewhere in the codebase (e.g. no_autofocus.rs, use_scoped_styles.rs) by using eq_ignore_ascii_case:

Proposed fix
 fn is_style_attribute(node: &HtmlAttribute) -> bool {
     node.name().is_ok_and(|name| {
         name.value_token()
-            .is_ok_and(|value_token| value_token.text_trimmed() == "style")
+            .is_ok_and(|value_token| value_token.text_trimmed().eq_ignore_ascii_case("style"))
     })
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn is_style_attribute(node: &HtmlAttribute) -> bool {
node.name().is_ok_and(|name| {
name.value_token()
.is_ok_and(|value_token| value_token.text_trimmed() == "style")
})
fn is_style_attribute(node: &HtmlAttribute) -> bool {
node.name().is_ok_and(|name| {
name.value_token()
.is_ok_and(|value_token| value_token.text_trimmed().eq_ignore_ascii_case("style"))
})
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_html_analyze/src/lint/nursery/no_inline_styles.rs` around lines
103 - 107, The is_style_attribute function currently compares the attribute name
with a case-sensitive "style", which misses variants like "STYLE" or "Style";
update the comparison inside is_style_attribute (working with HtmlAttribute and
name.value_token().text_trimmed()) to use a case-insensitive comparison (e.g.,
call eq_ignore_ascii_case on the trimmed text) so the rule correctly detects
style attributes regardless of case, matching the approach used in
no_autofocus.rs and use_scoped_styles.rs.

Copy link
Copy Markdown
Contributor

@dyc3 dyc3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a js rule that detects this in jsx as well!

Comment on lines +103 to +107
fn is_style_attribute(node: &HtmlAttribute) -> bool {
node.name().is_ok_and(|name| {
name.value_token()
.is_ok_and(|value_token| value_token.text_trimmed() == "style")
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: tbh, i would just inline this instead of having another function for it

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Mar 4, 2026

Merging this PR will not alter performance

✅ 64 untouched benchmarks
⏩ 152 skipped benchmarks1


Comparing Beltranss:feat/no-inline-styles (3737c6d) with main (40869b5)2

Open in CodSpeed

Footnotes

  1. 152 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (1022662) during the generation of this report, so 40869b5 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Copy link
Copy Markdown
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR isn't ready. It doesn't handle components, which means components with style attribute are triggered.

Please implement that and tests

@Netail
Copy link
Copy Markdown
Member

Netail commented Mar 4, 2026

Duplicate of #9146, but this implementation seems better. However, this PR does not seem to handle JSX, while the other does

@ematipico
Copy link
Copy Markdown
Member

ematipico commented Mar 9, 2026

Yeah but both didn't address out comments yet

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-CLI Area: CLI A-Diagnostic Area: diagnostocis A-Linter Area: linter A-Project Area: project L-HTML Language: HTML and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

📎 Port no-inline-styles from html-eslint

4 participants