feat(enforcer): Tailwind v4 utilities (bg-primary, text-muted-foreground/80) count as token references

verify_code flagged every shadcn/Tailwind screen as hard-coded because it only looked for
var(--name) and --color-name in the code text.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Marcos Paulo
2026-10-25 17:42:43 -03:00
parent a0be70d5a6
commit 033ae23b7c
2 changed files with 52 additions and 1 deletions
@@ -64,3 +64,16 @@
[ 321743ms] [LOG] [Fast Refresh] done in 3ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 333352ms] [LOG] [Fast Refresh] done in 3ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 369410ms] [LOG] [Fast Refresh] done in 3ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 413886ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 414902ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 415904ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 416906ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 417908ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 418911ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 423918ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 428928ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 433950ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 438984ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 444022ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 449184ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 454479ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
+39 -1
View File
@@ -125,7 +125,28 @@ fn web_var_referenced(code: &str, var_name: &str) -> bool {
let outside = strip_token_declaration_blocks(code);
let css_var = format!("--{var_name}");
let tw_color_var = format!("--color-{var_name}");
outside.contains(&css_var) || outside.contains(&tw_color_var)
outside.contains(&css_var) || outside.contains(&tw_color_var) || tailwind_utility_references(&outside, var_name)
}
/// `bg-primary`, `text-muted-foreground/80`, `hover:border-ring`: Tailwind v4 utilities generated
/// from `@theme` colour tokens reference the variable without spelling `--name`. A hit needs a
/// utility prefix letter before `-name` and no identifier char after it (`bg-primary-foreground`
/// is not a reference to `primary`).
fn tailwind_utility_references(code: &str, var_name: &str) -> bool {
let needle = format!("-{var_name}");
let bytes = code.as_bytes();
let mut from = 0;
while let Some(pos) = code[from..].find(&needle) {
let start = from + pos;
let end = start + needle.len();
let before_ok = start > 0 && bytes[start - 1].is_ascii_alphabetic();
let after_ok = end >= bytes.len() || !(bytes[end].is_ascii_alphanumeric() || bytes[end] == b'-' || bytes[end] == b'_');
if before_ok && after_ok {
return true;
}
from = end;
}
false
}
fn count_web_absolute(code: &str) -> usize {
@@ -745,3 +766,20 @@ mod tests {
assert!(rules.contains(&"spacing-drift"), "{rules:?}");
}
}
#[cfg(test)]
mod tailwind_reference_tests {
use super::*;
#[test]
fn tailwind_v4_utilities_count_as_variable_references() {
assert!(web_var_referenced(r#"<div className="bg-background text-foreground">"#, "background"));
assert!(web_var_referenced(r#"className="hover:text-muted-foreground/80""#, "muted-foreground"));
assert!(web_var_referenced(r#"className="border-ring""#, "ring"));
// `bg-primary-foreground` references primary-foreground, not primary.
assert!(!web_var_referenced(r#"className="bg-primary-foreground""#, "primary"));
// A word that merely ends with the name is not a utility.
assert!(!web_var_referenced("const x = 'anything-ring_'", "ring"));
assert!(web_var_referenced("style={{ color: 'var(--primary)' }}", "primary"));
}
}