From 42830f03b560a7b7bf2914866a95e186d3e91cbf Mon Sep 17 00:00:00 2001 From: AutoJanitor <121303252+Scottcjn@users.noreply.github.com> Date: Fri, 13 Mar 2026 22:26:58 -0500 Subject: [PATCH 1/2] Warn when supportedFunctions regex entries are invalid --- src/core/classDetector.ts | 21 ++++++++++++++++++ test/__mocks__/vscode.ts | 1 + test/core/classDetector.test.ts | 38 +++++++++++++++++++++++++++++++-- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/core/classDetector.ts b/src/core/classDetector.ts index a0224d7..610d2a8 100644 --- a/src/core/classDetector.ts +++ b/src/core/classDetector.ts @@ -12,6 +12,21 @@ export interface ClassRange { range: vscode.Range } +const warnedInvalidSupportedFunctionPatterns = new Set() + +function warnInvalidSupportedFunctionPattern(pattern: string, error: unknown) { + if (warnedInvalidSupportedFunctionPatterns.has(pattern)) { + return + } + + warnedInvalidSupportedFunctionPatterns.add(pattern) + + const suffix = error instanceof Error && error.message ? `: ${error.message}` : "" + vscode.window.showWarningMessage( + `Tailwind Stash: invalid supportedFunctions regex "${pattern}" was ignored${suffix}`, + ) +} + /** * Detects Tailwind class strings in a document, including those wrapped * in utility functions like cn(), clsx(), cva(), twMerge(), etc. @@ -53,6 +68,12 @@ export function detectClassRanges( // Strip ^/$ anchors — they don't work inside a group in a larger regex. // Use \b boundaries instead for correct matching. const cleaned = regexMatch[1].replace(/^\^/, "").replace(/\$$/, "") + try { + new RegExp(cleaned) + } catch (error) { + warnInvalidSupportedFunctionPattern(fn, error) + continue + } regexPatterns.push(`\\b${cleaned}`) } else { literalNames.push(escapeRegex(fn)) diff --git a/test/__mocks__/vscode.ts b/test/__mocks__/vscode.ts index 9d728de..f24685c 100644 --- a/test/__mocks__/vscode.ts +++ b/test/__mocks__/vscode.ts @@ -371,6 +371,7 @@ export const window = { onDidChangeActiveTextEditor: makeEventEmitter("onDidChangeActiveTextEditor"), onDidChangeTextEditorSelection: makeEventEmitter("onDidChangeTextEditorSelection"), showInformationMessage(_msg: string) {}, + showWarningMessage(_msg: string) {}, showTextDocument() {}, visibleTextEditors: [] as unknown[], } diff --git a/test/core/classDetector.test.ts b/test/core/classDetector.test.ts index 133bbda..470dcb5 100644 --- a/test/core/classDetector.test.ts +++ b/test/core/classDetector.test.ts @@ -1,6 +1,6 @@ -import { describe, it, expect } from "vitest" +import { afterEach, describe, expect, it, vi } from "vitest" -import { createMockDocument } from "../__mocks__/vscode" +import { createMockDocument, window } from "../__mocks__/vscode" import { detectClassRanges } from "../../src/core/classDetector" const defaultFunctions = [ @@ -14,6 +14,10 @@ const defaultFunctions = [ "classnames", ] +afterEach(() => { + vi.restoreAllMocks() +}) + function detect(text: string, opts?: { functions?: string[]; minClasses?: number }) { const doc = createMockDocument(text) return detectClassRanges(doc, opts?.functions ?? defaultFunctions, opts?.minClasses ?? 4) @@ -432,6 +436,36 @@ describe("empty and invalid function patterns", () => { expect(results[0].element).toBe("div") }) + it("warns once and keeps valid detections when a regex pattern is invalid", () => { + const warningSpy = vi.spyOn(window, "showWarningMessage") + const text = ` +const a = cn("flex items-center p-4 rounded"); +const b = getButtonClasses("grid gap-4 p-6 bg-white"); +
+` + + const results = detect(text, { + functions: ["cn", "/[invalid(/", "/^get.*Classes$/"], + }) + + expect(results).toHaveLength(3) + expect(warningSpy).toHaveBeenCalledTimes(1) + expect(warningSpy.mock.calls[0]?.[0]).toContain("/[invalid(/") + }) + + it("does not repeat the warning for the same invalid regex pattern", () => { + const warningSpy = vi.spyOn(window, "showWarningMessage") + + detect('
', { + functions: ["/[still-invalid(/"], + }) + detect('
', { + functions: ["/[still-invalid(/"], + }) + + expect(warningSpy).toHaveBeenCalledTimes(1) + }) + it("returns results gracefully when regex pattern is invalid", () => { const results = detect('
', { functions: ["/[invalid(/"], From 9500be7323373d04788c21375d33ed5e245794c4 Mon Sep 17 00:00:00 2001 From: Scott Boudreaux <121303252+Scottcjn@users.noreply.github.com> Date: Wed, 20 May 2026 11:26:17 -0500 Subject: [PATCH 2/2] fix: resolve oxlint failures blocking CI - classDetector.ts: the regex-validation construction tripped no-new; add a targeted oxlint-disable since the constructor is intentionally invoked only to validate (unicorn/new-for-builtins still requires new). - vscode.ts mock: reorder window members so showTextDocument precedes showWarningMessage, satisfying perfectionist sort-objects. No behavior change. --- src/core/classDetector.ts | 2 ++ test/__mocks__/vscode.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/classDetector.ts b/src/core/classDetector.ts index 610d2a8..331075c 100644 --- a/src/core/classDetector.ts +++ b/src/core/classDetector.ts @@ -69,6 +69,8 @@ export function detectClassRanges( // Use \b boundaries instead for correct matching. const cleaned = regexMatch[1].replace(/^\^/, "").replace(/\$$/, "") try { + // Validate the pattern compiles; the constructor throws SyntaxError on invalid syntax. + // oxlint-disable-next-line no-new -- constructed only to validate; result intentionally unused new RegExp(cleaned) } catch (error) { warnInvalidSupportedFunctionPattern(fn, error) diff --git a/test/__mocks__/vscode.ts b/test/__mocks__/vscode.ts index b3cfe88..5ddb9c9 100644 --- a/test/__mocks__/vscode.ts +++ b/test/__mocks__/vscode.ts @@ -409,8 +409,8 @@ export const window = { onDidChangeTextEditorSelection: makeEventEmitter("onDidChangeTextEditorSelection"), onDidChangeVisibleTextEditors: makeEventEmitter("onDidChangeVisibleTextEditors"), showInformationMessage(_msg: string) {}, - showWarningMessage(_msg: string) {}, showTextDocument() {}, + showWarningMessage(_msg: string) {}, visibleTextEditors: [] as unknown[], }