",
+ "language": "javascriptreact",
+ "expected": [
+ {
+ "className": {
+ "classList": {
+ "classList": "nonexistent $",
+ "range": {
+ "end": {
+ "character": 30,
+ "line": 0
+ },
+ "start": {
+ "character": 17,
+ "line": 0
+ }
+ }
+ },
+ "className": "nonexistent",
+ "range": {
+ "end": {
+ "character": 28,
+ "line": 0
+ },
+ "start": {
+ "character": 17,
+ "line": 0
+ }
+ },
+ "relativeRange": {
+ "end": {
+ "character": 11,
+ "line": 0
+ },
+ "start": {
+ "character": 0,
+ "line": 0
+ }
+ }
+ },
+ "code": "invalidClass",
+ "message": "Unknown utility class 'nonexistent'.",
+ "range": {
+ "end": {
+ "character": 28,
+ "line": 0
+ },
+ "start": {
+ "character": 17,
+ "line": 0
+ }
+ },
+ "severity": 3,
+ "source": "tailwindcss"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/jsx-template-literal.json b/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/jsx-template-literal.json
new file mode 100644
index 00000000..c3624af2
--- /dev/null
+++ b/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/jsx-template-literal.json
@@ -0,0 +1,58 @@
+{
+ "code": "
",
+ "language": "javascriptreact",
+ "expected": [
+ {
+ "className": {
+ "classList": {
+ "classList": "nonexistent $",
+ "range": {
+ "end": {
+ "character": 30,
+ "line": 0
+ },
+ "start": {
+ "character": 17,
+ "line": 0
+ }
+ }
+ },
+ "className": "nonexistent",
+ "range": {
+ "end": {
+ "character": 28,
+ "line": 0
+ },
+ "start": {
+ "character": 17,
+ "line": 0
+ }
+ },
+ "relativeRange": {
+ "end": {
+ "character": 11,
+ "line": 0
+ },
+ "start": {
+ "character": 0,
+ "line": 0
+ }
+ }
+ },
+ "code": "invalidClass",
+ "message": "Unknown utility class 'nonexistent'.",
+ "range": {
+ "end": {
+ "character": 28,
+ "line": 0
+ },
+ "start": {
+ "character": 17,
+ "line": 0
+ }
+ },
+ "severity": 3,
+ "source": "tailwindcss"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/simple.json b/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/simple.json
new file mode 100644
index 00000000..9c8f70d4
--- /dev/null
+++ b/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/simple.json
@@ -0,0 +1,27 @@
+{
+ "code": "
",
+ "expected": [
+ {
+ "code": "invalidClass",
+ "source": "tailwindcss",
+ "className": {
+ "className": "nonexistent-class",
+ "classList": {
+ "classList": "px-4 nonexistent-class",
+ "range": {
+ "start": { "line": 0, "character": 12 },
+ "end": { "line": 0, "character": 34 }
+ }
+ },
+ "relativeRange": {
+ "start": { "line": 0, "character": 5 },
+ "end": { "line": 0, "character": 22 }
+ },
+ "range": { "start": { "line": 0, "character": 17 }, "end": { "line": 0, "character": 34 } }
+ },
+ "range": { "start": { "line": 0, "character": 17 }, "end": { "line": 0, "character": 34 } },
+ "severity": 3,
+ "message": "Unknown utility class 'nonexistent-class'."
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/variants.json b/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/variants.json
new file mode 100644
index 00000000..b8c469a7
--- /dev/null
+++ b/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/variants.json
@@ -0,0 +1,57 @@
+{
+ "code": "
",
+ "expected": [
+ {
+ "code": "invalidClass",
+ "source": "tailwindcss",
+ "className": {
+ "className": "hover:nonexistent",
+ "classList": {
+ "classList": "hover:nonexistent",
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 12
+ },
+ "end": {
+ "line": 0,
+ "character": 29
+ }
+ }
+ },
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 12
+ },
+ "end": {
+ "line": 0,
+ "character": 29
+ }
+ },
+ "relativeRange": {
+ "start": {
+ "line": 0,
+ "character": 0
+ },
+ "end": {
+ "line": 0,
+ "character": 17
+ }
+ }
+ },
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 12
+ },
+ "end": {
+ "line": 0,
+ "character": 29
+ }
+ },
+ "severity": 3,
+ "message": "Unknown utility class 'hover:nonexistent'."
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/vue-style-lang-sass.json b/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/vue-style-lang-sass.json
new file mode 100644
index 00000000..2a25ad04
--- /dev/null
+++ b/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/vue-style-lang-sass.json
@@ -0,0 +1,59 @@
+{
+ "code": "",
+ "language": "vue",
+ "expected": [
+ {
+ "code": "invalidClass",
+ "source": "tailwindcss",
+ "className": {
+ "className": "nonexistent",
+ "classList": {
+ "classList": "nonexistent",
+ "important": false,
+ "range": {
+ "start": {
+ "line": 2,
+ "character": 9
+ },
+ "end": {
+ "line": 2,
+ "character": 20
+ }
+ }
+ },
+ "range": {
+ "start": {
+ "line": 2,
+ "character": 9
+ },
+ "end": {
+ "line": 2,
+ "character": 20
+ }
+ },
+ "relativeRange": {
+ "start": {
+ "line": 0,
+ "character": 0
+ },
+ "end": {
+ "line": 0,
+ "character": 11
+ }
+ }
+ },
+ "range": {
+ "start": {
+ "line": 2,
+ "character": 9
+ },
+ "end": {
+ "line": 2,
+ "character": 20
+ }
+ },
+ "severity": 3,
+ "message": "Unknown utility class 'nonexistent'."
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/whitespace-negative.json b/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/whitespace-negative.json
new file mode 100644
index 00000000..f6d8d799
--- /dev/null
+++ b/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/whitespace-negative.json
@@ -0,0 +1,4 @@
+{
+ "code": "
",
+ "expected": []
+}
\ No newline at end of file
diff --git a/packages/tailwindcss-language-service/src/codeActions/codeActionProvider.ts b/packages/tailwindcss-language-service/src/codeActions/codeActionProvider.ts
index b25e9f63..53cba044 100644
--- a/packages/tailwindcss-language-service/src/codeActions/codeActionProvider.ts
+++ b/packages/tailwindcss-language-service/src/codeActions/codeActionProvider.ts
@@ -14,11 +14,13 @@ import {
isInvalidVariantDiagnostic,
isRecommendedVariantOrderDiagnostic,
isSuggestCanonicalClasses,
+ isInvalidClassDiagnostic,
} from '../diagnostics/types'
import { flatten, dedupeBy } from '../util/array'
import { provideCssConflictCodeActions } from './provideCssConflictCodeActions'
import { provideInvalidApplyCodeActions } from './provideInvalidApplyCodeActions'
import { provideSuggestionCodeActions } from './provideSuggestionCodeActions'
+import { provideInvalidClassCodeActions } from './provideInvalidClassCodeActions'
async function getDiagnosticsFromCodeActionParams(
state: State,
@@ -70,6 +72,10 @@ export async function doCodeActions(
return provideCssConflictCodeActions(state, params, diagnostic)
}
+ if (isInvalidClassDiagnostic(diagnostic)) {
+ return provideInvalidClassCodeActions(state, params, diagnostic)
+ }
+
if (
isInvalidConfigPathDiagnostic(diagnostic) ||
isInvalidTailwindDirectiveDiagnostic(diagnostic) ||
diff --git a/packages/tailwindcss-language-service/src/codeActions/provideInvalidClassCodeActions.ts b/packages/tailwindcss-language-service/src/codeActions/provideInvalidClassCodeActions.ts
new file mode 100644
index 00000000..03fb6a4a
--- /dev/null
+++ b/packages/tailwindcss-language-service/src/codeActions/provideInvalidClassCodeActions.ts
@@ -0,0 +1,31 @@
+import type { CodeAction, CodeActionParams } from 'vscode-languageserver'
+import type { State } from '../util/state'
+import type { InvalidClassDiagnostic } from '../diagnostics/types'
+import { removeRangesFromString } from '../util/removeRangesFromString'
+
+export function provideInvalidClassCodeActions(
+ _state: State,
+ params: CodeActionParams,
+ diagnostic: InvalidClassDiagnostic,
+): CodeAction[] {
+ return [
+ {
+ title: `Remove unknown utility class '${diagnostic.className.className}'`,
+ kind: 'quickfix',
+ diagnostics: [diagnostic],
+ edit: {
+ changes: {
+ [params.textDocument.uri]: [
+ {
+ range: diagnostic.className.classList.range,
+ newText: removeRangesFromString(
+ diagnostic.className.classList.classList,
+ [diagnostic.className.relativeRange],
+ ),
+ },
+ ],
+ },
+ },
+ },
+ ]
+}
\ No newline at end of file
diff --git a/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts b/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts
index 1244b181..c44932ce 100644
--- a/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts
+++ b/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts
@@ -11,6 +11,7 @@ import { getRecommendedVariantOrderDiagnostics } from './getRecommendedVariantOr
import { getInvalidSourceDiagnostics } from './getInvalidSourceDiagnostics'
import { getUsedBlocklistedClassDiagnostics } from './getUsedBlocklistedClassDiagnostics'
import { getSuggestCanonicalClassesDiagnostics } from './canonical-classes'
+import { getInvalidClassDiagnostics } from './getInvalidClassDiagnostics'
export async function doValidate(
state: State,
@@ -26,6 +27,7 @@ export async function doValidate(
DiagnosticKind.RecommendedVariantOrder,
DiagnosticKind.UsedBlocklistedClass,
DiagnosticKind.SuggestCanonicalClasses,
+ DiagnosticKind.InvalidClass,
],
): Promise
{
const settings = await state.editor.getConfiguration(document.uri)
@@ -62,6 +64,9 @@ export async function doValidate(
...(only.includes(DiagnosticKind.SuggestCanonicalClasses)
? await getSuggestCanonicalClassesDiagnostics(state, document, settings)
: []),
+ ...(only.includes(DiagnosticKind.InvalidClass)
+ ? await getInvalidClassDiagnostics(state, document, settings)
+ : []),
]
: []
}
diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidClassDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidClassDiagnostics.ts
new file mode 100644
index 00000000..90be2469
--- /dev/null
+++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidClassDiagnostics.ts
@@ -0,0 +1,78 @@
+import type { State, Settings } from '../util/state'
+import { type InvalidClassDiagnostic, DiagnosticKind } from './types'
+import { findClassListsInDocument, getClassNamesInClassList } from '../util/find'
+import { visit } from './getCssConflictDiagnostics'
+import { getClassNameDecls } from '../util/getClassNameDecls'
+import * as jit from '../util/jit'
+import type { TextDocument } from 'vscode-languageserver-textdocument'
+
+function isCustomProperty(property: string): boolean {
+ return property.startsWith('--')
+}
+
+function isClassValid(state: State, className: string): boolean {
+ if (state.v4) {
+ // V4: Use design system compilation
+ let roots = state.designSystem.compile([className])
+ let hasDeclarations = false
+ let hasNonCustomProperties = false
+
+ visit([roots[0]], (node) => {
+ if ((node.type === 'rule' || node.type === 'atrule') && node.nodes) {
+ for (let child of node.nodes) {
+ if (child.type === 'decl') {
+ hasDeclarations = true
+ if (!isCustomProperty(child.prop)) {
+ hasNonCustomProperties = true
+ }
+ }
+ }
+ }
+ })
+ return hasDeclarations && hasNonCustomProperties
+ } else if (state.jit) {
+ // JIT: Try to generate rules
+ let { rules } = jit.generateRules(state, [className])
+ return rules.length > 0
+ } else {
+ // Static: Check if decls exist
+ let decls = getClassNameDecls(state, className)
+ return !!decls
+ }
+}
+
+export async function getInvalidClassDiagnostics(
+ state: State,
+ document: TextDocument,
+ settings: Settings,
+): Promise {
+ let severity = settings.tailwindCSS.lint.invalidClass || 'warning' // Assuming a new setting, default to warning
+ if (severity === 'ignore') return []
+
+ let diagnostics: InvalidClassDiagnostic[] = []
+ const classLists = await findClassListsInDocument(state, document)
+
+ classLists.forEach((classList) => {
+ const classNames = getClassNamesInClassList(classList, state.blocklist)
+
+ classNames.forEach((className) => {
+ if (!isClassValid(state, className.className)) {
+ diagnostics.push({
+ code: DiagnosticKind.InvalidClass,
+ source: 'tailwindcss',
+ className,
+ range: className.range,
+ severity:
+ severity === 'error'
+ ? 1 /* DiagnosticSeverity.Error */
+ : severity === 'warning'
+ ? 2 /* DiagnosticSeverity.Warning */
+ : 3 /* DiagnosticSeverity.Information */,
+ message: `Unknown utility class '${className.className}'.`,
+ })
+ }
+ })
+ })
+
+ return diagnostics
+}
\ No newline at end of file
diff --git a/packages/tailwindcss-language-service/src/diagnostics/types.ts b/packages/tailwindcss-language-service/src/diagnostics/types.ts
index 444d8021..705d3374 100644
--- a/packages/tailwindcss-language-service/src/diagnostics/types.ts
+++ b/packages/tailwindcss-language-service/src/diagnostics/types.ts
@@ -12,6 +12,7 @@ export enum DiagnosticKind {
RecommendedVariantOrder = 'recommendedVariantOrder',
UsedBlocklistedClass = 'usedBlocklistedClass',
SuggestCanonicalClasses = 'suggestCanonicalClasses',
+ InvalidClass = 'invalidClass',
}
export type CssConflictDiagnostic = Diagnostic & {
@@ -123,6 +124,17 @@ export function isSuggestCanonicalClasses(
return diagnostic.code === DiagnosticKind.SuggestCanonicalClasses
}
+export type InvalidClassDiagnostic = Diagnostic & {
+ code: DiagnosticKind.InvalidClass
+ className: DocumentClassName
+}
+
+export function isInvalidClassDiagnostic(
+ diagnostic: AugmentedDiagnostic,
+): diagnostic is InvalidClassDiagnostic {
+ return diagnostic.code === DiagnosticKind.InvalidClass
+}
+
export type AugmentedDiagnostic =
| CssConflictDiagnostic
| InvalidApplyDiagnostic
@@ -134,3 +146,4 @@ export type AugmentedDiagnostic =
| RecommendedVariantOrderDiagnostic
| UsedBlocklistedClassDiagnostic
| SuggestCanonicalClassesDiagnostic
+ | InvalidClassDiagnostic
diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts
index 1bd51cfa..487e16da 100644
--- a/packages/tailwindcss-language-service/src/util/find.ts
+++ b/packages/tailwindcss-language-service/src/util/find.ts
@@ -40,7 +40,7 @@ export function getClassNamesInClassList(
const names: DocumentClassName[] = []
let index = 0
for (let i = 0; i < parts.length; i++) {
- if (i % 2 === 0 && !blocklist.includes(parts[i])) {
+ if (i % 2 === 0 && parts[i] && parts[i] !== '$' && !blocklist.includes(parts[i])) {
const start = indexToPosition(classList, index)
const end = indexToPosition(classList, index + parts[i].length)
names.push({
diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts
index 10fe81a7..70c8f444 100644
--- a/packages/tailwindcss-language-service/src/util/state.ts
+++ b/packages/tailwindcss-language-service/src/util/state.ts
@@ -36,7 +36,7 @@ export type EditorState = {
) => Promise>
}
-type DiagnosticSeveritySetting = 'ignore' | 'warning' | 'error'
+type DiagnosticSeveritySetting = 'ignore' | 'info' | 'warning' | 'error'
export type EditorSettings = {
tabSize: number
@@ -67,6 +67,7 @@ export type TailwindCssSettings = {
recommendedVariantOrder: DiagnosticSeveritySetting
usedBlocklistedClass: DiagnosticSeveritySetting
suggestCanonicalClasses: DiagnosticSeveritySetting
+ invalidClass: DiagnosticSeveritySetting
}
experimental: {
classRegex: string[] | [string, string][]
@@ -207,6 +208,7 @@ export function getDefaultTailwindSettings(): Settings {
recommendedVariantOrder: 'warning',
usedBlocklistedClass: 'warning',
suggestCanonicalClasses: 'warning',
+ invalidClass: 'info',
},
showPixelEquivalents: true,
includeLanguages: {},
diff --git a/packages/vscode-tailwindcss/package.json b/packages/vscode-tailwindcss/package.json
index 2f8cba5f..14571829 100644
--- a/packages/vscode-tailwindcss/package.json
+++ b/packages/vscode-tailwindcss/package.json
@@ -327,6 +327,18 @@
"markdownDescription": "Indicate when utilities may be written in a more optimal form",
"scope": "language-overridable"
},
+ "tailwindCSS.lint.invalidClass": {
+ "type": "string",
+ "enum": [
+ "ignore",
+ "info",
+ "warning",
+ "error"
+ ],
+ "default": "info",
+ "markdownDescription": "Classes that are not recognized as valid Tailwind CSS classes",
+ "scope": "language-overridable"
+ },
"tailwindCSS.experimental.classRegex": {
"type": "array",
"scope": "language-overridable"