From 94e957d7fc7a98f6a165e5db0ea8ae216875ba01 Mon Sep 17 00:00:00 2001 From: Abdul Wahab Date: Mon, 10 Aug 2026 13:05:24 +0500 Subject: [PATCH] Keep shadow context across template interpolations Review finding on #553: the end-anchored shadow-context tails excluded `}` (JS) and `{`/`}` (CSS), so a documented shadow color after a ${...} interpolation in a boxShadow template literal or a CSS-in-JS box-shadow line lost its allowance and fired as drift. Both tails now admit complete ${...} interpolations; a bare `}`, quote, or `;` still ends the context, so the allowance cannot leak past a template's closing backtick into a later property. AI-assisted (Cursor agent), reviewed by maintainer. Co-authored-by: Cursor --- cli/engine/design-system.mjs | 9 ++++++--- tests/design-system.test.mjs | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/cli/engine/design-system.mjs b/cli/engine/design-system.mjs index 87320dbe5..511329c5c 100644 --- a/cli/engine/design-system.mjs +++ b/cli/engine/design-system.mjs @@ -742,9 +742,12 @@ function isShadowPropertyContext(line, match) { const before = line.slice(0, index); // Unlike jsColorKeyContext, the JS tail admits commas: a multi-layer shadow // string is comma-separated, and a later property on the same line is still - // blocked because it sits past the string's closing quote. - return /(?:^|[{\s;"'`(,])(?:box-shadow|text-shadow)\s*:\s*[^;{}"'`]*$/i.test(before) - || /(?:^|[,{]\s*)(?:boxShadow|textShadow)\s*[:=]\s*["'`]?[^"'`}]*$/i.test(before); + // blocked because it sits past the string's closing quote. Both tails also + // admit complete `${...}` interpolations, so a tokenized dynamic shadow + // (template literal or CSS-in-JS) keeps its context; a bare `}`, quote, or + // `;` still ends it. + return /(?:^|[{\s;"'`(,])(?:box-shadow|text-shadow)\s*:\s*(?:\$\{[^}"'`]*\}|[^;{}"'`])*$/i.test(before) + || /(?:^|[,{]\s*)(?:boxShadow|textShadow)\s*[:=]\s*["'`]?(?:\$\{[^}"'`]*\}|[^"'`}])*$/i.test(before); } function isInsideCssAttributeSelector(line, index) { diff --git a/tests/design-system.test.mjs b/tests/design-system.test.mjs index 6dc3bcffa..c9bddd8c6 100644 --- a/tests/design-system.test.mjs +++ b/tests/design-system.test.mjs @@ -567,6 +567,25 @@ const leak = { boxShadow: "0 1px 2px rgba(0, 0, 0, 0.28)", color: "rgba(0, 0, 0, assert.equal(findings.some((item) => item.antipattern === 'design-system-color'), false); }); + it('keeps shadow context across template interpolations', () => { + const designSystem = shadowDesignSystem(); + const findings = checkSourceDesignSystem(` +const card = { boxShadow: \`0 \${offset}px 2px rgba(0, 0, 0, 0.28)\` }; + box-shadow: 0 1px \${blur}px rgba(0, 0, 0, 0.28); +const leak = { boxShadow: \`0 \${offset}px rgba(0, 0, 0, 0.28)\`, color: "rgba(0, 0, 0, 0.28)" }; +`, '/tmp/interpolated.js', { designSystem }); + const colors = findings.filter((item) => item.antipattern === 'design-system-color'); + + // The documented shadow color passes after a \${...} interpolation in + // both the JS template literal and the CSS-in-JS line; the color key on + // the leak line still fires because it sits past the template's closing + // backtick. + assert.deepEqual( + colors.map((item) => [item.line, item.ignoreValue]), + [[4, 'rgba(0, 0, 0, 0.28)']], + ); + }); + it('does not allow a later declaration to inherit shadow context from earlier on the line', () => { const designSystem = shadowDesignSystem(); const findings = checkSourceDesignSystem(