From 428b86b1393846afd4e291fa7acd3a70499682f1 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Sat, 18 Jul 2026 15:25:29 -0700 Subject: [PATCH] Detect single-edge stripes painted with an inset box-shadow (#378) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Detect single-edge stripes painted with an inset box-shadow The side-tab rule caught bordered stripes but not the inset box-shadow spelling of the same anti-pattern, which is how it usually reaches an Astro/CSS source file. Adds a structural CSS scan for `box-shadow: inset` layers whose shape is a 3-12px stripe on exactly one edge with no blur or spread, reusing the existing `side-tab` rule id, so the rule count is unchanged. Scoped narrowly, because a stripe is correct design in some places. It skips selection and focus indicators (the rule's one documented exception), interactive and semantic elements, narrow artwork, and neutral colors: `inset 4px 0 0 #000` is a hairline, not an AI tell. Chromatic intent is read from the color literal or from a `var(--token)` name. Grammar rather than one spelling, learned the hard way — three of the four false-negative shapes below were found only after the first pass shipped: - `inset` is order-independent, so `4px 0 0 red inset` is the same stripe. Only a standalone keyword is stripped, so `var(--inset-accent)` is not mangled. - box-shadow takes {2,4}: `inset 4px 0 red` omits blur and spread, which default to 0. That is exactly the stripe shape. - Authored CSS spells neutrals as `#000` / `black`, and shared/color.mjs only parses the computed function forms a browser emits, deliberately reporting anything else as chromatic. Routing authored colors through it flagged plain black hairlines, so hex and named neutrals are handled before deferring. - Comment bodies are blanked before matching, preserving byte offsets so line numbers stay right, and the selector's line is taken from its first non-whitespace character rather than the greedy match start. Fixture covers 8 flag shapes and 13 pass shapes, including a literal-color column that the original had none of, which is why the neutral bug survived review. Prepared with AI assistance under maintainer direction. Co-Authored-By: Claude * Parse box-shadow layers by grammar, not by one spelling Three review-bot findings, two of them the same mistake I had already made twice in this rule. Color-first layers were missed (greptile). `box-shadow` orders `inset`, the lengths, and the color freely, so `red 4px 0 inset` and `var(--brand-accent) 4px 0 0 inset` paint the stripe the length-first regex was looking for and were skipped. That is the third valid spelling this rule has missed after trailing `inset` and the two-length form, all from encoding one spelling instead of the grammar. Stop patching spellings: tokenize the layer, pick out `inset` and the 2-4 lengths in any order, and treat the single remaining token as the color. Tokenizing is paren-aware because `rgb(0 0 0)` is one color value whose channels would otherwise read as lengths. Neutral `rgb()` with space-separated channels was flagged (cursor). shared/color.mjs parses only the comma form that getComputedStyle emits, so an authored `rgb(0 0 0)` fell through it and reported chromatic — the exemption isNeutralAuthoredColor exists for, missed. Parse both separators before delegating. Left shared/color.mjs alone: it reads computed styles, where the comma form is all a browser produces. Line numbers were derived by re-slicing the whole prefix per rule, O(n^2) on a large stylesheet (Copilot). Matches arrive in source order, so carry a monotonic cursor: one pass total. Fixtures cover both flag shapes and the neutral pass shape; all three fail against the previous parse ("expected Color First Edge to flag", and Space Rgb Neutral Edge appearing in the old flag list). Assisted-by: Claude Code * Fix the !important regression my tokenizer introduced, plus two cascade bugs Three findings from Cursor on the grammar rewrite. The first is mine, from the commit that claimed to end this bug class. `!important` stopped flagging. Tokenizing split it into its own token, so the color count came out at two and the layer was skipped — a shape the regex it replaced handled correctly. `!important` qualifies the declaration, not the shadow value, so strip it before reading layers. Style-block findings reported one line low. block.startLine is the first line after the