Admit one brace level inside shadow interpolations

Review finding on #553: an object-literal argument like
${getOffset({ size: 2 })} ended the interpolation match at the inner
closing brace, losing the shadow context. Interpolations now admit one
level of braces (with paired quotes inside); the shared subpattern is
hoisted into compiled constants. Deeper nesting stays fail-safe by
design: a line-scoped regex cannot balance arbitrary braces, and the
miss produces a waivable finding, never a leak.

AI-assisted (Cursor agent), reviewed by maintainer.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Abdul Wahab
2026-08-10 13:28:11 +05:00
co-authored by Cursor
parent 82234515e0
commit 520a55547e
2 changed files with 28 additions and 13 deletions
+22 -9
View File
@@ -730,6 +730,27 @@ function isProbablyColorLiteral(line, match) {
return styleContext || cssFunctionContext || jsColorKeyContext;
}
// One complete `${...}` template interpolation. Its content may carry paired
// quoted strings (function arguments, ternary branches) and one level of
// braces (an object-literal argument, itself allowing paired quotes). Deeper
// nesting would need a parser, so the regex deliberately fails safe there:
// the context check misses and the finding fires — a false positive a waiver
// can silence, never a leak.
const QUOTED_STRING_SRC = `"[^"]*"|'[^']*'`;
const INTERPOLATION_SRC =
`\\$\\{(?:${QUOTED_STRING_SRC}|\\{(?:${QUOTED_STRING_SRC}|[^{}"'\`])*\\}|[^{}"'\`])*\\}`;
// The two shadow-context tails. 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. Both tails admit complete interpolations; a bare
// `}`, quote, or `;` still ends the context.
const SHADOW_CSS_CONTEXT_RE = new RegExp(
`(?:^|[{\\s;"'\`(,])(?:box-shadow|text-shadow)\\s*:\\s*(?:${INTERPOLATION_SRC}|[^;{}"'\`])*$`, 'i',
);
const SHADOW_JS_CONTEXT_RE = new RegExp(
`(?:^|[,{]\\s*)(?:boxShadow|textShadow)\\s*[:=]\\s*["'\`]?(?:${INTERPOLATION_SRC}|[^"'\`}])*$`, 'i',
);
// True when the color literal sits inside a box-shadow / text-shadow value —
// the only contexts where a documented shadow color is legal. Anchored to the
// end of `before` (no ; } { or quote in between) so a shadow property earlier
@@ -740,15 +761,7 @@ function isShadowPropertyContext(line, match) {
const index = match.index ?? -1;
if (index < 0) return false;
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. Both tails also
// admit complete `${...}` interpolations (including paired quoted strings
// inside them, for function arguments and ternaries), 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);
return SHADOW_CSS_CONTEXT_RE.test(before) || SHADOW_JS_CONTEXT_RE.test(before);
}
function isInsideCssAttributeSelector(line, index) {
+6 -4
View File
@@ -575,18 +575,20 @@ const card = { boxShadow: \`0 \${offset}px 2px rgba(0, 0, 0, 0.28)\` };
const fn = { boxShadow: \`0 \${getShadow('lg')} 2px rgba(0, 0, 0, 0.28)\` };
const tern = { boxShadow: \`0 1px \${dark ? "4px" : "2px"} rgba(0, 0, 0, 0.28)\` };
box-shadow: 0 \${theme('blur')} rgba(0, 0, 0, 0.28);
const nested = { boxShadow: \`0 \${getOffset({ size: 2 })}px 2px rgba(0, 0, 0, 0.28)\` };
const nestedQ = { boxShadow: \`0 \${getOffset({ size: 'lg' })}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
// the JS template literal and the CSS-in-JS line, including
// interpolations carrying quoted function arguments or ternary branches;
// the color key on the leak line still fires because it sits past the
// template's closing backtick.
// interpolations carrying quoted function arguments, ternary branches,
// and one level of object-literal braces; 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]),
[[7, 'rgba(0, 0, 0, 0.28)']],
[[9, 'rgba(0, 0, 0, 0.28)']],
);
});