Skip to content

Commit 1f1b443

Browse files
Fix crash when class regex matches an empty string (#897)
* Add tests * Update TS configs * Refactor * Use `d` flag from RegExp This means we no longer need an external package for this too! * Refactor * Remove dependency * Don’t crash when regex is missing a capture group * Update lockfile * Update changelog
1 parent d19cb81 commit 1f1b443

16 files changed

+997
-823
lines changed

package-lock.json

+567-688
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/tailwindcss-language-server/ThirdPartyNotices.txt

-15
Original file line numberDiff line numberDiff line change
@@ -992,21 +992,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
992992

993993
================================================================================
994994

995-
becke-ch--regex--s0-0-v1--base--pl--lib@1.4.0
996-
997-
Copyright 2017 becke.ch - All Rights Reserved
998-
This file is part of becke-ch--regex--s0-v1
999-
1000-
becke.ch (MIT-style) License for the becke-ch--regex--s0-v1 Software
1001-
1002-
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
1003-
1004-
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
1005-
1006-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1007-
1008-
================================================================================
1009-
1010995
css.escape@1.5.1
1011996

1012997
Copyright Mathias Bynens <https://mathiasbynens.be/>

packages/tailwindcss-language-server/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@
7171
"stack-trace": "0.0.10",
7272
"tailwindcss": "3.4.1",
7373
"@tailwindcss/language-service": "*",
74-
"typescript": "4.6.4",
75-
"vitest": "0.34.2",
74+
"typescript": "5.3.3",
75+
"vitest": "^1.1.2",
7676
"vscode-css-languageservice": "6.2.9",
7777
"vscode-jsonrpc": "8.1.0",
7878
"vscode-languageserver": "8.0.2",

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

+75
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,19 @@ withFixture('basic', (c) => {
8484
})
8585
})
8686

87+
test.concurrent('classRegex simple (no matches)', async () => {
88+
let result = await completion({
89+
text: 'tron ',
90+
position: {
91+
line: 0,
92+
character: 5,
93+
},
94+
settings: { tailwindCSS: { experimental: { classRegex: ['test (\\S*)'] } } },
95+
})
96+
97+
expect(result).toBe(null)
98+
})
99+
87100
test.concurrent('classRegex nested', async () => {
88101
await expectCompletions({
89102
text: 'test ""',
@@ -97,6 +110,68 @@ withFixture('basic', (c) => {
97110
})
98111
})
99112

113+
test.concurrent('classRegex nested (no matches, container)', async () => {
114+
let result = await completion({
115+
text: 'tron ""',
116+
position: {
117+
line: 0,
118+
character: 6,
119+
},
120+
settings: {
121+
tailwindCSS: { experimental: { classRegex: [['test (\\S*)', '"([^"]*)"']] } },
122+
},
123+
})
124+
125+
expect(result).toBe(null)
126+
})
127+
128+
test.concurrent('classRegex nested (no matches, class)', async () => {
129+
let result = await completion({
130+
text: 'test ``',
131+
position: {
132+
line: 0,
133+
character: 6,
134+
},
135+
settings: {
136+
tailwindCSS: { experimental: { classRegex: [['test (\\S*)', '"([^"]*)"']] } },
137+
},
138+
})
139+
140+
expect(result).toBe(null)
141+
})
142+
143+
test('classRegex matching empty string', async () => {
144+
try {
145+
let result = await completion({
146+
text: "let _ = ''",
147+
position: {
148+
line: 0,
149+
character: 18,
150+
},
151+
settings: {
152+
tailwindCSS: { experimental: { classRegex: [["(?<=')(\\w*)(?=')"]] } },
153+
},
154+
})
155+
expect(result).toBe(null)
156+
} catch (err) {
157+
console.log(err.toJson())
158+
throw err
159+
}
160+
161+
let result2 = await completion({
162+
text: "let _ = ''; let _2 = 'text-",
163+
position: {
164+
line: 0,
165+
character: 27,
166+
},
167+
settings: {
168+
tailwindCSS: { experimental: { classRegex: [["(?<=')(\\w*)(?=')"]] } },
169+
},
170+
})
171+
172+
expect(result2).toBe(null)
173+
})
174+
100175
test.concurrent('resolve', async () => {
101176
let result = await completion({
102177
text: '<div class="">',

packages/tailwindcss-language-server/tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"compilerOptions": {
33
"module": "commonjs",
44
"target": "es6",
5-
"lib": ["ES2020"],
5+
"lib": ["ES2022"],
66
"rootDir": "..",
77
"sourceMap": true,
88
"moduleResolution": "node",

packages/tailwindcss-language-service/package.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"scripts": {
1010
"start": "node ./scripts/build.mjs --watch",
1111
"build": "node ./scripts/build.mjs",
12-
"prepublishOnly": "npm run build"
12+
"prepublishOnly": "npm run build",
13+
"test": "vitest"
1314
},
1415
"dependencies": {
1516
"@csstools/css-parser-algorithms": "2.1.1",
@@ -18,7 +19,6 @@
1819
"@types/culori": "^2.0.0",
1920
"@types/moo": "0.5.3",
2021
"@types/semver": "7.3.10",
21-
"becke-ch--regex--s0-0-v1--base--pl--lib": "1.4.0",
2222
"color-name": "1.1.4",
2323
"css.escape": "1.5.1",
2424
"culori": "0.20.1",
@@ -43,6 +43,7 @@
4343
"esbuild-node-externals": "^1.9.0",
4444
"prettier": "2.3.0",
4545
"tslib": "2.2.0",
46-
"typescript": "^5.2"
46+
"typescript": "^5.3.3",
47+
"vitest": "^1.1.2"
4748
}
4849
}

packages/tailwindcss-language-service/src/completionProvider.ts

+23-62
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ import { flagEnabled } from './util/flagEnabled'
3131
import * as jit from './util/jit'
3232
import { getVariantsFromClassName } from './util/getVariantsFromClassName'
3333
import * as culori from 'culori'
34-
import Regex from 'becke-ch--regex--s0-0-v1--base--pl--lib'
3534
import {
3635
addPixelEquivalentsToMediaQuery,
3736
addPixelEquivalentsToValue,
3837
} from './util/pixelEquivalents'
38+
import { customClassesIn } from './util/classes'
3939

4040
let isUtil = (className) =>
4141
Array.isArray(className.__info)
@@ -503,71 +503,32 @@ async function provideCustomClassNameCompletions(
503503
context?: CompletionContext
504504
): Promise<CompletionList> {
505505
const settings = await state.editor.getConfiguration(document.uri)
506-
const regexes = settings.tailwindCSS.experimental.classRegex
507-
if (regexes.length === 0) return null
506+
const filters = settings.tailwindCSS.experimental.classRegex
507+
if (filters.length === 0) return null
508508

509-
const positionOffset = document.offsetAt(position)
509+
const cursor = document.offsetAt(position)
510510

511-
const searchRange: Range = {
511+
let text = document.getText({
512512
start: document.positionAt(0),
513-
end: document.positionAt(positionOffset + 2000),
514-
}
515-
516-
let str = document.getText(searchRange)
517-
518-
for (let i = 0; i < regexes.length; i++) {
519-
try {
520-
let [containerRegexString, classRegexString] = Array.isArray(regexes[i])
521-
? regexes[i]
522-
: [regexes[i]]
523-
524-
let containerRegex = new Regex(containerRegexString, 'g')
525-
let containerMatch: ReturnType<Regex['exec']>
526-
527-
while ((containerMatch = containerRegex.exec(str)) !== null) {
528-
const searchStart = document.offsetAt(searchRange.start)
529-
const matchStart = searchStart + containerMatch.index[1]
530-
const matchEnd = matchStart + containerMatch[1].length
531-
const cursor = document.offsetAt(position)
532-
if (cursor >= matchStart && cursor <= matchEnd) {
533-
let classList: string
534-
535-
if (classRegexString) {
536-
let classRegex = new Regex(classRegexString, 'g')
537-
let classMatch: ReturnType<Regex['exec']>
538-
539-
while ((classMatch = classRegex.exec(containerMatch[1])) !== null) {
540-
const classMatchStart = matchStart + classMatch.index[1]
541-
const classMatchEnd = classMatchStart + classMatch[1].length
542-
if (cursor >= classMatchStart && cursor <= classMatchEnd) {
543-
classList = classMatch[1].substr(0, cursor - classMatchStart)
544-
}
545-
}
546-
547-
if (typeof classList === 'undefined') {
548-
throw Error()
549-
}
550-
} else {
551-
classList = containerMatch[1].substr(0, cursor - matchStart)
552-
}
513+
end: document.positionAt(cursor + 2000),
514+
})
553515

554-
return completionsFromClassList(
555-
state,
556-
classList,
557-
{
558-
start: {
559-
line: position.line,
560-
character: position.character - classList.length,
561-
},
562-
end: position,
563-
},
564-
settings.tailwindCSS.rootFontSize,
565-
undefined,
566-
context
567-
)
568-
}
569-
}
570-
} catch (_) {}
516+
// Get completions from the first matching regex or regex pair
517+
for (let match of customClassesIn({ text, cursor, filters })) {
518+
return completionsFromClassList(
519+
state,
520+
match.classList,
521+
{
522+
start: {
523+
line: position.line,
524+
character: position.character - match.classList.length,
525+
},
526+
end: position,
527+
},
528+
settings.tailwindCSS.rootFontSize,
529+
undefined,
530+
context
531+
)
571532
}
572533

573534
return null

0 commit comments

Comments
 (0)