Skip to content

chore(eslint-plugin-internal): add no-style-prop-css-overrides rule#768

Open
cb-ekuersch wants to merge 3 commits into
masterfrom
erichkuerschner/eslint-style-prop-css-override
Open

chore(eslint-plugin-internal): add no-style-prop-css-overrides rule#768
cb-ekuersch wants to merge 3 commits into
masterfrom
erichkuerschner/eslint-style-prop-css-override

Conversation

@cb-ekuersch

Copy link
Copy Markdown
Contributor

What changed? Why?

Adds a new ESLint rule internal/no-style-prop-css-overrides to libs/eslint-plugin-internal, scoped to cds-web (packages/web/src).

The rule flags top-level CSS declarations inside Linaria css blocks for properties that are already owned by a CDS style prop (height, width, min/max-*, flex/grid sizing, top/left/..., opacity, z-index, color, background-color, box-shadowelevation, border-*, font-*, line-height, padding-*, margin-*, gap, display, position, overflow, align-*, justify-content, etc.).

Why: cds-web style props compile to single-class Linaria rules (the value is injected as an inline CSS variable, but the declaration consuming it lives in a class), which keeps style props consumer-overridable. A component's own css block sets the same property at the same specificity but is emitted later in the stylesheet, so it wins the CSS source-order tiebreaker and silently overrides values passed via the style prop. The rule turns this implicit footgun into a lint warning.

What it intentionally allows (no false positives):

  • Properties no style prop owns (cursor, transition, …).
  • Declarations nested inside selectors / pseudo-states / at-rules (&:hover, @media, descendant selectors) — these can't be expressed via static style props.
  • Multi-value padding/margin shorthands (e.g. padding: 4px 8px).
  • css imported from anywhere other than @linaria/core.

Scoped in eslint.config.mjs to packages/web/src/**, excluding the style system itself (packages/web/src/styles/**), stories, tests, mocks, fixtures, and Figma Code Connect files. Severity is warn.

Includes a dedicated rule README (src/no-style-prop-css-overrides/README.md) with the full cascade rationale and remediation guidance, plus a summary entry in the plugin README.

Root cause (required for bugfixes)

Not a bugfix — this is a preventative lint rule. It guards against the class of bug fixed in CDS-2118 (Button height/width props not applying), where a component's css block declared height/width that out-ordered the style-prop class in the cascade.

UI changes

N/A — lint/tooling-only change with no runtime or visual impact.

Testing

How has it been tested?

  • Unit tests
  • Interaction tests
  • Pseudo State tests
  • Manual - Web
  • Manual - Android (Emulator / Device)
  • Manual - iOS (Emulator / Device)

Testing instructions

  • Rule unit tests (8 cases): yarn nx run eslint-plugin-internal:test --testPathPattern=no-style-prop-css-overrides
  • Manual: add a top-level owned property (e.g. height: 40px) to a css block in packages/web/src/** and confirm internal/no-style-prop-css-overrides warns; confirm the same declaration inside &:hover { ... } does not warn.

Illustrations/Icons Checklist

N/A — no changes under packages/illustrations/** or packages/icons/**.

Change management

type=routine
risk=low
impact=sev5

automerge=false

Made with Cursor

Flags top-level CSS declarations inside Linaria `css` blocks (scoped to
cds-web) for properties already owned by a CDS style prop (height, width,
padding-*, background-color, box-shadow, etc.).

Such a declaration sits at the same single-class specificity as the base
style-prop class but is emitted later in the stylesheet, so it wins the
CSS source-order tiebreaker and silently overrides values consumers pass
via the matching style prop. This is the class of bug fixed in CDS-2118
(Button height/width props not applying).

Only top-level declarations are flagged; nested selectors/pseudo-states/
at-rules and multi-value padding/margin shorthands are allowed. Scoped in
eslint.config.mjs to packages/web/src (excluding the style system,
stories, tests, mocks, fixtures, and Figma files) at the `warn` level.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cb-heimdall

Copy link
Copy Markdown
Collaborator

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 1
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1
CODEOWNERS 🟡 See below

🟡 CODEOWNERS

Code Owner Status Calculation
ui-systems-eng-team 🟡 0/1
Denominator calculation
Additional CODEOWNERS Requirement
Show calculation
Sum 0
0
From CODEOWNERS 1
Sum 1

cb-ekuersch and others added 2 commits June 23, 2026 13:45
…-location aware

Reduce false positives by only flagging a CSS property hard-coded in a Linaria
`css` class when the element wearing that class is *also* explicitly passed the
matching cds-web style prop. Previously the rule flagged every top-level owned
property in any `css` block, which incorrectly caught properties components
legitimately hard-code but never expose as props on that element (e.g. Button's
baseCss `display: inline-flex`).

The element's `className` is resolved through identifiers, inline `css`,
`cx`/`cn`/`clsx`/`classnames`, and logical/conditional/array/template
expressions; padding/margin shorthands and longhands are compared per physical
side. Implemented via AST (explicit JSX attributes) rather than the type
checker: the repo doesn't enable type-aware linting for web, and matching a
`{...props}` spread's type would re-surface style props a component extends but
never intends to expose, reinstating the false positives. typescript-eslint
also discourages rules whose behavior depends on type-info availability.

Validated against packages/web/src: catches real latent overrides (e.g. Button
`minWidth` vs `min-width: unset`, Text `fontFamily`/`display` via conditional
css classes) with no false positives on hard-coded, non-exposed properties.

Co-authored-by: Cursor <cursoragent@cursor.com>
… in no-style-prop-css-overrides

Extend the rule to catch style props that reach an element through a
`{...spread}` rather than an explicit attribute. The rule now resolves the
TypeScript type of each spread argument and treats any property whose name is a
cds-web style prop as able to land on the element, then flags it when a
co-located Linaria `css` class hard-codes the same CSS property.

This catches the spread-forwarding footgun (e.g. Button hard-coding
`height: fit-content` in baseCss while `height` flows to Pressable via
`{...props}`), which the explicit-attribute check missed. Explicit attributes
take precedence over spreads for reporting, and a prop destructured out of the
spread is correctly not flagged.

Because the spread analysis requires type information, the rule's config block
in eslint.config.mjs now enables parserOptions.projectService for the in-scope
packages/web/src TS/TSX files (per typescript-eslint guidance, the rule requires
type info rather than degrading when it is absent). Tests are converted to the
type-aware RuleTester (projectService) and cover spread, destructure-out, and
explicit-precedence cases.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

2 participants