From af53a2a31136ca71025eb1096bb23d820d910447 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 6 Oct 2025 16:54:27 -0400 Subject: [PATCH 1/3] Tweak comments --- .../tailwindcss-language-service/src/util/v4/design-system.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss-language-service/src/util/v4/design-system.ts b/packages/tailwindcss-language-service/src/util/v4/design-system.ts index 13c657c3..5aebbe14 100644 --- a/packages/tailwindcss-language-service/src/util/v4/design-system.ts +++ b/packages/tailwindcss-language-service/src/util/v4/design-system.ts @@ -38,8 +38,10 @@ export interface DesignSystem { getClassList(): ClassEntry[] getVariants(): VariantEntry[] - // Optional because it did not exist in earlier v4 alpha versions + // Added in v4.0.0-alpha.24 resolveThemeValue?(path: string, forceInline?: boolean): string | undefined + + // Added in v4.0.0-alpha.26 invalidCandidates?: Set } From 310167ed2ddba16d9428872aec6398cc92499223 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 6 Oct 2025 17:01:51 -0400 Subject: [PATCH 2/3] Detect when classes may be written more optimally --- .../tests/diagnostics/diagnostics.test.js | 60 ++++++++++++++++++- .../src/codeActions/codeActionProvider.ts | 4 +- .../provideSuggestionCodeActions.ts | 4 +- .../src/diagnostics/canonical-classes.ts | 59 ++++++++++++++++++ .../src/diagnostics/diagnosticsProvider.ts | 5 ++ .../src/diagnostics/types.ts | 13 ++++ .../src/util/state.ts | 2 + .../src/util/v4/design-system.ts | 3 + packages/vscode-tailwindcss/README.md | 11 ++++ packages/vscode-tailwindcss/package.json | 11 ++++ 10 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 packages/tailwindcss-language-service/src/diagnostics/canonical-classes.ts diff --git a/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js b/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js index afda8837..df3a58c4 100644 --- a/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js +++ b/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js @@ -1,7 +1,7 @@ import * as fs from 'node:fs/promises' import { expect, test } from 'vitest' import { withFixture } from '../common' -import { css, defineTest } from '../../src/testing' +import { css, defineTest, json } from '../../src/testing' import { createClient } from '../utils/client' withFixture('basic', (c) => { @@ -425,3 +425,61 @@ defineTest({ ]) }, }) + +defineTest({ + name: 'Shows warning when using non-canonical classes', + fs: { + // TODO: Drop this when the embedded version of tailwindcss is v4.1.15 + 'package.json': json` + { + "dependencies": { + "tailwindcss": "0.0.0-insiders.efe084b" + } + } + `, + 'app.css': css` + @import 'tailwindcss'; + `, + }, + prepare: async ({ root }) => ({ + client: await createClient({ + root, + settings: { + tailwindCSS: { + lint: { suggestCanonicalClasses: 'warning' }, + }, + }, + }), + }), + handle: async ({ client }) => { + let doc = await client.open({ + lang: 'html', + text: '
', + }) + + let diagnostics = await doc.diagnostics() + + expect(diagnostics).toEqual([ + { + code: 'suggestCanonicalClasses', + message: 'The class `[@media_print]:flex` can be written as `print:flex`', + range: { + start: { line: 0, character: 12 }, + end: { line: 0, character: 31 }, + }, + severity: 2, + suggestions: ['print:flex'], + }, + { + code: 'suggestCanonicalClasses', + message: 'The class `[color:red]/50` can be written as `text-[red]/50`', + range: { + start: { line: 0, character: 32 }, + end: { line: 0, character: 46 }, + }, + severity: 2, + suggestions: ['text-[red]/50'], + }, + ]) + }, +}) diff --git a/packages/tailwindcss-language-service/src/codeActions/codeActionProvider.ts b/packages/tailwindcss-language-service/src/codeActions/codeActionProvider.ts index 7ebf79cb..b25e9f63 100644 --- a/packages/tailwindcss-language-service/src/codeActions/codeActionProvider.ts +++ b/packages/tailwindcss-language-service/src/codeActions/codeActionProvider.ts @@ -13,6 +13,7 @@ import { isInvalidScreenDiagnostic, isInvalidVariantDiagnostic, isRecommendedVariantOrderDiagnostic, + isSuggestCanonicalClasses, } from '../diagnostics/types' import { flatten, dedupeBy } from '../util/array' import { provideCssConflictCodeActions } from './provideCssConflictCodeActions' @@ -74,7 +75,8 @@ export async function doCodeActions( isInvalidTailwindDirectiveDiagnostic(diagnostic) || isInvalidScreenDiagnostic(diagnostic) || isInvalidVariantDiagnostic(diagnostic) || - isRecommendedVariantOrderDiagnostic(diagnostic) + isRecommendedVariantOrderDiagnostic(diagnostic) || + isSuggestCanonicalClasses(diagnostic) ) { return provideSuggestionCodeActions(state, params, diagnostic) } diff --git a/packages/tailwindcss-language-service/src/codeActions/provideSuggestionCodeActions.ts b/packages/tailwindcss-language-service/src/codeActions/provideSuggestionCodeActions.ts index a0201699..af59afb4 100644 --- a/packages/tailwindcss-language-service/src/codeActions/provideSuggestionCodeActions.ts +++ b/packages/tailwindcss-language-service/src/codeActions/provideSuggestionCodeActions.ts @@ -6,6 +6,7 @@ import type { InvalidScreenDiagnostic, InvalidVariantDiagnostic, RecommendedVariantOrderDiagnostic, + SuggestCanonicalClassesDiagnostic, } from '../diagnostics/types' export function provideSuggestionCodeActions( @@ -16,7 +17,8 @@ export function provideSuggestionCodeActions( | InvalidTailwindDirectiveDiagnostic | InvalidScreenDiagnostic | InvalidVariantDiagnostic - | RecommendedVariantOrderDiagnostic, + | RecommendedVariantOrderDiagnostic + | SuggestCanonicalClassesDiagnostic, ): CodeAction[] { return diagnostic.suggestions.map((suggestion) => ({ title: `Replace with '${suggestion}'`, diff --git a/packages/tailwindcss-language-service/src/diagnostics/canonical-classes.ts b/packages/tailwindcss-language-service/src/diagnostics/canonical-classes.ts new file mode 100644 index 00000000..c47c40ce --- /dev/null +++ b/packages/tailwindcss-language-service/src/diagnostics/canonical-classes.ts @@ -0,0 +1,59 @@ +import type { TextDocument } from 'vscode-languageserver-textdocument' +import type { State, Settings } from '../util/state' +import { type SuggestCanonicalClassesDiagnostic, DiagnosticKind } from './types' +import { findClassListsInDocument, getClassNamesInClassList } from '../util/find' + +export async function getSuggestCanonicalClassesDiagnostics( + state: State, + document: TextDocument, + settings: Settings, +): Promise { + if (!state.v4) return [] + if (!state.designSystem.canonicalizeCandidates) return [] + + let severity = settings.tailwindCSS.lint.suggestCanonicalClasses + if (severity === 'ignore') return [] + + let diagnostics: SuggestCanonicalClassesDiagnostic[] = [] + + let classLists = await findClassListsInDocument(state, document) + + for (let classList of classLists) { + let classNames = getClassNamesInClassList(classList, []) + + // NOTES: + // + // A planned enhancement to `canonicalizeCandidates` is to operate on class *lists* which would + // allow `[font-size:0.875rem] [line-height:0.25rem]` to turn into a single `text-sm/3` class. + // + // To account for this future we process class names individually. At some future point we can + // then take the list of individual classes and pass it in *again* to issue a diagnostic for the + // class list as a whole. + // + // This may not allow you to see *which classes* got combined since the inputs/outputs map to + // entire lists but this seems fine to do + // + // We'd probably want to only issue a class list diagnostic once there are no individual class + // diagnostics in a given class list. + + for (let className of classNames) { + let canonicalized = state.designSystem.canonicalizeCandidates([className.className])[0] + let isCanonical = canonicalized === className.className + + if (isCanonical) continue + + diagnostics.push({ + code: DiagnosticKind.SuggestCanonicalClasses, + range: className.range, + severity: + severity === 'error' + ? 1 /* DiagnosticSeverity.Error */ + : 2 /* DiagnosticSeverity.Warning */, + message: `The class \`${className.className}\` can be written as \`${canonicalized}\``, + suggestions: [canonicalized], + }) + } + } + + return diagnostics +} diff --git a/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts b/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts index 075cea38..1244b181 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts @@ -10,6 +10,7 @@ import { getInvalidTailwindDirectiveDiagnostics } from './getInvalidTailwindDire import { getRecommendedVariantOrderDiagnostics } from './getRecommendedVariantOrderDiagnostics' import { getInvalidSourceDiagnostics } from './getInvalidSourceDiagnostics' import { getUsedBlocklistedClassDiagnostics } from './getUsedBlocklistedClassDiagnostics' +import { getSuggestCanonicalClassesDiagnostics } from './canonical-classes' export async function doValidate( state: State, @@ -24,6 +25,7 @@ export async function doValidate( DiagnosticKind.InvalidSourceDirective, DiagnosticKind.RecommendedVariantOrder, DiagnosticKind.UsedBlocklistedClass, + DiagnosticKind.SuggestCanonicalClasses, ], ): Promise { const settings = await state.editor.getConfiguration(document.uri) @@ -57,6 +59,9 @@ export async function doValidate( ...(only.includes(DiagnosticKind.UsedBlocklistedClass) ? await getUsedBlocklistedClassDiagnostics(state, document, settings) : []), + ...(only.includes(DiagnosticKind.SuggestCanonicalClasses) + ? await getSuggestCanonicalClassesDiagnostics(state, document, settings) + : []), ] : [] } diff --git a/packages/tailwindcss-language-service/src/diagnostics/types.ts b/packages/tailwindcss-language-service/src/diagnostics/types.ts index f022503a..444d8021 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/types.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/types.ts @@ -11,6 +11,7 @@ export enum DiagnosticKind { InvalidSourceDirective = 'invalidSourceDirective', RecommendedVariantOrder = 'recommendedVariantOrder', UsedBlocklistedClass = 'usedBlocklistedClass', + SuggestCanonicalClasses = 'suggestCanonicalClasses', } export type CssConflictDiagnostic = Diagnostic & { @@ -111,6 +112,17 @@ export function isUsedBlocklistedClass( return diagnostic.code === DiagnosticKind.UsedBlocklistedClass } +export type SuggestCanonicalClassesDiagnostic = Diagnostic & { + code: DiagnosticKind.SuggestCanonicalClasses + suggestions: string[] +} + +export function isSuggestCanonicalClasses( + diagnostic: AugmentedDiagnostic, +): diagnostic is SuggestCanonicalClassesDiagnostic { + return diagnostic.code === DiagnosticKind.SuggestCanonicalClasses +} + export type AugmentedDiagnostic = | CssConflictDiagnostic | InvalidApplyDiagnostic @@ -121,3 +133,4 @@ export type AugmentedDiagnostic = | InvalidSourceDirectiveDiagnostic | RecommendedVariantOrderDiagnostic | UsedBlocklistedClassDiagnostic + | SuggestCanonicalClassesDiagnostic diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts index ecd9d0d8..10fe81a7 100644 --- a/packages/tailwindcss-language-service/src/util/state.ts +++ b/packages/tailwindcss-language-service/src/util/state.ts @@ -66,6 +66,7 @@ export type TailwindCssSettings = { invalidSourceDirective: DiagnosticSeveritySetting recommendedVariantOrder: DiagnosticSeveritySetting usedBlocklistedClass: DiagnosticSeveritySetting + suggestCanonicalClasses: DiagnosticSeveritySetting } experimental: { classRegex: string[] | [string, string][] @@ -205,6 +206,7 @@ export function getDefaultTailwindSettings(): Settings { invalidSourceDirective: 'error', recommendedVariantOrder: 'warning', usedBlocklistedClass: 'warning', + suggestCanonicalClasses: 'warning', }, showPixelEquivalents: true, includeLanguages: {}, diff --git a/packages/tailwindcss-language-service/src/util/v4/design-system.ts b/packages/tailwindcss-language-service/src/util/v4/design-system.ts index 5aebbe14..3b3e3153 100644 --- a/packages/tailwindcss-language-service/src/util/v4/design-system.ts +++ b/packages/tailwindcss-language-service/src/util/v4/design-system.ts @@ -43,6 +43,9 @@ export interface DesignSystem { // Added in v4.0.0-alpha.26 invalidCandidates?: Set + + // Added in v4.1.15 + canonicalizeCandidates?(classes: string[]): string[] } export interface DesignSystem { diff --git a/packages/vscode-tailwindcss/README.md b/packages/vscode-tailwindcss/README.md index 6ce777d1..09252cbd 100644 --- a/packages/vscode-tailwindcss/README.md +++ b/packages/vscode-tailwindcss/README.md @@ -183,6 +183,17 @@ Class variants not in the recommended order (applies in [JIT mode](https://tailw Usage of class names that have been blocklisted via `@source not inline(…)`. **Default: `warning`** +#### `tailwindCSS.lint.suggestCanonicalClasses` + +Detect usage of class names that are not in the most optimal form. **Default: `warning`** + +Some examples of the changes this makes: + +| Class | Canonical Form | +| ------------------------------- | -------------- | +| `[color:red]/100` | `text-[red]` | +| `[@media_print]:[display:flex]` | `print:flex` | + ### `tailwindCSS.inspectPort` Enable the Node.js inspector agent for the language server and listen on the specified port. **Default: `null`** diff --git a/packages/vscode-tailwindcss/package.json b/packages/vscode-tailwindcss/package.json index 9ba7ca76..3e555514 100644 --- a/packages/vscode-tailwindcss/package.json +++ b/packages/vscode-tailwindcss/package.json @@ -316,6 +316,17 @@ "markdownDescription": "Usage of class names that have been blocklisted via `@source not inline(…)`", "scope": "language-overridable" }, + "tailwindCSS.lint.suggestCanonicalClasses": { + "type": "string", + "enum": [ + "ignore", + "warning", + "error" + ], + "default": "warning", + "markdownDescription": "Indicate when utilities may be written in a more optimal form", + "scope": "language-overridable" + }, "tailwindCSS.experimental.classRegex": { "type": "array", "scope": "language-overridable" From e55605d98dfd203ee1a929a760814786e24036e4 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 7 Oct 2025 10:48:25 -0400 Subject: [PATCH 3/3] Update changelog --- packages/vscode-tailwindcss/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index b8ccf1e9..bcef6285 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -4,6 +4,7 @@ - Fix infinite recursion in theme variable lookups ([#1473](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1473)) - Fix infinite recursion when replacing unbalanced calc expressions ([#1473](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1473)) +- Add diagnostic to suggest canonical classes by default ([#1475](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1475)) ## 0.14.27