Skip to content

Commit 0866277

Browse files
committed
wip
1 parent af53a2a commit 0866277

File tree

8 files changed

+147
-1
lines changed

8 files changed

+147
-1
lines changed

packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as fs from 'node:fs/promises'
22
import { expect, test } from 'vitest'
33
import { withFixture } from '../common'
4-
import { css, defineTest } from '../../src/testing'
4+
import { css, defineTest, json } from '../../src/testing'
55
import { createClient } from '../utils/client'
66

77
withFixture('basic', (c) => {
@@ -425,3 +425,51 @@ defineTest({
425425
])
426426
},
427427
})
428+
429+
defineTest({
430+
name: 'Shows warning when using non-canonical classes',
431+
fs: {
432+
'app.css': css`
433+
@import 'tailwindcss';
434+
`,
435+
},
436+
prepare: async ({ root }) => ({
437+
client: await createClient({
438+
root,
439+
settings: {
440+
tailwindCSS: {
441+
lint: { usedNonCanonicalClass: 'warning' },
442+
},
443+
},
444+
}),
445+
}),
446+
handle: async ({ client }) => {
447+
let doc = await client.open({
448+
lang: 'html',
449+
text: '<div class="[@media_print]:flex [color:red]/50">',
450+
})
451+
452+
let diagnostics = await doc.diagnostics()
453+
454+
expect(diagnostics).toEqual([
455+
{
456+
code: 'usedNonCanonicalClass',
457+
message: 'The class "[@media_print]:flex" is usually written as "print:flex"',
458+
range: {
459+
start: { line: 0, character: 12 },
460+
end: { line: 0, character: 31 },
461+
},
462+
severity: 2,
463+
},
464+
{
465+
code: 'usedNonCanonicalClass',
466+
message: 'The class "[color:red]/50" is usually written as "text-[red]/50"',
467+
range: {
468+
start: { line: 0, character: 32 },
469+
end: { line: 0, character: 46 },
470+
},
471+
severity: 2,
472+
},
473+
])
474+
},
475+
})
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { TextDocument } from 'vscode-languageserver-textdocument'
2+
import type { State, Settings } from '../util/state'
3+
import { type UsedNonCanonicalClassDiagnostic, DiagnosticKind } from './types'
4+
import { findClassListsInDocument, getClassNamesInClassList } from '../util/find'
5+
6+
export async function getUsedNonCanonicalClassDiagnostics(
7+
state: State,
8+
document: TextDocument,
9+
settings: Settings,
10+
): Promise<UsedNonCanonicalClassDiagnostic[]> {
11+
if (!state.v4) return []
12+
if (!state.designSystem.canonicalizeCandidates) return []
13+
14+
let severity = settings.tailwindCSS.lint.usedNonCanonicalClass
15+
if (severity === 'ignore') return []
16+
17+
let diagnostics: UsedNonCanonicalClassDiagnostic[] = []
18+
19+
let classLists = await findClassListsInDocument(state, document)
20+
21+
for (let classList of classLists) {
22+
let classNames = getClassNamesInClassList(classList, [])
23+
24+
// What if:
25+
// We pass in the entire class list. This would allow `canonicalizeCandidates` to canonicalize class *lists* too.
26+
// e.g. `[font-size:0.875rem] [line-height:0.25rem]` to `text-sm/3`
27+
//
28+
// What if the output class list is re-sorted? Maybe it shouldn't be because otherwise that
29+
// would be weird DX. Like at that point its about "this list isn't written how it should be"
30+
// but that could be because of sorting *or* non-canonical classes.
31+
//
32+
// So maybe output order needs to be preserved but then where do you put the new class?
33+
for (let className of classNames) {
34+
let canonicalized = state.designSystem.canonicalizeCandidates([className.className])[0]
35+
let isCanonical = canonicalized === className.className
36+
37+
if (isCanonical) continue
38+
39+
diagnostics.push({
40+
code: DiagnosticKind.UsedNonCanonicalClass,
41+
range: className.range,
42+
severity:
43+
severity === 'error'
44+
? 1 /* DiagnosticSeverity.Error */
45+
: 2 /* DiagnosticSeverity.Warning */,
46+
message: `The class \`${className.className}\` is usually written as \`${canonicalized}\``,
47+
})
48+
}
49+
}
50+
51+
return diagnostics
52+
}

packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { getInvalidTailwindDirectiveDiagnostics } from './getInvalidTailwindDire
1010
import { getRecommendedVariantOrderDiagnostics } from './getRecommendedVariantOrderDiagnostics'
1111
import { getInvalidSourceDiagnostics } from './getInvalidSourceDiagnostics'
1212
import { getUsedBlocklistedClassDiagnostics } from './getUsedBlocklistedClassDiagnostics'
13+
import { getUsedNonCanonicalClassDiagnostics } from './canonical-classes'
1314

1415
export async function doValidate(
1516
state: State,
@@ -24,6 +25,7 @@ export async function doValidate(
2425
DiagnosticKind.InvalidSourceDirective,
2526
DiagnosticKind.RecommendedVariantOrder,
2627
DiagnosticKind.UsedBlocklistedClass,
28+
DiagnosticKind.UsedNonCanonicalClass,
2729
],
2830
): Promise<AugmentedDiagnostic[]> {
2931
const settings = await state.editor.getConfiguration(document.uri)
@@ -57,6 +59,9 @@ export async function doValidate(
5759
...(only.includes(DiagnosticKind.UsedBlocklistedClass)
5860
? await getUsedBlocklistedClassDiagnostics(state, document, settings)
5961
: []),
62+
...(only.includes(DiagnosticKind.UsedNonCanonicalClass)
63+
? await getUsedNonCanonicalClassDiagnostics(state, document, settings)
64+
: []),
6065
]
6166
: []
6267
}

packages/tailwindcss-language-service/src/diagnostics/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export enum DiagnosticKind {
1111
InvalidSourceDirective = 'invalidSourceDirective',
1212
RecommendedVariantOrder = 'recommendedVariantOrder',
1313
UsedBlocklistedClass = 'usedBlocklistedClass',
14+
UsedNonCanonicalClass = 'usedNonCanonicalClass',
1415
}
1516

1617
export type CssConflictDiagnostic = Diagnostic & {
@@ -111,6 +112,16 @@ export function isUsedBlocklistedClass(
111112
return diagnostic.code === DiagnosticKind.UsedBlocklistedClass
112113
}
113114

115+
export type UsedNonCanonicalClassDiagnostic = Diagnostic & {
116+
code: DiagnosticKind.UsedNonCanonicalClass
117+
}
118+
119+
export function isUsedNonCanonicalClass(
120+
diagnostic: AugmentedDiagnostic,
121+
): diagnostic is UsedNonCanonicalClassDiagnostic {
122+
return diagnostic.code === DiagnosticKind.UsedNonCanonicalClass
123+
}
124+
114125
export type AugmentedDiagnostic =
115126
| CssConflictDiagnostic
116127
| InvalidApplyDiagnostic
@@ -121,3 +132,4 @@ export type AugmentedDiagnostic =
121132
| InvalidSourceDirectiveDiagnostic
122133
| RecommendedVariantOrderDiagnostic
123134
| UsedBlocklistedClassDiagnostic
135+
| UsedNonCanonicalClassDiagnostic

packages/tailwindcss-language-service/src/util/state.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export type TailwindCssSettings = {
6666
invalidSourceDirective: DiagnosticSeveritySetting
6767
recommendedVariantOrder: DiagnosticSeveritySetting
6868
usedBlocklistedClass: DiagnosticSeveritySetting
69+
usedNonCanonicalClass: DiagnosticSeveritySetting
6970
}
7071
experimental: {
7172
classRegex: string[] | [string, string][]
@@ -205,6 +206,7 @@ export function getDefaultTailwindSettings(): Settings {
205206
invalidSourceDirective: 'error',
206207
recommendedVariantOrder: 'warning',
207208
usedBlocklistedClass: 'warning',
209+
usedNonCanonicalClass: 'ignore',
208210
},
209211
showPixelEquivalents: true,
210212
includeLanguages: {},

packages/tailwindcss-language-service/src/util/v4/design-system.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ export interface DesignSystem {
4343

4444
// Added in v4.0.0-alpha.26
4545
invalidCandidates?: Set<string>
46+
47+
// Added in v4.1.15
48+
canonicalizeCandidates?(classes: string[]): string[]
4649
}
4750

4851
export interface DesignSystem {

packages/vscode-tailwindcss/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,19 @@ Class variants not in the recommended order (applies in [JIT mode](https://tailw
183183

184184
Usage of class names that have been blocklisted via `@source not inline(…)`. **Default: `warning`**
185185

186+
#### `tailwindCSS.lint.usedNonCanonicalClass`
187+
188+
Usage of class names that are not in the most "standardized" form. **Default: `ignore`**
189+
190+
Some examples of the changes this makes:
191+
192+
| Class | Canonical Form |
193+
| --------------------- | -------------- |
194+
| `[color:red]/100` | `text-[red]` |
195+
| `[@media_print]:flex` | `print:flex` |
196+
197+
This feature is off by default as it has a non-trivial performance and memory cost on startup and when editing your CSS.
198+
186199
### `tailwindCSS.inspectPort`
187200

188201
Enable the Node.js inspector agent for the language server and listen on the specified port. **Default: `null`**

packages/vscode-tailwindcss/package.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,17 @@
316316
"markdownDescription": "Usage of class names that have been blocklisted via `@source not inline(…)`",
317317
"scope": "language-overridable"
318318
},
319+
"tailwindCSS.lint.usedNonCanonicalClass": {
320+
"type": "string",
321+
"enum": [
322+
"ignore",
323+
"warning",
324+
"error"
325+
],
326+
"default": "warning",
327+
"markdownDescription": "Usage of class names that may be written in a more optimal form",
328+
"scope": "language-overridable"
329+
},
319330
"tailwindCSS.experimental.classRegex": {
320331
"type": "array",
321332
"scope": "language-overridable"

0 commit comments

Comments
 (0)