Skip to content

Ignore regex literals when analyzing language boundaries #1275

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 20, 2025
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
48 changes: 48 additions & 0 deletions packages/tailwindcss-language-server/tests/env/v4.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -791,3 +791,51 @@ defineTest({
})
},
})

defineTest({
options: { only: true },
name: 'regex literals do not break language boundaries',
fs: {
'app.css': css`
@import 'tailwindcss';
`,
},
prepare: async ({ root }) => ({ client: await createClient({ root }) }),
handle: async ({ client }) => {
let doc = await client.open({
lang: 'javascriptreact',
text: js`
export default function Page() {
let styles = "str".match(/<style>[\s\S]*?<\/style>/m)
return <div className="bg-[#000]">{styles}</div>
}
`,
})

expect(await client.project()).toMatchObject({
tailwind: {
version: '4.0.6',
isDefaultVersion: true,
},
})

// return <div className="bg-[#000]">{styles}</div>
// ^
let hover = await doc.hover({ line: 2, character: 26 })

expect(hover).toEqual({
contents: {
language: 'css',
value: dedent`
.bg-\[\#000\] {
background-color: #000;
}
`,
},
range: {
start: { line: 2, character: 25 },
end: { line: 2, character: 34 },
},
})
},
})
150 changes: 150 additions & 0 deletions packages/tailwindcss-language-service/src/util/doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,5 +127,155 @@ function getJsWithoutComments(text: string): string {
}
}

str = stripRegexLiterals(str)

return str
}

function stripRegexLiterals(input: string) {
const BACKSLASH = 0x5c // \
const SLASH = 0x2f // /
const LINE_BREAK = 0x0a // \n
const COMMA = 0x2c // ,
const COLON = 0x3a // :
const EQUALS = 0x3d // =
const SEMICOLON = 0x3b // ;
const BRACKET_OPEN = 0x5b // [
const BRACKET_CLOSE = 0x5d // ]
const QUESTION_MARK = 0x3f // ?
const PAREN_OPEN = 0x28 // (
const CURLY_OPEN = 0x7b // {
const DOUBLE_QUOTE = 0x22 // "
const SINGLE_QUOTE = 0x27 // '
const BACKTICK = 0x60 // `

let SPACE = 0x20 // " "
let TAB = 0x09 // \t

// Top level; or
// after comma
// after colon
// after equals
// after semicolon
// after square bracket (arrays, object property expressions)
// after question mark
// after open paren
// after curly (jsx only)

let inRegex = false
let inEscape = false
let inCharacterClass = false

let regexStart = -1
let regexEnd = -1

// Based on the oxc_parser crate
// https://github.com/oxc-project/oxc/blob/5f97f28ddbd2cd303a306f7fb0092b0e54bda43c/crates/oxc_parser/src/lexer/regex.rs#L29
let prev = null
for (let i = 0; i < input.length; ++i) {
let c = input.charCodeAt(i)

if (inRegex) {
if (c === LINE_BREAK) {
break
} else if (inEscape) {
inEscape = false
} else if (c === SLASH && !inCharacterClass) {
inRegex = false
regexEnd = i
break
} else if (c === BRACKET_OPEN) {
inCharacterClass = true
} else if (c === BACKSLASH) {
inEscape = true
} else if (c === BRACKET_CLOSE) {
inCharacterClass = false
}

continue
}

// Skip over strings
if (c === SINGLE_QUOTE) {
for (let j = i; j < input.length; ++j) {
let peekChar = input.charCodeAt(j)

if (peekChar === BACKSLASH) {
j += 1
} else if (peekChar === SINGLE_QUOTE) {
i = j
break
} else if (peekChar === LINE_BREAK) {
i = j
break
}
}
}
//
else if (c === DOUBLE_QUOTE) {
for (let j = i; j < input.length; ++j) {
let peekChar = input.charCodeAt(j)

if (peekChar === BACKSLASH) {
j += 1
} else if (peekChar === DOUBLE_QUOTE) {
i = j
break
} else if (peekChar === LINE_BREAK) {
i = j
break
}
}
}
//
else if (c === BACKTICK) {
for (let j = i; j < input.length; ++j) {
let peekChar = input.charCodeAt(j)

if (peekChar === BACKSLASH) {
j += 1
} else if (peekChar === BACKTICK) {
i = j
break
} else if (peekChar === LINE_BREAK) {
i = j
break
}
}
}
//
else if (c === SPACE || c === TAB) {
// do nothing
}
//
else if (c === SLASH) {
if (
prev === COMMA ||
prev === COLON ||
prev === EQUALS ||
prev === SEMICOLON ||
prev === BRACKET_OPEN ||
prev === QUESTION_MARK ||
prev === PAREN_OPEN ||
prev === CURLY_OPEN ||
prev === LINE_BREAK
) {
inRegex = true
regexStart = i
}
}
//
else {
prev = c
}
}

// Unterminated regex literal
if (inRegex) return input

if (regexStart === -1 || regexEnd === -1) return input

return (
input.slice(0, regexStart) + ' '.repeat(regexEnd - regexStart + 1) + input.slice(regexEnd + 1)
)
}
61 changes: 1 addition & 60 deletions packages/tailwindcss-language-service/src/util/find.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { createState, getDefaultTailwindSettings, Settings, type DocumentClassList } from './state'
import { test } from 'vitest'
import { TextDocument } from 'vscode-languageserver-textdocument'
import { findClassListsInHtmlRange } from './find'
import type { DeepPartial } from '../types'
import dedent from 'dedent'

const js = dedent
const html = dedent
import { js, html, createDocument } from './test-utils'

test('class regex works in astro', async ({ expect }) => {
let file = createDocument({
Expand Down Expand Up @@ -688,56 +682,3 @@ test('classFunctions should only match in JS-like contexts', async ({ expect })
},
])
})

function createDocument({
name,
lang,
content,
settings,
}: {
name: string
lang: string
content: string | string[]
settings: DeepPartial<Settings>
}) {
let doc = TextDocument.create(
`file://${name}`,
lang,
1,
typeof content === 'string' ? content : content.join('\n'),
)
let defaults = getDefaultTailwindSettings()
let state = createState({
editor: {
getConfiguration: async () => ({
...defaults,
...settings,
tailwindCSS: {
...defaults.tailwindCSS,
...settings.tailwindCSS,
lint: {
...defaults.tailwindCSS.lint,
...(settings.tailwindCSS?.lint ?? {}),
},
experimental: {
...defaults.tailwindCSS.experimental,
...(settings.tailwindCSS?.experimental ?? {}),
},
files: {
...defaults.tailwindCSS.files,
...(settings.tailwindCSS?.files ?? {}),
},
},
editor: {
...defaults.editor,
...settings.editor,
},
}),
},
})

return {
doc,
state,
}
}
Loading