Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down Expand Up @@ -425,3 +425,51 @@
])
},
})

defineTest({
name: 'Shows warning when using non-canonical classes',
fs: {
'app.css': css`
@import 'tailwindcss';
`,
},
prepare: async ({ root }) => ({
client: await createClient({
root,
settings: {
tailwindCSS: {
lint: { usedNonCanonicalClass: 'warning' },
},
},
}),
}),
handle: async ({ client }) => {
let doc = await client.open({
lang: 'html',
text: '<div class="[@media_print]:flex [color:red]/50">',
})

let diagnostics = await doc.diagnostics()

expect(diagnostics).toEqual([

Check failure on line 454 in packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js

View workflow job for this annotation

GitHub Actions / Run Tests - Node v20 / namespace-profile-windows-amd64

tests/diagnostics/diagnostics.test.js > Shows warning when using non-canonical classes

AssertionError: expected [] to deeply equal [ { …(4) }, { …(4) } ] - Expected + Received - [ - { - "code": "usedNonCanonicalClass", - "message": "The class \"[@media_print]:flex\" is usually written as \"print:flex\"", - "range": { - "end": { - "character": 31, - "line": 0, - }, - "start": { - "character": 12, - "line": 0, - }, - }, - "severity": 2, - }, - { - "code": "usedNonCanonicalClass", - "message": "The class \"[color:red]/50\" is usually written as \"text-[red]/50\"", - "range": { - "end": { - "character": 46, - "line": 0, - }, - "start": { - "character": 32, - "line": 0, - }, - }, - "severity": 2, - }, - ] + [] ❯ Object.handle tests/diagnostics/diagnostics.test.js:454:25 ❯ runTest src/testing/index.ts:49:5

Check failure on line 454 in packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js

View workflow job for this annotation

GitHub Actions / Run Tests - Node v22 / namespace-profile-default

tests/diagnostics/diagnostics.test.js > Shows warning when using non-canonical classes

AssertionError: expected [] to deeply equal [ { …(4) }, { …(4) } ] - Expected + Received - [ - { - "code": "usedNonCanonicalClass", - "message": "The class \"[@media_print]:flex\" is usually written as \"print:flex\"", - "range": { - "end": { - "character": 31, - "line": 0, - }, - "start": { - "character": 12, - "line": 0, - }, - }, - "severity": 2, - }, - { - "code": "usedNonCanonicalClass", - "message": "The class \"[color:red]/50\" is usually written as \"text-[red]/50\"", - "range": { - "end": { - "character": 46, - "line": 0, - }, - "start": { - "character": 32, - "line": 0, - }, - }, - "severity": 2, - }, - ] + [] ❯ Object.handle tests/diagnostics/diagnostics.test.js:454:25 ❯ runTest src/testing/index.ts:49:5

Check failure on line 454 in packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js

View workflow job for this annotation

GitHub Actions / Run Tests - Node v20 / namespace-profile-default

tests/diagnostics/diagnostics.test.js > Shows warning when using non-canonical classes

AssertionError: expected [] to deeply equal [ { …(4) }, { …(4) } ] - Expected + Received - [ - { - "code": "usedNonCanonicalClass", - "message": "The class \"[@media_print]:flex\" is usually written as \"print:flex\"", - "range": { - "end": { - "character": 31, - "line": 0, - }, - "start": { - "character": 12, - "line": 0, - }, - }, - "severity": 2, - }, - { - "code": "usedNonCanonicalClass", - "message": "The class \"[color:red]/50\" is usually written as \"text-[red]/50\"", - "range": { - "end": { - "character": 46, - "line": 0, - }, - "start": { - "character": 32, - "line": 0, - }, - }, - "severity": 2, - }, - ] + [] ❯ Object.handle tests/diagnostics/diagnostics.test.js:454:25 ❯ runTest src/testing/index.ts:49:5

Check failure on line 454 in packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js

View workflow job for this annotation

GitHub Actions / Run Tests - Node v22 / namespace-profile-windows-amd64

tests/diagnostics/diagnostics.test.js > Shows warning when using non-canonical classes

AssertionError: expected [] to deeply equal [ { …(4) }, { …(4) } ] - Expected + Received - [ - { - "code": "usedNonCanonicalClass", - "message": "The class \"[@media_print]:flex\" is usually written as \"print:flex\"", - "range": { - "end": { - "character": 31, - "line": 0, - }, - "start": { - "character": 12, - "line": 0, - }, - }, - "severity": 2, - }, - { - "code": "usedNonCanonicalClass", - "message": "The class \"[color:red]/50\" is usually written as \"text-[red]/50\"", - "range": { - "end": { - "character": 46, - "line": 0, - }, - "start": { - "character": 32, - "line": 0, - }, - }, - "severity": 2, - }, - ] + [] ❯ Object.handle tests/diagnostics/diagnostics.test.js:454:25 ❯ runTest src/testing/index.ts:49:5

Check failure on line 454 in packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js

View workflow job for this annotation

GitHub Actions / Run Tests - Node v24 / namespace-profile-macos-arm64

tests/diagnostics/diagnostics.test.js > Shows warning when using non-canonical classes

AssertionError: expected [] to deeply equal [ { …(4) }, { …(4) } ] - Expected + Received - [ - { - "code": "usedNonCanonicalClass", - "message": "The class \"[@media_print]:flex\" is usually written as \"print:flex\"", - "range": { - "end": { - "character": 31, - "line": 0, - }, - "start": { - "character": 12, - "line": 0, - }, - }, - "severity": 2, - }, - { - "code": "usedNonCanonicalClass", - "message": "The class \"[color:red]/50\" is usually written as \"text-[red]/50\"", - "range": { - "end": { - "character": 46, - "line": 0, - }, - "start": { - "character": 32, - "line": 0, - }, - }, - "severity": 2, - }, - ] + [] ❯ Object.handle tests/diagnostics/diagnostics.test.js:454:25 ❯ runTest src/testing/index.ts:49:5

Check failure on line 454 in packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js

View workflow job for this annotation

GitHub Actions / Run Tests - Node v18 / namespace-profile-default

tests/diagnostics/diagnostics.test.js > Shows warning when using non-canonical classes

AssertionError: expected [] to deeply equal [ { …(4) }, { …(4) } ] - Expected + Received - [ - { - "code": "usedNonCanonicalClass", - "message": "The class \"[@media_print]:flex\" is usually written as \"print:flex\"", - "range": { - "end": { - "character": 31, - "line": 0, - }, - "start": { - "character": 12, - "line": 0, - }, - }, - "severity": 2, - }, - { - "code": "usedNonCanonicalClass", - "message": "The class \"[color:red]/50\" is usually written as \"text-[red]/50\"", - "range": { - "end": { - "character": 46, - "line": 0, - }, - "start": { - "character": 32, - "line": 0, - }, - }, - "severity": 2, - }, - ] + [] ❯ Object.handle tests/diagnostics/diagnostics.test.js:454:25 ❯ runTest src/testing/index.ts:49:5

Check failure on line 454 in packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js

View workflow job for this annotation

GitHub Actions / Run Tests - Node v18 / namespace-profile-windows-amd64

tests/diagnostics/diagnostics.test.js > Shows warning when using non-canonical classes

AssertionError: expected [] to deeply equal [ { …(4) }, { …(4) } ] - Expected + Received - [ - { - "code": "usedNonCanonicalClass", - "message": "The class \"[@media_print]:flex\" is usually written as \"print:flex\"", - "range": { - "end": { - "character": 31, - "line": 0, - }, - "start": { - "character": 12, - "line": 0, - }, - }, - "severity": 2, - }, - { - "code": "usedNonCanonicalClass", - "message": "The class \"[color:red]/50\" is usually written as \"text-[red]/50\"", - "range": { - "end": { - "character": 46, - "line": 0, - }, - "start": { - "character": 32, - "line": 0, - }, - }, - "severity": 2, - }, - ] + [] ❯ Object.handle tests/diagnostics/diagnostics.test.js:454:25 ❯ runTest src/testing/index.ts:49:5

Check failure on line 454 in packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js

View workflow job for this annotation

GitHub Actions / Run Tests - Node v22 / namespace-profile-macos-arm64

tests/diagnostics/diagnostics.test.js > Shows warning when using non-canonical classes

AssertionError: expected [] to deeply equal [ { …(4) }, { …(4) } ] - Expected + Received - [ - { - "code": "usedNonCanonicalClass", - "message": "The class \"[@media_print]:flex\" is usually written as \"print:flex\"", - "range": { - "end": { - "character": 31, - "line": 0, - }, - "start": { - "character": 12, - "line": 0, - }, - }, - "severity": 2, - }, - { - "code": "usedNonCanonicalClass", - "message": "The class \"[color:red]/50\" is usually written as \"text-[red]/50\"", - "range": { - "end": { - "character": 46, - "line": 0, - }, - "start": { - "character": 32, - "line": 0, - }, - }, - "severity": 2, - }, - ] + [] ❯ Object.handle tests/diagnostics/diagnostics.test.js:454:25 ❯ runTest src/testing/index.ts:49:5

Check failure on line 454 in packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js

View workflow job for this annotation

GitHub Actions / Run Tests - Node v20 / namespace-profile-macos-arm64

tests/diagnostics/diagnostics.test.js > Shows warning when using non-canonical classes

AssertionError: expected [] to deeply equal [ { …(4) }, { …(4) } ] - Expected + Received - [ - { - "code": "usedNonCanonicalClass", - "message": "The class \"[@media_print]:flex\" is usually written as \"print:flex\"", - "range": { - "end": { - "character": 31, - "line": 0, - }, - "start": { - "character": 12, - "line": 0, - }, - }, - "severity": 2, - }, - { - "code": "usedNonCanonicalClass", - "message": "The class \"[color:red]/50\" is usually written as \"text-[red]/50\"", - "range": { - "end": { - "character": 46, - "line": 0, - }, - "start": { - "character": 32, - "line": 0, - }, - }, - "severity": 2, - }, - ] + [] ❯ Object.handle tests/diagnostics/diagnostics.test.js:454:25 ❯ runTest src/testing/index.ts:49:5

Check failure on line 454 in packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js

View workflow job for this annotation

GitHub Actions / Run Tests - Node v24 / namespace-profile-default

tests/diagnostics/diagnostics.test.js > Shows warning when using non-canonical classes

AssertionError: expected [] to deeply equal [ { …(4) }, { …(4) } ] - Expected + Received - [ - { - "code": "usedNonCanonicalClass", - "message": "The class \"[@media_print]:flex\" is usually written as \"print:flex\"", - "range": { - "end": { - "character": 31, - "line": 0, - }, - "start": { - "character": 12, - "line": 0, - }, - }, - "severity": 2, - }, - { - "code": "usedNonCanonicalClass", - "message": "The class \"[color:red]/50\" is usually written as \"text-[red]/50\"", - "range": { - "end": { - "character": 46, - "line": 0, - }, - "start": { - "character": 32, - "line": 0, - }, - }, - "severity": 2, - }, - ] + [] ❯ Object.handle tests/diagnostics/diagnostics.test.js:454:25 ❯ runTest src/testing/index.ts:49:5

Check failure on line 454 in packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js

View workflow job for this annotation

GitHub Actions / Run Tests - Node v18 / namespace-profile-macos-arm64

tests/diagnostics/diagnostics.test.js > Shows warning when using non-canonical classes

AssertionError: expected [] to deeply equal [ { …(4) }, { …(4) } ] - Expected + Received - [ - { - "code": "usedNonCanonicalClass", - "message": "The class \"[@media_print]:flex\" is usually written as \"print:flex\"", - "range": { - "end": { - "character": 31, - "line": 0, - }, - "start": { - "character": 12, - "line": 0, - }, - }, - "severity": 2, - }, - { - "code": "usedNonCanonicalClass", - "message": "The class \"[color:red]/50\" is usually written as \"text-[red]/50\"", - "range": { - "end": { - "character": 46, - "line": 0, - }, - "start": { - "character": 32, - "line": 0, - }, - }, - "severity": 2, - }, - ] + [] ❯ Object.handle tests/diagnostics/diagnostics.test.js:454:25 ❯ runTest src/testing/index.ts:49:5

Check failure on line 454 in packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js

View workflow job for this annotation

GitHub Actions / Run Tests - Node v24 / namespace-profile-windows-amd64

tests/diagnostics/diagnostics.test.js > Shows warning when using non-canonical classes

AssertionError: expected [] to deeply equal [ { …(4) }, { …(4) } ] - Expected + Received - [ - { - "code": "usedNonCanonicalClass", - "message": "The class \"[@media_print]:flex\" is usually written as \"print:flex\"", - "range": { - "end": { - "character": 31, - "line": 0, - }, - "start": { - "character": 12, - "line": 0, - }, - }, - "severity": 2, - }, - { - "code": "usedNonCanonicalClass", - "message": "The class \"[color:red]/50\" is usually written as \"text-[red]/50\"", - "range": { - "end": { - "character": 46, - "line": 0, - }, - "start": { - "character": 32, - "line": 0, - }, - }, - "severity": 2, - }, - ] + [] ❯ Object.handle tests/diagnostics/diagnostics.test.js:454:25 ❯ runTest src/testing/index.ts:49:5
{
code: 'usedNonCanonicalClass',
message: 'The class "[@media_print]:flex" is usually written as "print:flex"',
range: {
start: { line: 0, character: 12 },
end: { line: 0, character: 31 },
},
severity: 2,
},
{
code: 'usedNonCanonicalClass',
message: 'The class "[color:red]/50" is usually written as "text-[red]/50"',
range: {
start: { line: 0, character: 32 },
end: { line: 0, character: 46 },
},
severity: 2,
},
])
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { TextDocument } from 'vscode-languageserver-textdocument'
import type { State, Settings } from '../util/state'
import { type UsedNonCanonicalClassDiagnostic, DiagnosticKind } from './types'
import { findClassListsInDocument, getClassNamesInClassList } from '../util/find'

export async function getUsedNonCanonicalClassDiagnostics(
state: State,
document: TextDocument,
settings: Settings,
): Promise<UsedNonCanonicalClassDiagnostic[]> {
if (!state.v4) return []
if (!state.designSystem.canonicalizeCandidates) return []

let severity = settings.tailwindCSS.lint.usedNonCanonicalClass
if (severity === 'ignore') return []

let diagnostics: UsedNonCanonicalClassDiagnostic[] = []

let classLists = await findClassListsInDocument(state, document)

for (let classList of classLists) {
let classNames = getClassNamesInClassList(classList, [])

// What if:
// We pass in the entire class list. This would allow `canonicalizeCandidates` to canonicalize class *lists* too.
// e.g. `[font-size:0.875rem] [line-height:0.25rem]` to `text-sm/3`
//
// What if the output class list is re-sorted? Maybe it shouldn't be because otherwise that
// would be weird DX. Like at that point its about "this list isn't written how it should be"
// but that could be because of sorting *or* non-canonical classes.
//
// So maybe output order needs to be preserved but then where do you put the new class?
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.UsedNonCanonicalClass,
range: className.range,
severity:
severity === 'error'
? 1 /* DiagnosticSeverity.Error */
: 2 /* DiagnosticSeverity.Warning */,
message: `The class \`${className.className}\` is usually written as \`${canonicalized}\``,
})
}
}

return diagnostics
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { getInvalidTailwindDirectiveDiagnostics } from './getInvalidTailwindDire
import { getRecommendedVariantOrderDiagnostics } from './getRecommendedVariantOrderDiagnostics'
import { getInvalidSourceDiagnostics } from './getInvalidSourceDiagnostics'
import { getUsedBlocklistedClassDiagnostics } from './getUsedBlocklistedClassDiagnostics'
import { getUsedNonCanonicalClassDiagnostics } from './canonical-classes'

export async function doValidate(
state: State,
Expand All @@ -24,6 +25,7 @@ export async function doValidate(
DiagnosticKind.InvalidSourceDirective,
DiagnosticKind.RecommendedVariantOrder,
DiagnosticKind.UsedBlocklistedClass,
DiagnosticKind.UsedNonCanonicalClass,
],
): Promise<AugmentedDiagnostic[]> {
const settings = await state.editor.getConfiguration(document.uri)
Expand Down Expand Up @@ -57,6 +59,9 @@ export async function doValidate(
...(only.includes(DiagnosticKind.UsedBlocklistedClass)
? await getUsedBlocklistedClassDiagnostics(state, document, settings)
: []),
...(only.includes(DiagnosticKind.UsedNonCanonicalClass)
? await getUsedNonCanonicalClassDiagnostics(state, document, settings)
: []),
]
: []
}
12 changes: 12 additions & 0 deletions packages/tailwindcss-language-service/src/diagnostics/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export enum DiagnosticKind {
InvalidSourceDirective = 'invalidSourceDirective',
RecommendedVariantOrder = 'recommendedVariantOrder',
UsedBlocklistedClass = 'usedBlocklistedClass',
UsedNonCanonicalClass = 'usedNonCanonicalClass',
}

export type CssConflictDiagnostic = Diagnostic & {
Expand Down Expand Up @@ -111,6 +112,16 @@ export function isUsedBlocklistedClass(
return diagnostic.code === DiagnosticKind.UsedBlocklistedClass
}

export type UsedNonCanonicalClassDiagnostic = Diagnostic & {
code: DiagnosticKind.UsedNonCanonicalClass
}

export function isUsedNonCanonicalClass(
diagnostic: AugmentedDiagnostic,
): diagnostic is UsedNonCanonicalClassDiagnostic {
return diagnostic.code === DiagnosticKind.UsedNonCanonicalClass
}

export type AugmentedDiagnostic =
| CssConflictDiagnostic
| InvalidApplyDiagnostic
Expand All @@ -121,3 +132,4 @@ export type AugmentedDiagnostic =
| InvalidSourceDirectiveDiagnostic
| RecommendedVariantOrderDiagnostic
| UsedBlocklistedClassDiagnostic
| UsedNonCanonicalClassDiagnostic
2 changes: 2 additions & 0 deletions packages/tailwindcss-language-service/src/util/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export type TailwindCssSettings = {
invalidSourceDirective: DiagnosticSeveritySetting
recommendedVariantOrder: DiagnosticSeveritySetting
usedBlocklistedClass: DiagnosticSeveritySetting
usedNonCanonicalClass: DiagnosticSeveritySetting
}
experimental: {
classRegex: string[] | [string, string][]
Expand Down Expand Up @@ -205,6 +206,7 @@ export function getDefaultTailwindSettings(): Settings {
invalidSourceDirective: 'error',
recommendedVariantOrder: 'warning',
usedBlocklistedClass: 'warning',
usedNonCanonicalClass: 'ignore',
},
showPixelEquivalents: true,
includeLanguages: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,14 @@ 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<string>

// Added in v4.1.15
canonicalizeCandidates?(classes: string[]): string[]
}

export interface DesignSystem {
Expand Down
13 changes: 13 additions & 0 deletions packages/vscode-tailwindcss/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,19 @@ 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.usedNonCanonicalClass`

Usage of class names that are not in the most "standardized" form. **Default: `ignore`**

Some examples of the changes this makes:

| Class | Canonical Form |
| --------------------- | -------------- |
| `[color:red]/100` | `text-[red]` |
| `[@media_print]:flex` | `print:flex` |

This feature is off by default as it has a non-trivial performance and memory cost on startup and when editing your CSS.

### `tailwindCSS.inspectPort`

Enable the Node.js inspector agent for the language server and listen on the specified port. **Default: `null`**
Expand Down
11 changes: 11 additions & 0 deletions packages/vscode-tailwindcss/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,17 @@
"markdownDescription": "Usage of class names that have been blocklisted via `@source not inline(…)`",
"scope": "language-overridable"
},
"tailwindCSS.lint.usedNonCanonicalClass": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "warning",
"markdownDescription": "Usage of class names that may be written in a more optimal form",
"scope": "language-overridable"
},
"tailwindCSS.experimental.classRegex": {
"type": "array",
"scope": "language-overridable"
Expand Down
Loading