From a5b9836afe95fd18f4ed9c887bee213dd93a24ee Mon Sep 17 00:00:00 2001 From: "felix.schneider" Date: Tue, 18 Nov 2025 22:56:37 +0100 Subject: [PATCH 1/8] feat: add support for invalid class diagnostics and update settings --- .../src/diagnostics/diagnosticsProvider.ts | 5 ++ .../diagnostics/getInvalidClassDiagnostics.ts | 61 +++++++++++++++++++ .../src/diagnostics/types.ts | 13 ++++ .../src/util/state.ts | 4 +- packages/vscode-tailwindcss/package.json | 12 ++++ 5 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 packages/tailwindcss-language-service/src/diagnostics/getInvalidClassDiagnostics.ts 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..429f7bd7 --- /dev/null +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidClassDiagnostics.ts @@ -0,0 +1,61 @@ +import type { State, Settings, DocumentClassName } from '../util/state' +import { type InvalidClassDiagnostic, DiagnosticKind } from './types' +import { findClassListsInDocument, getClassNamesInClassList } from '../util/find' +import { visit } from './getCssConflictDiagnostics' +import type { TextDocument } from 'vscode-languageserver-textdocument' + +function isClassValid(state: State, className: string): boolean { + if (!state.v4) return true // Only check for v4 + + let roots = state.designSystem.compile([className]) + let hasDeclarations = 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 + break + } + } + } + }) + + return hasDeclarations +} + +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: `'${className.className}' is not a recognized Tailwind CSS class.`, + }) + } + }) + }) + + 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/state.ts b/packages/tailwindcss-language-service/src/util/state.ts index 10fe81a7..e534ccb5 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: 'warning', }, 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" From 0e4fb163620b99f189905cabe6e360447196cf00 Mon Sep 17 00:00:00 2001 From: "felix.schneider" Date: Tue, 18 Nov 2025 23:53:29 +0100 Subject: [PATCH 2/8] feat: refactor isClassValid function to improve invalid class checks --- .../diagnostics/getInvalidClassDiagnostics.ts | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidClassDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidClassDiagnostics.ts index 429f7bd7..c0571e3b 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidClassDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidClassDiagnostics.ts @@ -2,26 +2,36 @@ import type { State, Settings, DocumentClassName } 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 isClassValid(state: State, className: string): boolean { - if (!state.v4) return true // Only check for v4 + if (state.v4) { + // V4: Use design system compilation + let roots = state.designSystem.compile([className]) + let hasDeclarations = false - let roots = state.designSystem.compile([className]) - let hasDeclarations = 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 - break + 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 + break + } } } - } - }) - - return hasDeclarations + }) + return hasDeclarations + } 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( From f4d6cdb1c49e7bbce117d267c30decc2bc49b595 Mon Sep 17 00:00:00 2001 From: "felix.schneider" Date: Wed, 19 Nov 2025 15:01:04 +0100 Subject: [PATCH 3/8] feat: update invalid class severity level from warning to info --- .../src/diagnostics/getInvalidClassDiagnostics.ts | 2 +- packages/tailwindcss-language-service/src/util/state.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidClassDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidClassDiagnostics.ts index c0571e3b..8f286837 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidClassDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidClassDiagnostics.ts @@ -1,4 +1,4 @@ -import type { State, Settings, DocumentClassName } from '../util/state' +import type { State, Settings } from '../util/state' import { type InvalidClassDiagnostic, DiagnosticKind } from './types' import { findClassListsInDocument, getClassNamesInClassList } from '../util/find' import { visit } from './getCssConflictDiagnostics' diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts index e534ccb5..70c8f444 100644 --- a/packages/tailwindcss-language-service/src/util/state.ts +++ b/packages/tailwindcss-language-service/src/util/state.ts @@ -208,7 +208,7 @@ export function getDefaultTailwindSettings(): Settings { recommendedVariantOrder: 'warning', usedBlocklistedClass: 'warning', suggestCanonicalClasses: 'warning', - invalidClass: 'warning', + invalidClass: 'info', }, showPixelEquivalents: true, includeLanguages: {}, From 535429cfce0b1fade1a647af0d5aa904c81f31c3 Mon Sep 17 00:00:00 2001 From: "felix.schneider" Date: Wed, 19 Nov 2025 19:35:55 +0100 Subject: [PATCH 4/8] feat: add support for invalid class code actions and update diagnostic message --- .../src/codeActions/codeActionProvider.ts | 6 ++++ .../provideInvalidClassCodeActions.ts | 31 +++++++++++++++++++ .../diagnostics/getInvalidClassDiagnostics.ts | 2 +- 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 packages/tailwindcss-language-service/src/codeActions/provideInvalidClassCodeActions.ts 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..98a67718 --- /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 invalid 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/getInvalidClassDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidClassDiagnostics.ts index 8f286837..34da3239 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidClassDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidClassDiagnostics.ts @@ -61,7 +61,7 @@ export async function getInvalidClassDiagnostics( : severity === 'warning' ? 2 /* DiagnosticSeverity.Warning */ : 3 /* DiagnosticSeverity.Information */, - message: `'${className.className}' is not a recognized Tailwind CSS class.`, + message: `'${className.className}' is not a recognized Tailwind CSS utility class.`, }) } }) From 37423663e2c42f172057720e4e2bf0026850dc95 Mon Sep 17 00:00:00 2001 From: "felix.schneider" Date: Wed, 19 Nov 2025 21:28:22 +0100 Subject: [PATCH 5/8] feat: add tests for invalid class diagnostics and update related logic --- .../tests/diagnostics/diagnostics.test.js | 13 +++++++++ .../diagnostics/invalid-class/simple.json | 27 +++++++++++++++++++ .../diagnostics/getInvalidClassDiagnostics.ts | 13 ++++++--- 3 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 packages/tailwindcss-language-server/tests/diagnostics/invalid-class/simple.json diff --git a/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js b/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js index 957185d0..c43d7f90 100644 --- a/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js +++ b/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js @@ -37,6 +37,7 @@ withFixture('basic', (c) => { testFixture('css-conflict/vue-style-lang-sass') testFixture('invalid-screen/simple') testFixture('invalid-theme/simple') + testFixture('invalid-class/simple') }) withFixture('v4/basic', (c) => { @@ -88,6 +89,7 @@ withFixture('v4/basic', (c) => { // testFixture('css-conflict/css-multi-rule') // testFixture('css-conflict/css-multi-prop') // testFixture('invalid-screen/simple') + testFixture('invalid-class/simple') testInline('simple typos in theme keys (in key)', { code: '.test { color: theme(--color-red-901) }', @@ -324,6 +326,17 @@ withFixture('v4/basic', (c) => { }, ], }, + { + code: 'invalidClass', + source: 'tailwindcss', + message: "Unknown utility class 'foo'.", + className: { + className: 'foo', + classList: { + classList: 'foo max-w-4xl max-w-6xl hover:underline', + }, + }, + }, ], }) 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-service/src/diagnostics/getInvalidClassDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidClassDiagnostics.ts index 34da3239..90be2469 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidClassDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidClassDiagnostics.ts @@ -6,23 +6,30 @@ 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 - break + if (!isCustomProperty(child.prop)) { + hasNonCustomProperties = true + } } } } }) - return hasDeclarations + return hasDeclarations && hasNonCustomProperties } else if (state.jit) { // JIT: Try to generate rules let { rules } = jit.generateRules(state, [className]) @@ -61,7 +68,7 @@ export async function getInvalidClassDiagnostics( : severity === 'warning' ? 2 /* DiagnosticSeverity.Warning */ : 3 /* DiagnosticSeverity.Information */, - message: `'${className.className}' is not a recognized Tailwind CSS utility class.`, + message: `Unknown utility class '${className.className}'.`, }) } }) From 5ff8448955690a443e3c6e8892226cf49b68bec3 Mon Sep 17 00:00:00 2001 From: "felix.schneider" Date: Wed, 19 Nov 2025 22:46:28 +0100 Subject: [PATCH 6/8] feat: add test for valid classes and update invalid class code action title --- .../tests/diagnostics/diagnostics.test.js | 2 ++ .../tests/diagnostics/invalid-class/valid-classes.json | 4 ++++ .../src/codeActions/provideInvalidClassCodeActions.ts | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 packages/tailwindcss-language-server/tests/diagnostics/invalid-class/valid-classes.json diff --git a/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js b/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js index c43d7f90..257651a2 100644 --- a/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js +++ b/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js @@ -38,6 +38,7 @@ withFixture('basic', (c) => { testFixture('invalid-screen/simple') testFixture('invalid-theme/simple') testFixture('invalid-class/simple') + testFixture('invalid-class/valid-classes') }) withFixture('v4/basic', (c) => { @@ -90,6 +91,7 @@ withFixture('v4/basic', (c) => { // testFixture('css-conflict/css-multi-prop') // testFixture('invalid-screen/simple') testFixture('invalid-class/simple') + testFixture('invalid-class/valid-classes') testInline('simple typos in theme keys (in key)', { code: '.test { color: theme(--color-red-901) }', diff --git a/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/valid-classes.json b/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/valid-classes.json new file mode 100644 index 00000000..f6d8d799 --- /dev/null +++ b/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/valid-classes.json @@ -0,0 +1,4 @@ +{ + "code": "
", + "expected": [] +} \ No newline at end of file diff --git a/packages/tailwindcss-language-service/src/codeActions/provideInvalidClassCodeActions.ts b/packages/tailwindcss-language-service/src/codeActions/provideInvalidClassCodeActions.ts index 98a67718..03fb6a4a 100644 --- a/packages/tailwindcss-language-service/src/codeActions/provideInvalidClassCodeActions.ts +++ b/packages/tailwindcss-language-service/src/codeActions/provideInvalidClassCodeActions.ts @@ -10,7 +10,7 @@ export function provideInvalidClassCodeActions( ): CodeAction[] { return [ { - title: `Remove invalid class '${diagnostic.className.className}'`, + title: `Remove unknown utility class '${diagnostic.className.className}'`, kind: 'quickfix', diagnostics: [diagnostic], edit: { From c8597edb307e83d3dacffe4d0bd884ce64398ab7 Mon Sep 17 00:00:00 2001 From: "felix.schneider" Date: Wed, 19 Nov 2025 22:48:24 +0100 Subject: [PATCH 7/8] fix: getClassNamesInClassList to ignore newlines and spaces --- packages/tailwindcss-language-service/src/util/find.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts index 1bd51cfa..7d7aea51 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] && !blocklist.includes(parts[i])) { const start = indexToPosition(classList, index) const end = indexToPosition(classList, index + parts[i].length) names.push({ From 9cc237b46ff02ab094340b31b12cf61cbf7ffde5 Mon Sep 17 00:00:00 2001 From: "felix.schneider" Date: Wed, 19 Nov 2025 23:39:05 +0100 Subject: [PATCH 8/8] feat: add comprehensive tests for invalid class diagnostics and update find.ts getClassNamesInClassList logic for '$' handling --- .../tests/diagnostics/diagnostics.test.js | 18 +++++- .../invalid-class/css-multi-prop.json | 59 +++++++++++++++++++ .../invalid-class/css-multi-rule.json | 59 +++++++++++++++++++ .../tests/diagnostics/invalid-class/css.json | 59 +++++++++++++++++++ .../invalid-class/jsx-concat-positive.json | 58 ++++++++++++++++++ .../invalid-class/jsx-template-literal.json | 58 ++++++++++++++++++ .../diagnostics/invalid-class/variants.json | 57 ++++++++++++++++++ .../invalid-class/vue-style-lang-sass.json | 59 +++++++++++++++++++ ...-classes.json => whitespace-negative.json} | 0 .../src/util/find.ts | 2 +- 10 files changed, 426 insertions(+), 3 deletions(-) create mode 100644 packages/tailwindcss-language-server/tests/diagnostics/invalid-class/css-multi-prop.json create mode 100644 packages/tailwindcss-language-server/tests/diagnostics/invalid-class/css-multi-rule.json create mode 100644 packages/tailwindcss-language-server/tests/diagnostics/invalid-class/css.json create mode 100644 packages/tailwindcss-language-server/tests/diagnostics/invalid-class/jsx-concat-positive.json create mode 100644 packages/tailwindcss-language-server/tests/diagnostics/invalid-class/jsx-template-literal.json create mode 100644 packages/tailwindcss-language-server/tests/diagnostics/invalid-class/variants.json create mode 100644 packages/tailwindcss-language-server/tests/diagnostics/invalid-class/vue-style-lang-sass.json rename packages/tailwindcss-language-server/tests/diagnostics/invalid-class/{valid-classes.json => whitespace-negative.json} (100%) diff --git a/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js b/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js index 257651a2..ae3f3b05 100644 --- a/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js +++ b/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js @@ -38,7 +38,14 @@ withFixture('basic', (c) => { testFixture('invalid-screen/simple') testFixture('invalid-theme/simple') testFixture('invalid-class/simple') - testFixture('invalid-class/valid-classes') + testFixture('invalid-class/whitespace-negative') + testFixture('invalid-class/variants') + testFixture('invalid-class/jsx-concat-positive') + testFixture('invalid-class/jsx-template-literal') + testFixture('invalid-class/css') + testFixture('invalid-class/css-multi-prop') + testFixture('invalid-class/css-multi-rule') + testFixture('invalid-class/vue-style-lang-sass') }) withFixture('v4/basic', (c) => { @@ -91,7 +98,14 @@ withFixture('v4/basic', (c) => { // testFixture('css-conflict/css-multi-prop') // testFixture('invalid-screen/simple') testFixture('invalid-class/simple') - testFixture('invalid-class/valid-classes') + testFixture('invalid-class/whitespace-negative') + testFixture('invalid-class/variants') + testFixture('invalid-class/jsx-concat-positive') + testFixture('invalid-class/jsx-template-literal') + testFixture('invalid-class/css') + testFixture('invalid-class/css-multi-prop') + testFixture('invalid-class/css-multi-rule') + testFixture('invalid-class/vue-style-lang-sass') testInline('simple typos in theme keys (in key)', { code: '.test { color: theme(--color-red-901) }', diff --git a/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/css-multi-prop.json b/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/css-multi-prop.json new file mode 100644 index 00000000..5def25bf --- /dev/null +++ b/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/css-multi-prop.json @@ -0,0 +1,59 @@ +{ + "code": ".test { @apply px-4; color: red; @apply nonexistent }", + "language": "css", + "expected": [ + { + "code": "invalidClass", + "source": "tailwindcss", + "className": { + "className": "nonexistent", + "classList": { + "classList": "nonexistent", + "important": false, + "range": { + "start": { + "line": 0, + "character": 40 + }, + "end": { + "line": 0, + "character": 51 + } + } + }, + "range": { + "start": { + "line": 0, + "character": 40 + }, + "end": { + "line": 0, + "character": 51 + } + }, + "relativeRange": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 11 + } + } + }, + "range": { + "start": { + "line": 0, + "character": 40 + }, + "end": { + "line": 0, + "character": 51 + } + }, + "severity": 3, + "message": "Unknown utility class 'nonexistent'." + } + ] +} \ No newline at end of file diff --git a/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/css-multi-rule.json b/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/css-multi-rule.json new file mode 100644 index 00000000..57e27f1a --- /dev/null +++ b/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/css-multi-rule.json @@ -0,0 +1,59 @@ +{ + "code": ".test { @apply px-4 }\n.test { @apply nonexistent }", + "language": "css", + "expected": [ + { + "code": "invalidClass", + "source": "tailwindcss", + "className": { + "className": "nonexistent", + "classList": { + "classList": "nonexistent", + "important": false, + "range": { + "start": { + "line": 1, + "character": 15 + }, + "end": { + "line": 1, + "character": 26 + } + } + }, + "range": { + "start": { + "line": 1, + "character": 15 + }, + "end": { + "line": 1, + "character": 26 + } + }, + "relativeRange": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 11 + } + } + }, + "range": { + "start": { + "line": 1, + "character": 15 + }, + "end": { + "line": 1, + "character": 26 + } + }, + "severity": 3, + "message": "Unknown utility class 'nonexistent'." + } + ] +} \ No newline at end of file diff --git a/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/css.json b/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/css.json new file mode 100644 index 00000000..02f87336 --- /dev/null +++ b/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/css.json @@ -0,0 +1,59 @@ +{ + "code": ".test { @apply nonexistent; }", + "language": "css", + "expected": [ + { + "code": "invalidClass", + "source": "tailwindcss", + "className": { + "className": "nonexistent", + "classList": { + "classList": "nonexistent", + "important": false, + "range": { + "start": { + "line": 0, + "character": 15 + }, + "end": { + "line": 0, + "character": 26 + } + } + }, + "range": { + "start": { + "line": 0, + "character": 15 + }, + "end": { + "line": 0, + "character": 26 + } + }, + "relativeRange": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 11 + } + } + }, + "range": { + "start": { + "line": 0, + "character": 15 + }, + "end": { + "line": 0, + "character": 26 + } + }, + "severity": 3, + "message": "Unknown utility class 'nonexistent'." + } + ] +} \ No newline at end of file diff --git a/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/jsx-concat-positive.json b/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/jsx-concat-positive.json new file mode 100644 index 00000000..c3624af2 --- /dev/null +++ b/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/jsx-concat-positive.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/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/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/valid-classes.json b/packages/tailwindcss-language-server/tests/diagnostics/invalid-class/whitespace-negative.json similarity index 100% rename from packages/tailwindcss-language-server/tests/diagnostics/invalid-class/valid-classes.json rename to packages/tailwindcss-language-server/tests/diagnostics/invalid-class/whitespace-negative.json diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts index 7d7aea51..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 && parts[i] && !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({