Accept a list of token values in the guard, and quote what was written
A transition-duration or -timing-function listing one token per property (a spatial spring for scale, an effects spring for opacity) was reported as literal: blanking each var() left a bare comma, which is not one of the safe values. Every item of a list is now checked on its own. A message also quoted the blanked shell, `var( ), 200ms`; it now quotes the declaration as written. SealShare's drop zone found it (plan step 46). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f307079a45
commit
d557dd9b49
+23
-11
@@ -859,7 +859,7 @@ class DesignGuard
|
|||||||
|
|
||||||
foreach ([
|
foreach ([
|
||||||
...$this->literalColours($css),
|
...$this->literalColours($css),
|
||||||
...$this->literalDeclarations($css),
|
...$this->literalDeclarations($css, $masked),
|
||||||
...$this->offScaleMediaQueries($css),
|
...$this->offScaleMediaQueries($css),
|
||||||
] as [$line, $what]) {
|
] as [$line, $what]) {
|
||||||
$violations[] = "{$where}:{$line} {$what}";
|
$violations[] = "{$where}:{$line} {$what}";
|
||||||
@@ -913,23 +913,26 @@ class DesignGuard
|
|||||||
* Every literal value on a property check (iii) knows, plus a `transition`/`animation`
|
* Every literal value on a property check (iii) knows, plus a `transition`/`animation`
|
||||||
* shorthand's embedded easing or duration (its property name alone cannot say which part of
|
* shorthand's embedded easing or duration (its property name alone cannot say which part of
|
||||||
* the value is which, so both are searched for whenever either is written literally). A
|
* the value is which, so both are searched for whenever either is written literally). A
|
||||||
* custom property is never one of them, whatever its name says.
|
* custom property is never one of them, whatever its name says. `$css` has its token functions
|
||||||
|
* blanked; a message quotes the declaration as written in `$source`, the same CSS before that
|
||||||
|
* (blanking keeps every offset).
|
||||||
*
|
*
|
||||||
* @return list<array{0: int, 1: string}>
|
* @return list<array{0: int, 1: string}>
|
||||||
*/
|
*/
|
||||||
protected function literalDeclarations(string $css): array
|
protected function literalDeclarations(string $css, ?string $source = null): array
|
||||||
{
|
{
|
||||||
$found = [];
|
$found = [];
|
||||||
|
|
||||||
foreach ($this->declarations($css) as [$property, $value, $offset]) {
|
foreach ($this->declarations($css) as [$property, $blanked, $offset]) {
|
||||||
$value = trim($value);
|
$value = trim($blanked);
|
||||||
|
$written = trim(substr($source ?? $css, $offset, strlen($blanked)));
|
||||||
$line = substr_count(substr($css, 0, $offset), "\n") + 1;
|
$line = substr_count(substr($css, 0, $offset), "\n") + 1;
|
||||||
|
|
||||||
$isRing = $property === 'box-shadow' && $this->isSafeBoxShadowRing($value);
|
$isRing = $property === 'box-shadow' && $this->isSafeBoxShadowRing($value);
|
||||||
|
|
||||||
if (isset(self::LITERAL_PROPERTIES[$property]) && ! $this->isSafeLiteralValue($value) && ! $isRing) {
|
if (isset(self::LITERAL_PROPERTIES[$property]) && ! $this->isSafeLiteralValue($value) && ! $isRing) {
|
||||||
$config = self::LITERAL_PROPERTIES[$property];
|
$config = self::LITERAL_PROPERTIES[$property];
|
||||||
$found[] = [$line, "literal {$config['kind']} `{$property}: {$value}` — {$config['hint']}"];
|
$found[] = [$line, "literal {$config['kind']} `{$property}: {$written}` — {$config['hint']}"];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! in_array($property, ['transition', 'animation'], true)) {
|
if (! in_array($property, ['transition', 'animation'], true)) {
|
||||||
@@ -941,13 +944,13 @@ class DesignGuard
|
|||||||
// unlike `ease`/`ease-in`/`ease-out`/`ease-in-out`, a spring's own shape and never
|
// unlike `ease`/`ease-in`/`ease-out`/`ease-in-out`, a spring's own shape and never
|
||||||
// written by hand, or `cubic-bezier()`/`steps()`, always a hand-rolled curve.
|
// written by hand, or `cubic-bezier()`/`steps()`, always a hand-rolled curve.
|
||||||
if (preg_match('/cubic-bezier\(|steps\(|(?<![\w-])(?:ease(?:-in-out|-in|-out)?|step-start|step-end)(?![\w-])/i', $value) === 1) {
|
if (preg_match('/cubic-bezier\(|steps\(|(?<![\w-])(?:ease(?:-in-out|-in|-out)?|step-start|step-end)(?![\w-])/i', $value) === 1) {
|
||||||
$found[] = [$line, "literal easing in `{$property}: {$value}` — use `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)`, paired with its `-duration`"];
|
$found[] = [$line, "literal easing in `{$property}: {$written}` — use `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)`, paired with its `-duration`"];
|
||||||
}
|
}
|
||||||
|
|
||||||
preg_match_all('/(?<![\w.-])(\d*\.?\d+)m?s(?![\w-])/i', $value, $durations);
|
preg_match_all('/(?<![\w.-])(\d*\.?\d+)m?s(?![\w-])/i', $value, $durations);
|
||||||
|
|
||||||
if (array_filter($durations[1], fn (string $number): bool => (float) $number > 0) !== []) {
|
if (array_filter($durations[1], fn (string $number): bool => (float) $number > 0) !== []) {
|
||||||
$found[] = [$line, "literal duration in `{$property}: {$value}` — use `var(--md-sys-motion-…-duration)`, paired with its easing"];
|
$found[] = [$line, "literal duration in `{$property}: {$written}` — use `var(--md-sys-motion-…-duration)`, paired with its easing"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -957,13 +960,22 @@ class DesignGuard
|
|||||||
/**
|
/**
|
||||||
* A value `withoutTokenFunctions()` left with nothing but the empty shell of a token function
|
* A value `withoutTokenFunctions()` left with nothing but the empty shell of a token function
|
||||||
* — whitespace where the call's own argument list used to be, the call's own name and
|
* — whitespace where the call's own argument list used to be, the call's own name and
|
||||||
* parentheses still standing — is exactly as fine as one masked away entirely.
|
* parentheses still standing — is exactly as fine as one masked away entirely. A list of values
|
||||||
|
* (`transition-duration: var(…), var(…)`) is fine when every item in it is.
|
||||||
*/
|
*/
|
||||||
protected function isSafeLiteralValue(string $value): bool
|
protected function isSafeLiteralValue(string $value): bool
|
||||||
{
|
{
|
||||||
$value = trim((string) preg_replace('/\b(?:var|calc|min|max|clamp)\(\s*\)/i', '', $value));
|
$value = (string) preg_replace('/\b(?:var|calc|min|max|clamp)\(\s*\)/i', '', $value);
|
||||||
|
|
||||||
return $value === '' || in_array(strtolower($value), ['0', '0px', '0s', '0ms', 'none', 'inherit', 'initial', 'unset', 'revert', 'normal', 'auto'], true);
|
foreach (explode(',', $value) as $item) {
|
||||||
|
$item = trim($item);
|
||||||
|
|
||||||
|
if ($item !== '' && ! in_array(strtolower($item), ['0', '0px', '0s', '0ms', 'none', 'inherit', 'initial', 'unset', 'revert', 'normal', 'auto'], true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -553,6 +553,15 @@ it('finds every literal design value and off-scale media query in the applicatio
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('accepts a list of values when every item is a token, and quotes a mixed one as written', function () {
|
||||||
|
// SealShare's drop zone pairs a spatial spring for scale with an effects spring for opacity in one
|
||||||
|
// transition-duration list; blanking both var()s used to leave a bare comma the guard called literal.
|
||||||
|
expect(fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/css-lists/lists.css'))->violations()))->toBe([
|
||||||
|
'css-lists/lists.css:8 literal duration `transition-duration: var(--md-sys-motion-spatial-slow-duration), 200ms` — use `var(--md-sys-motion-…-duration)`, paired with its easing',
|
||||||
|
'css-lists/lists.css:9 literal easing `transition-timing-function: var(--md-sys-motion-spatial-slow), ease-out` — use `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)`, paired with its `-duration`',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('leaves a box-shadow ring in a colour role alone, but still reports a blurred or literal-coloured one', function () {
|
it('leaves a box-shadow ring in a colour role alone, but still reports a blurred or literal-coloured one', function () {
|
||||||
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/shadows/rings.css'))->violations());
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/shadows/rings.css'))->violations());
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/* A list of values, each from a token, is fine; one literal in the list is not. */
|
||||||
|
.fine-list {
|
||||||
|
transition-duration: var(--md-sys-motion-spatial-slow-duration), var(--md-sys-motion-effects-slow-duration);
|
||||||
|
transition-timing-function: var(--md-sys-motion-spatial-slow), var(--md-sys-motion-effects-slow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mixed-list {
|
||||||
|
transition-duration: var(--md-sys-motion-spatial-slow-duration), 200ms;
|
||||||
|
transition-timing-function: var(--md-sys-motion-spatial-slow), ease-out;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user