Retarget forbidColours() at where 2.0.0 writes a role

Plan step 41 review (the user, 2026-09-15): an application without Tailwind
names a role through its --md-sys-color-* custom property (CSS, an inline
style, a script), an md-ink-* class, or a component's color/tone prop, not
a bg-tertiary utility. forbidColours() keeps its name and signature and
reports each of those, the role's on- and container roles included;
a Tailwind leftover stays family (i)'s single report. ReStride's
forbidColours(['tertiary', 'primary-container']) keeps its meaning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-15 08:35:06 +02:00
co-authored by Claude Opus 5
parent 6dc996afdf
commit d5972b8ddb
6 changed files with 126 additions and 13 deletions
+85 -7
View File
@@ -239,7 +239,11 @@ class DesignGuard
/**
* Roles the application's own rules leave out, e.g. ['tertiary', 'primary-container'].
* Their on-roles and containers are forbidden with them.
* Their on-roles and containers are forbidden with them, wherever 2.0.0 lets an application
* write one: the `--md-sys-color-*` custom property (a `var()` in its CSS, an inline `style`, a
* script reading it), the `md-ink-*` class text.css has for it, and a component's `color` or
* `tone` prop (`color="tertiary"`, `:tone="'tertiary'"`). A Tailwind `bg-tertiary` left behind
* is family (i)'s to report, as every colour utility is.
*
* @param list<string> $roles
*/
@@ -325,6 +329,10 @@ class DesignGuard
$violations[] = "{$where}:{$line} Blade directive `{$directive}` inside a component tag, where it does not compile";
}
foreach ($this->forbiddenRoleProps($contents) as [$line, $what]) {
$violations[] = "{$where}:{$line} {$what}";
}
if ($this->cssEntry !== null) {
foreach ($this->missingStylesheetViolations($contents, $resolved) as [$line, $what]) {
$violations[] = "{$where}:{$line} {$what}";
@@ -341,7 +349,7 @@ class DesignGuard
}
}
foreach ($this->offTheTokens($text) as $what) {
foreach ([...$this->offTheTokens($text), ...$this->forbiddenRoles($text)] as $what) {
$violations[] = "{$where}:{$line} {$what}";
}
@@ -376,14 +384,77 @@ class DesignGuard
protected function colourPattern(): string
{
$names = [self::PALETTE];
return '/(?<![\w-])'.self::UTILITY.'-'.self::PALETTE.'(?![\w-])/';
}
/**
* Every place one line names a role `forbidColours()` left out: its `--md-sys-color-*` custom
* property, or the `md-ink-*` class text.css draws it with.
*
* @return list<string>
*/
protected function forbiddenRoles(string $text): array
{
$found = [];
foreach ($this->forbiddenColours as $role) {
$role = preg_quote($role, '/');
$names[] = "(?:on-)?{$role}(?:-container)?";
$names = ['--md-sys-color-(?:on-)?'.preg_quote($role, '/').'(?:-container)?'];
foreach ([$role, "on-{$role}", "{$role}-container", "on-{$role}-container"] as $variant) {
if (isset(self::INK_ROLE[$variant])) {
$names[] = preg_quote(self::INK_ROLE[$variant], '/');
}
}
preg_match_all('/(?<![\w-])(?:'.implode('|', $names).')(?![\w-])/', $text, $matches);
foreach ($matches[0] as $written) {
$found[] = "`{$written}`: role `{$role}` is not part of this application's palette";
}
}
return '/(?<![\w-])'.self::UTILITY.'-(?:'.implode('|', $names).')(?![\w-])/';
return $found;
}
/**
* Every component `color` or `tone` prop in `$contents` naming a role `forbidColours()` left
* out, literal (`color="tertiary"`) or any string a bound one can take
* (`:tone="$failed ? 'error' : 'info'"`).
*
* @return list<array{0: int, 1: string}>
*/
protected function forbiddenRoleProps(string $contents): array
{
if ($this->forbiddenColours === []) {
return [];
}
preg_match_all('/<x-[\w.:-]+((?:[^>"]|"[^"]*")*)>/s', $contents, $tags, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
$found = [];
foreach ($tags as $tag) {
preg_match_all('/\s(?<bound>:?)(?:color|tone)="(?<value>[^"]*)"/', $tag[1][0], $props, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
foreach ($props as $prop) {
$values = $prop['bound'][0] === ':'
? (preg_match_all("/'([^']*)'/", $prop['value'][0], $strings) ? $strings[1] : [])
: [$prop['value'][0]];
foreach ($values as $value) {
foreach ($this->forbiddenColours as $role) {
if (preg_match('/^(?:on-)?'.preg_quote($role, '/').'(?:-container)?$/', $value) === 1) {
$found[] = [
substr_count(substr($contents, 0, $tag[1][1] + $prop[0][1]), "\n") + 1,
'`'.trim($prop[0][0])."`: role `{$role}` is not part of this application's palette",
];
}
}
}
}
}
return $found;
}
/**
@@ -592,9 +663,16 @@ class DesignGuard
$violations = [];
foreach ($this->applicationStylesheets() as $file) {
$css = $this->withoutTokenFunctions($this->maskedCss((string) file_get_contents($file)));
$masked = $this->maskedCss((string) file_get_contents($file));
$css = $this->withoutTokenFunctions($masked);
$where = $this->relative($file);
foreach (explode("\n", $masked) as $index => $text) {
foreach ($this->forbiddenRoles($text) as $what) {
$violations[] = "{$where}:".($index + 1)." {$what}";
}
}
foreach ([
...$this->literalColours($css),
...$this->literalDeclarations($css),