feat(rules): add 10 motion, accessibility, and design lint rules#825
Conversation
commit: |
|
No React Doctor issues found. 🎉 Reviewed by React Doctor for commit |
| classNameValue | ||
| .split(/\s+/) | ||
| .filter((token) => token.length > 0) | ||
| .map((token) => token.split(":").pop() ?? token); |
There was a problem hiding this comment.
Important prefix breaks token matching
Low Severity
getClassNameTokens leaves Tailwind’s leading ! on utilities, so tokens like !flex-shrink-0 or !transition-all no longer match equality or startsWith checks in rules that depend on normalized base names.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 2945fec. Configure here.
| node, | ||
| message: | ||
| "`overflow-hidden text-ellipsis whitespace-nowrap` is exactly what the `truncate` utility does — collapse the three classes into `truncate`.", | ||
| }); |
There was a problem hiding this comment.
Truncate rule ignores existing truncate
Low Severity
The rule fires whenever overflow-hidden, text-ellipsis, and whitespace-nowrap all appear, but never checks for an existing truncate class. Class lists that already include truncate plus those three utilities still get told to collapse into truncate, which is redundant noise.
Reviewed by Cursor Bugbot for commit c173c29. Configure here.
| isNodeOfType(property.value, "Literal") && | ||
| typeof property.value.value === "string" && | ||
| property.value.value.startsWith("all") | ||
| property.value.value.trim().startsWith("all") |
There was a problem hiding this comment.
transitionProperty prefix matches too broadly
Low Severity
Inline transition and transitionProperty both use trim().startsWith("all"). That fits transition shorthand (all 200ms), but transitionProperty values are property names, so custom idents like alloy or allocation can be reported even though they are not all.
Reviewed by Cursor Bugbot for commit c173c29. Configure here.
c173c29 to
7b59646
Compare
Derived from a cross-resource design reference (every principle from the design knowledge base), deduped against the full existing rule inventory. Motion: extend no-transition-all to the Tailwind `transition-all` class (was inline-only) and add no-tailwind-layout-transition for arbitrary `transition-[<layout-prop>]`. Accessibility: no-autoplay-without-muted, no-uninformative-aria-label, no-target-blank-without-rel, and no-low-contrast-inline-style (computes the real WCAG 2.1 ratio from co-located inline color + backgroundColor). Design/Tailwind hygiene: no-redundant-display-class, prefer-truncate-shorthand, no-full-viewport-width, and no-svg-currentcolor-with-fill-class. All register via codegen (388 rules); 77 co-located tests pass; typecheck, lint, and format clean.
- no-low-contrast-inline-style: when the inline style has no `fontSize`, the text may be sized large via a class (`text-5xl`), so fall back to the lenient 3:1 large-text threshold instead of 4.5:1 to avoid flagging large text that only needs 3:1. Stricter 4.5:1 applies only when the size is a visible normal size. - no-redundant-display-class: `<li>` defaults to `display: list-item`, not `block`, so `block` on an `<li>` is meaningful — removed `li` from the block-default set. Adds regression tests for both.
- no-low-contrast-inline-style: use a solid `background` shorthand color as the background (was skipped entirely, missing low-contrast pairs); skip only gradients/images/ambiguous (both backgroundColor + background). Also treat a string `fontWeight: "700"` as bold for the large-text threshold. - no-transition-all: match `transition-all` as a whole Tailwind token (segment after variant prefixes) so compound classes like `transition-all-custom` are not falsely flagged. - no-svg-currentcolor-with-fill-class: exclude stroke-WIDTH utilities (`stroke-2`, `stroke-[1.5]`) — they set thickness, not color — so they no longer false-conflict with `stroke="currentColor"`. Adds regression tests for each.
- no-deprecated-tailwind-class: flag Tailwind v4 renames (bg-gradient-* → bg-linear-*, flex-shrink-* → shrink-*, flex-grow-* → grow-*, overflow-ellipsis → text-ellipsis). Gated on a new `tailwind:4` capability. - no-arbitrary-px-font-size: `text-[13px]` → rem so text scales with the user's root font size (px stays fine for border-*/outline-*). - prefer-dvh-over-vh: `h-screen`/`min-h-screen`/`h-[100vh]` → dvh (tailwind:3.4). Adds `tailwind:4` capability to @react-doctor/core and extracts a shared `get-class-name-tokens` util (variant-aware token splitting), reused by no-transition-all and no-svg-currentcolor-with-fill-class.
- no-tailwind-layout-transition: match transition-[…] property names exactly (comma-split + set) so SVG `stroke-width` / `border-width` — which merely contain "width" — are no longer flagged as HTML layout thrash. - no-full-viewport-width: drop `max-w-*` / `maxWidth` (a defensive cap, not the overflow footgun); keep `w-screen`/`w-[100vw]`/`width`/`minWidth`. - prefer-dvh-over-vh: drop `max-h-*` / `maxHeight` (a valid height cap, e.g. a scrollable modal); keep `h-screen`/`min-h-screen`/`height`/`minHeight`. - no-deprecated-tailwind-class: only rename `bg-gradient-to-*` → `bg-linear-to-*` (v4 radial/conic are `bg-radial`/`bg-conic`, not `bg-linear-radial`). Adds regression tests for each false positive.
- no-svg-currentcolor-with-fill-class: only treat UNPREFIXED `fill-*`/`stroke-*`
color classes as conflicts; `hover:fill-blue-600` / `dark:fill-white` (state-
gated, no static conflict with the base `fill="currentColor"`) are no longer
flagged — a very common icon pattern.
- no-low-contrast-inline-style: reject 4-arg `rgb(0,0,0,0.5)` / slash-form alpha
(was judged opaque), and bail when the style object contains a `{...spread}`
that could override the colors. Tag `test-noise` to match sibling no-tiny-text.
- no-full-viewport-width: drop the redundant `category: "Architecture"` override
(it's the design-bucket default).
- Extract `has-jsx-spread-attribute` util; use it in the two a11y rules.
Adds regression tests for the variant-prefixed svg classes, rgb-alpha, opaque
rgb, and the style-spread bail.
Consolidate duplication surfaced by an adversarial deslop pass; no behavior change (the same diagnostics fire for the same inputs): - no-autoplay-without-muted: fold isStaticallyTrue/isStaticallyFalse into one tri-state resolveStaticBoolean, mirroring the existing parseJsxValue idiom - no-svg-currentcolor-with-fill-class: collapse the duplicated fill/stroke branches into a single loop over the two paint attributes - no-low-contrast-inline-style: hoist the repeated `properties` fallback - no-uncontrolled-input: drop the local hasJsxSpreadAttribute copy in favor of the branch's new shared utils/has-jsx-spread-attribute, completing the extraction Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`overflow-clip` is a current Tailwind utility for `overflow: clip` (v3.1+, still present in v4) — it was never renamed to `text-clip`, which sets the unrelated `text-overflow: clip`. The stray mapping produced an incorrect "renamed in v4" suggestion (Cursor Bugbot). Only `overflow-ellipsis -> text-ellipsis` is a real rename; keep that. Adds a regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d:4 gate `tailwind:4` gates no-deprecated-tailwind-class, which tells users a class was renamed/removed in v4. The optimistic-on-null policy (assume latest for an unparseable spec like `workspace:*`) is right for the suggestion rule behind `tailwind:3.4`, but for a deprecation rule it surfaces confidently-wrong "renamed in v4" warnings on a v3 project. Require a CONFIRMED parsed major >= 4 for tailwind:4 — favor a false negative over a false positive (Cursor Bugbot). `tailwind:3.4` stays optimistic. Adds an unparseable-version regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7b59646 to
74cf07b
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 4 total unresolved issues (including 3 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 74cf07b. Configure here.
| const value = token.slice(prefix.length); | ||
| if (value === "" || value === "current") return false; | ||
| if (/^\d/.test(value) || /^\[\d/.test(value)) return false; | ||
| return true; |
There was a problem hiding this comment.
Non-color fill stroke false positives
Medium Severity
hasColorUtility treats any unprefixed fill-* or stroke-* token whose suffix is not numeric as a color class. Tailwind utilities such as fill-opacity-*, fill-rule-*, stroke-linecap-*, stroke-linejoin-*, and stroke-dash* also match, so common SVG markup with fill="currentColor" or stroke="currentColor" plus those classes gets incorrect conflict warnings.
Reviewed by Cursor Bugbot for commit 74cf07b. Configure here.
RDE / OSS evaluation — 500 repos, 0 false positivesRan the
Notable real bugs caught
Confidence checks
Result: 0 false positives → no rule changes needed. For the rule-validate eval table: 500 repos scanned · ~2,562 target diagnostics · 0 FPs (validated ~25 hits across all firing rules + full-tag audit on the trickiest rule). |


Why
Adds 10 statically-lintable design rules derived from a cross-resource design knowledge base (anti-slop design languages, Tailwind/shadcn systems, motion sources, OKLCH/contrast, Apple HIG, jsx-a11y). Each was deduped against React Doctor's full existing inventory (not just
design/), so these fill real gaps — notably the Tailwind-class siblings of inline-only motion rules and a real WCAG-ratio contrast check.Before / after examples:
What changed
Motion
no-transition-all— extended to also flag the Tailwindtransition-allclass (was inline-style-only).no-tailwind-layout-transition— new: arbitrarytransition-[width|height|top|left|right|bottom|margin|padding].Accessibility
no-autoplay-without-muted—<video autoPlay>/<audio autoPlay>missingmuted(skips dynamic autoplay, spreads, truthy/dynamicmuted).no-uninformative-aria-label—aria-labelvalue that is a content-free element-type word ("icon","button","image", …).no-target-blank-without-rel—<a target="_blank">/<area>missingrel="noopener"/noreferrer(skips spreads + dynamicrel).no-low-contrast-inline-style— computes the real WCAG 2.1 ratio from co-located inlinecolor+backgroundColor; flags< 4.5:1(< 3:1large/bold). Sound-only: skips alpha,var(),transparent, gradients, unresolvable colors.Design / Tailwind hygiene
no-redundant-display-class—blockon<div>,inlineon<span>, etc. (skips variant-prefixed +flex/grid/hidden).prefer-truncate-shorthand—overflow-hidden text-ellipsis whitespace-nowrap→truncate.no-full-viewport-width—w-screen/w-[100vw]/ inline100vw→w-full.no-svg-currentcolor-with-fill-class—fill/stroke="currentColor"fighting afill-*/stroke-*class.Adds a reusable
get-wcag-contrast-ratioutil + WCAG constants; in-house a11y rules registered inRULES_NOT_PORTED_FROM_EXTERNALsocustomRulesOnlykeeps them. Registry regenerated via codegen (388 rules). Changeset included (patch).Validation
no-svg-currentcolor-with-fill-class) surfaced no matching pattern in the corpus — it's a narrow, unit-tested case. Notable real bugs caught: a white-on-white1.00:1contrast violation in a calcom email template, andno-redundant-display-classwas tag-audited across all 103 hits. The stricttailwind:4gate held —no-deprecated-tailwind-classfired only on confirmed-v4 repos.Test plan
pnpm --filter oxlint-plugin-react-doctor genpnpm exec vp test run(the 10 new*.test.ts+rule-registry.test.ts) — 77 passingpnpm --filter oxlint-plugin-react-doctor typecheckpnpm exec vp lint+pnpm exec vp fmton the new filesNote
Low Risk
Changes are additive static analysis with conservative skips and version-gated Tailwind rules; no production runtime or auth/data-path changes, only new warn-level diagnostics for consumers.
Overview
Adds 13 lint capabilities to
oxlint-plugin-react-doctor(plus an extension to an existing rule), wired through codegen-updatedrule-registryand a patch changeset across core + plugin + CLI.Motion / performance:
no-transition-allnow flags Tailwindtransition-all(not only inlinetransition/transitionProperty). Newno-tailwind-layout-transitioncatches arbitrarytransition-[…]on layout properties (width, height, margin, etc.).Accessibility: New rules for autoplay without
muted, uninformative staticaria-labelvalues,target="_blank"withoutnoopener/noreferrer, and WCAG 2.1 contrast on co-located inlinecolor+ background (with skips for spreads, alpha,var(), gradients, and ambiguousbackgroundshorthands). In-house a11y rules are listed inRULES_NOT_PORTED_FROM_EXTERNALsocustomRulesOnlystill ships them.Design / Tailwind hygiene: Rules for redundant default display classes,
truncateshorthand,100vw/w-screenoverflow, conflicting SVGcurrentColorvsfill-*/stroke-*classes, arbitrarytext-[Npx]font sizes,prefer-dvh-over-vh(requirestailwind:3.4), and Tailwind v4 renames viano-deprecated-tailwind-class(requires newtailwind:4capability).Core:
@react-doctor/coregainstailwind:4only when Tailwind major ≥ 4 is confirmed (stricter than optimistictailwind:3.4for unparseable versions), with tests. Shared helpers:get-class-name-tokens,get-wcag-contrast-ratio, WCAG constants indesign.ts, andhas-jsx-spread-attributecentralized (also used byno-uncontrolled-input).Reviewed by Cursor Bugbot for commit 74cf07b. Bugbot is set up for automated code reviews on this repo. Configure here.