mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-15 15:46:30 +03:00
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:
@@ -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) {
|
||||
|
||||
@@ -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)']],
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user