Skip to content
Merged
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,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: '<div class="[@media_print]:flex [color:red]/50">',
})

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'],
},
])
},
})
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
isInvalidScreenDiagnostic,
isInvalidVariantDiagnostic,
isRecommendedVariantOrderDiagnostic,
isSuggestCanonicalClasses,
} from '../diagnostics/types'
import { flatten, dedupeBy } from '../util/array'
import { provideCssConflictCodeActions } from './provideCssConflictCodeActions'
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
InvalidScreenDiagnostic,
InvalidVariantDiagnostic,
RecommendedVariantOrderDiagnostic,
SuggestCanonicalClassesDiagnostic,
} from '../diagnostics/types'

export function provideSuggestionCodeActions(
Expand All @@ -16,7 +17,8 @@ export function provideSuggestionCodeActions(
| InvalidTailwindDirectiveDiagnostic
| InvalidScreenDiagnostic
| InvalidVariantDiagnostic
| RecommendedVariantOrderDiagnostic,
| RecommendedVariantOrderDiagnostic
| SuggestCanonicalClassesDiagnostic,
): CodeAction[] {
return diagnostic.suggestions.map((suggestion) => ({
title: `Replace with '${suggestion}'`,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SuggestCanonicalClassesDiagnostic[]> {
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
}
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 { getSuggestCanonicalClassesDiagnostics } 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.SuggestCanonicalClasses,
],
): 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.SuggestCanonicalClasses)
? await getSuggestCanonicalClassesDiagnostics(state, document, settings)
: []),
]
: []
}
13 changes: 13 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',
SuggestCanonicalClasses = 'suggestCanonicalClasses',
}

export type CssConflictDiagnostic = Diagnostic & {
Expand Down Expand Up @@ -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
Expand All @@ -121,3 +133,4 @@ export type AugmentedDiagnostic =
| InvalidSourceDirectiveDiagnostic
| RecommendedVariantOrderDiagnostic
| UsedBlocklistedClassDiagnostic
| SuggestCanonicalClassesDiagnostic
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
suggestCanonicalClasses: DiagnosticSeveritySetting
}
experimental: {
classRegex: string[] | [string, string][]
Expand Down Expand Up @@ -205,6 +206,7 @@ export function getDefaultTailwindSettings(): Settings {
invalidSourceDirective: 'error',
recommendedVariantOrder: 'warning',
usedBlocklistedClass: 'warning',
suggestCanonicalClasses: 'warning',
},
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
1 change: 1 addition & 0 deletions packages/vscode-tailwindcss/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
11 changes: 11 additions & 0 deletions packages/vscode-tailwindcss/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`**
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.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"
Expand Down