From 033ae23b7c554296f6106ade25f7b208645ea5d3 Mon Sep 17 00:00:00 2001 From: Marcos Paulo Date: Sun, 25 Oct 2026 17:42:43 -0300 Subject: [PATCH] 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 --- .../console-2026-10-25T20-33-32-647Z.log | 13 ++++++ src/mcp/enforcer.rs | 40 ++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/.playwright-mcp/console-2026-10-25T20-33-32-647Z.log b/.playwright-mcp/console-2026-10-25T20-33-32-647Z.log index 1b06834..2a71928 100644 --- a/.playwright-mcp/console-2026-10-25T20-33-32-647Z.log +++ b/.playwright-mcp/console-2026-10-25T20-33-32-647Z.log @@ -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 diff --git a/src/mcp/enforcer.rs b/src/mcp/enforcer.rs index 529268e..812d78c 100644 --- a/src/mcp/enforcer.rs +++ b/src/mcp/enforcer.rs @@ -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#"
"#, "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")); + } +}