Skip to content

suggestCanonicalClasses false positive: warns to replace *:[[role=checkbox]]:translate-y-[2px] with itself #1562

@5m1Ly

Description

@5m1Ly

What version of VS Code are you using?

$ code --version
1.114.0
e7fb5e96c0730b9deb70b33781f98e2f35975036
x64

What version of Tailwind CSS IntelliSense are you using?

extentions > Tailwind CSS IntelliSence > Uninstall Dropdown > Install Specific Version...

Image

What version of Tailwind CSS are you using?

$ pnpm list tailwindcss
Legend: production dependency, optional only, dev only

@pia-development/solutions@0.9.1 /home/sm1ly/workspace/projects/web/solutions (PRIVATE)
│
│   devDependencies:
└── tailwindcss@4.2.2

1 package

What package manager are you using?

pnpm

What operating system are you using?

Linux

Tailwind CSS Stylesheet (v4) or config file (v3)

path: /tailwind.config.mts

import typography from '@tailwindcss/typography';
import scrollbar from 'tailwind-scrollbar';
import colors from 'tailwindcss/colors';
import type { Config } from 'tailwindcss';

export default {
  content: ['./src/**/*.{js,ts,jsx,tsx}'],
  theme: {
    extend: {
      colors: {
        // Use neutral gray instead of blue-tinted default gray
        gray: colors.neutral,
      },
      keyframes: {
        blob: {
          '0%, 100%': { transform: 'translate(0px, 0px) scale(1)' },
          '33%': { transform: 'translate(30px, -50px) scale(1.1)' },
          '66%': { transform: 'translate(-20px, 20px) scale(0.9)' },
        },
      },
      animation: { blob: 'blob 7s infinite' },
    },
  },
  darkMode: 'class',
  plugins: [scrollbar({ nocompatible: true }), typography()],
} satisfies Config;

path: /src/app/globals.css

@import "tailwindcss";
@import "tw-animate-css";
@import "@heroui/styles";

@custom-variant dark (&:is(.dark *));
@config "../../tailwind.config.mts";

@layer base {
  :root {
    --sidebar-background: 0 0% 98%;
    --sidebar-foreground: 240 5.3% 26.1%;
    --sidebar-primary: 240 5.9% 10%;
    --sidebar-primary-foreground: 0 0% 98%;
    --sidebar-accent: 240 4.8% 95.9%;
    --sidebar-accent-foreground: 240 5.9% 10%;
    --sidebar-border: 220 13% 91%;
    --sidebar-ring: 217.2 91.2% 59.8%;
  }
}

:root {
  --foreground-rgb: 0, 0, 0;
  --background-start-rgb: 214, 219, 220;
  --background-end-rgb: 255, 255, 255;
  --radius: 0.625rem;
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);
  --popover: oklch(1 0 0);
  --popover-foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  --secondary: oklch(0.97 0 0);
  --secondary-foreground: oklch(0.205 0 0);
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);
  --accent: oklch(0.97 0 0);
  --accent-foreground: oklch(0.205 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);
  --chart-1: oklch(0.646 0.222 41.116);
  --chart-2: oklch(0.6 0.118 184.704);
  --chart-3: oklch(0.398 0.07 227.392);
  --chart-4: oklch(0.828 0.189 84.429);
  --chart-5: oklch(0.769 0.188 70.08);
  --sidebar: oklch(0.985 0 0);
  --sidebar-foreground: oklch(0.145 0 0);
  --sidebar-primary: oklch(0.205 0 0);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.97 0 0);
  --sidebar-accent-foreground: oklch(0.205 0 0);
  --sidebar-border: oklch(0.922 0 0);
  --sidebar-ring: oklch(0.708 0 0);
}

@media (prefers-color-scheme: dark) {
  :root {
    --foreground-rgb: 255, 255, 255;
    --background-start-rgb: 0, 0, 0;
    --background-end-rgb: 0, 0, 0;
  }
}

@layer base {
  .dark {
    --sidebar-background: 240 5.9% 10%;
    --sidebar-foreground: 240 4.8% 95.9%;
    --sidebar-primary: 224.3 76.3% 48%;
    --sidebar-primary-foreground: 0 0% 100%;
    --sidebar-accent: 240 3.7% 15.9%;
    --sidebar-accent-foreground: 240 4.8% 95.9%;
    --sidebar-border: 240 3.7% 15.9%;
    --sidebar-ring: 217.2 91.2% 59.8%;
  }
}

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --card: oklch(0.205 0 0);
  --card-foreground: oklch(0.985 0 0);
  --popover: oklch(0.205 0 0);
  --popover-foreground: oklch(0.985 0 0);
  --primary: oklch(0.922 0 0);
  --primary-foreground: oklch(0.205 0 0);
  --secondary: oklch(0.269 0 0);
  --secondary-foreground: oklch(0.985 0 0);
  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);
  --accent: oklch(0.269 0 0);
  --accent-foreground: oklch(0.985 0 0);
  --destructive: oklch(0.704 0.191 22.216);
  --border: oklch(1 0 0 / 10%);
  --input: oklch(1 0 0 / 15%);
  --ring: oklch(0.556 0 0);
  --chart-1: oklch(0.488 0.243 264.376);
  --chart-2: oklch(0.696 0.17 162.48);
  --chart-3: oklch(0.769 0.188 70.08);
  --chart-4: oklch(0.627 0.265 303.9);
  --chart-5: oklch(0.645 0.246 16.439);
  --sidebar: oklch(0.275 0 0);
  --sidebar-foreground: oklch(0.985 0 0);
  --sidebar-primary: oklch(0.488 0.243 264.376);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.358 0 0);
  --sidebar-accent-foreground: oklch(0.985 0 0);
  --sidebar-border: oklch(1 0 0 / 50%);
  --sidebar-ring: oklch(0.656 0 0);
}

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --font-sans: var(--font-geist-sans);
  --font-mono: var(--font-geist-mono);
  --color-sidebar-ring: var(--sidebar-ring);
  --color-sidebar-border: var(--sidebar-border);
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
  --color-sidebar-accent: var(--sidebar-accent);
  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
  --color-sidebar-primary: var(--sidebar-primary);
  --color-sidebar-foreground: var(--sidebar-foreground);
  --color-sidebar: var(--sidebar);
  --color-chart-5: var(--chart-5);
  --color-chart-4: var(--chart-4);
  --color-chart-3: var(--chart-3);
  --color-chart-2: var(--chart-2);
  --color-chart-1: var(--chart-1);
  --color-ring: var(--ring);
  --color-input: var(--input);
  --color-border: var(--border);
  --color-destructive: var(--destructive);
  --color-accent-foreground: var(--accent-foreground);
  --color-accent: var(--accent);
  --color-muted-foreground: var(--muted-foreground);
  --color-muted: var(--muted);
  --color-secondary-foreground: var(--secondary-foreground);
  --color-secondary: var(--secondary);
  --color-primary-foreground: var(--primary-foreground);
  --color-primary: var(--primary);
  --color-popover-foreground: var(--popover-foreground);
  --color-popover: var(--popover);
  --color-card-foreground: var(--card-foreground);
  --color-card: var(--card);
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);
}

@layer base {
  * {
    @apply border-border outline-ring/50;
  }

  body {
    @apply bg-background text-foreground;
  }

  .small-caps {
    font-variant: all-small-caps;
  }
}

@layer utilities {
  .masonry {
    column-count: 1;
    column-gap: 1.5rem;
  }

  @media (min-width: 640px) {
    .masonry {
      column-count: 1;
    }
  }

  @media (min-width: 768px) {
    .masonry {
      column-count: 2;
    }
  }

  @media (min-width: 1024px) {
    .masonry {
      column-count: 2;
    }
  }

  @media (min-width: 1280px) {
    .masonry {
      column-count: 3;
    }
  }

  .masonry-item {
    break-inside: avoid;
    margin-bottom: 1.5rem;
  }

  /* Scrollbar styling utilities */
  .sb-thin {
    scrollbar-width: thin;
  }

  .sb-none {
    scrollbar-width: none;
    -ms-overflow-style: none;
  }

  .sb-none::-webkit-scrollbar {
    display: none;
  }

  /* Custom scrollbar for webkit browsers */
  .sb-custom::-webkit-scrollbar {
    width: 6px;
    height: 6px;
  }

  .sb-custom::-webkit-scrollbar-track {
    background: transparent;
  }

  .sb-custom::-webkit-scrollbar-thumb {
    background: rgba(156, 163, 175, 0.5);
    border-radius: 3px;
  }

  .sb-custom::-webkit-scrollbar-thumb:hover {
    background: rgba(156, 163, 175, 0.7);
  }

  .dark .sb-custom::-webkit-scrollbar-thumb {
    background: rgba(75, 85, 99, 0.5);
  }

  .dark .sb-custom::-webkit-scrollbar-thumb:hover {
    background: rgba(75, 85, 99, 0.7);
  }

  /* Word breaking utilities for chat messages */
  .word-break-anywhere {
    overflow-wrap: anywhere;
    word-break: break-word;
    hyphens: auto;
  }

  /* Combine Tailwind overflow utilities with custom scrollbar */
  .overflow-y-auto.sb-custom,
  .overflow-x-auto.sb-custom,
  .overflow-auto.sb-custom {
    scrollbar-width: thin;
  }
}

/* Add this to your global.css (or use @layer if using Tailwind's custom CSS) */
@layer utilities {
  .typewriter {
    overflow: hidden;
    white-space: nowrap;
    border-right: 0.15em solid #facc15;
    /* Tailwind green-400 */
    animation:
      typing 2s steps(30, end),
      blink-caret 0.75s step-end infinite;
    max-width: fit-content;
  }

  @keyframes typing {
    from {
      width: 0;
    }

    to {
      width: 100%;
    }
  }

  @keyframes blink-caret {
    50% {
      border-color: transparent;
    }
  }
}

@layer base {

  /* Global scrollbar styles */
  * {
    scrollbar-width: thin;
    scrollbar-color: rgb(156 163 175) transparent;
  }

  .dark * {
    scrollbar-color: rgb(75 85 99) transparent;
  }

  *::-webkit-scrollbar {
    width: 8px;
    height: 8px;
  }

  *::-webkit-scrollbar-track {
    background: transparent;
  }

  *::-webkit-scrollbar-thumb {
    background-color: rgb(156 163 175);
    border-radius: 9999px;
    border: none;
  }

  .dark *::-webkit-scrollbar-thumb {
    background-color: rgb(75 85 99);
  }

  *::-webkit-scrollbar-thumb:hover {
    background-color: rgb(107 114 128);
  }

  .dark *::-webkit-scrollbar-thumb:hover {
    background-color: rgb(107 114 128);
  }
}

VS Code settings

non-default applied settings

{
  "tailwindCSS.rootFontSize": 12
}

Reproduction URL

Describe your issue

The suggestCanonicalClasses diagnostic produces a false positive when the canonical form of a class is already being used in the source code.

Image

Steps to reproduce

  1. Create a component with the following Tailwind class string:

    className="text-foreground h-10 px-2 text-left align-middle font-medium whitespace-wrap [&:has([role=checkbox])]:pr-0 *:[[role=checkbox]]:translate-y-[2px]"
  2. Observe that the IntelliSense extension reports a suggestCanonicalClasses warning on the *:[[role=checkbox]]:translate-y-[2px] portion of the string.

  3. The warning message reads:

    The class [&>[role=checkbox]]:translate-y-[2px] can be written as *:[[role=checkbox]]:translate-y-[2px]

Expected behavior

No warning should be emitted because the class already uses the canonical form *:[[role=checkbox]]:translate-y-[2px]. The diagnostic's pattern matching appears to be incorrectly reading the existing *:[[role=checkbox]]:translate-y-[2px] class as [&>[role=checkbox]]:translate-y-[2px] and then suggesting the exact same text that is already present.

Actual behavior

The extension reports the warning on two lines (line 70 and line 83 in my file) even though both lines already contain the suggested canonical class. The warning is effectively telling me to replace a class with itself.

This suggests the regex/pattern matching logic in the suggestCanonicalClasses feature is misidentifying *:[[role=checkbox]] as [&>[role=checkbox]] before comparing against the canonical form.

Affected code

// Line 70 – TableHead component
'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-wrap [&:has([role=checkbox])]:pr-0 *:[[role=checkbox]]:translate-y-[2px]'

// Line 83 – TableCell component  
'p-2 align-middle whitespace-wrap [&:has([role=checkbox])]:pr-0 *:[[role=checkbox]]:translate-y-[2px]'

Both lines already use *:[[role=checkbox]]:translate-y-[2px] (the canonical form), yet the diagnostic fires on both.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions