Skip to content

Commit 6f94fc2

Browse files
committed
Tweak code a bit
1 parent 53509a8 commit 6f94fc2

File tree

2 files changed

+141
-70
lines changed

2 files changed

+141
-70
lines changed

packages/tailwindcss/src/css-parser.test.ts

Lines changed: 88 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
88
return CSS.parse(string.replaceAll(/\r?\n/g, lineEndings === 'Windows' ? '\r\n' : '\n'))
99
}
1010

11+
function parseWithLoc(string: string) {
12+
return CSS.parse(string.replaceAll(/\r?\n/g, lineEndings === 'Windows' ? '\r\n' : '\n'), {
13+
from: 'input.css',
14+
})
15+
}
16+
1117
describe('comments', () => {
1218
it('should parse a comment and ignore it', () => {
1319
expect(
@@ -1145,7 +1151,20 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
11451151
color: blue;
11461152
}
11471153
`),
1148-
).toThrowErrorMatchingInlineSnapshot(`[Error: Missing opening {]`)
1154+
).toThrowErrorMatchingInlineSnapshot(`[CssSyntaxError: Missing opening {]`)
1155+
1156+
expect(() =>
1157+
parseWithLoc(`
1158+
.foo {
1159+
color: red;
1160+
}
1161+
1162+
.bar
1163+
/* ^ Missing opening { */
1164+
color: blue;
1165+
}
1166+
`),
1167+
).toThrowErrorMatchingInlineSnapshot(`[CssSyntaxError: input.css: 9:10: Missing opening {]`)
11491168
})
11501169

11511170
it('should error when curly brackets are unbalanced (closing)', () => {
@@ -1160,7 +1179,22 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
11601179
11611180
/* ^ Missing closing } */
11621181
`),
1163-
).toThrowErrorMatchingInlineSnapshot(`[Error: Missing closing } at .bar]`)
1182+
).toThrowErrorMatchingInlineSnapshot(`[CssSyntaxError: Missing closing } at .bar]`)
1183+
1184+
expect(() =>
1185+
parseWithLoc(`
1186+
.foo {
1187+
color: red;
1188+
}
1189+
1190+
.bar {
1191+
color: blue;
1192+
1193+
/* ^ Missing closing } */
1194+
`),
1195+
).toThrowErrorMatchingInlineSnapshot(
1196+
`[CssSyntaxError: input.css: 7:12: Missing closing } at .bar]`,
1197+
)
11641198
})
11651199

11661200
it('should error when an unterminated string is used', () => {
@@ -1172,7 +1206,19 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
11721206
font-weight: bold;
11731207
}
11741208
`),
1175-
).toThrowErrorMatchingInlineSnapshot(`[Error: Unterminated string: "Hello world!"]`)
1209+
).toThrowErrorMatchingInlineSnapshot(`[CssSyntaxError: Unterminated string: "Hello world!"]`)
1210+
1211+
expect(() =>
1212+
parseWithLoc(css`
1213+
.foo {
1214+
content: "Hello world!
1215+
/* ^ missing " */
1216+
font-weight: bold;
1217+
}
1218+
`),
1219+
).toThrowErrorMatchingInlineSnapshot(
1220+
`[CssSyntaxError: input.css: 3:21: Unterminated string: "Hello world!"]`,
1221+
)
11761222
})
11771223

11781224
it('should error when an unterminated string is used with a `;`', () => {
@@ -1184,18 +1230,38 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
11841230
font-weight: bold;
11851231
}
11861232
`),
1187-
).toThrowErrorMatchingInlineSnapshot(`[Error: Unterminated string: "Hello world!;"]`)
1233+
).toThrowErrorMatchingInlineSnapshot(`[CssSyntaxError: Unterminated string: "Hello world!;"]`)
1234+
1235+
expect(() =>
1236+
parseWithLoc(css`
1237+
.foo {
1238+
content: "Hello world!;
1239+
/* ^ missing " */
1240+
font-weight: bold;
1241+
}
1242+
`),
1243+
).toThrowErrorMatchingInlineSnapshot(
1244+
`[CssSyntaxError: input.css: 3:21: Unterminated string: "Hello world!;"]`,
1245+
)
11881246
})
11891247

11901248
it('should error when incomplete custom properties are used', () => {
11911249
expect(() => parse('--foo')).toThrowErrorMatchingInlineSnapshot(
1192-
`[Error: Invalid custom property, expected a value]`,
1250+
`[CssSyntaxError: Invalid custom property, expected a value]`,
1251+
)
1252+
1253+
expect(() => parseWithLoc('--foo')).toThrowErrorMatchingInlineSnapshot(
1254+
`[CssSyntaxError: input.css: 1:0: Invalid custom property, expected a value]`,
11931255
)
11941256
})
11951257

11961258
it('should error when incomplete custom properties are used inside rules', () => {
11971259
expect(() => parse('.foo { --bar }')).toThrowErrorMatchingInlineSnapshot(
1198-
`[Error: Invalid custom property, expected a value]`,
1260+
`[CssSyntaxError: Invalid custom property, expected a value]`,
1261+
)
1262+
1263+
expect(() => parseWithLoc('.foo { --bar }')).toThrowErrorMatchingInlineSnapshot(
1264+
`[CssSyntaxError: input.css: 1:7: Invalid custom property, expected a value]`,
11991265
)
12001266
})
12011267

@@ -1207,53 +1273,28 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
12071273
/* ^ missing ' * /;
12081274
}
12091275
`),
1210-
).toThrowErrorMatchingInlineSnapshot(`[Error: Unterminated string: 'Hello world!']`)
1276+
).toThrowErrorMatchingInlineSnapshot(`[CssSyntaxError: Unterminated string: 'Hello world!']`)
1277+
1278+
expect(() =>
1279+
parseWithLoc(css`
1280+
.foo {
1281+
--bar: 'Hello world!
1282+
/* ^ missing ' * /;
1283+
}
1284+
`),
1285+
).toThrowErrorMatchingInlineSnapshot(
1286+
`[CssSyntaxError: input.css: 3:19: Unterminated string: 'Hello world!']`,
1287+
)
12111288
})
12121289

12131290
it('should error when a declaration is incomplete', () => {
12141291
expect(() => parse('.foo { bar }')).toThrowErrorMatchingInlineSnapshot(
1215-
`[Error: Invalid declaration: \`bar\`]`,
1292+
`[CssSyntaxError: Invalid declaration: \`bar\`]`,
12161293
)
1217-
})
1218-
1219-
it('should include filename and line number in error messages when from option is provided', () => {
1220-
expect(() => {
1221-
CSS.parse('.test { */ }', { from: 'test.css' })
1222-
}).toThrow(/CssSyntaxError: Invalid declaration: `\*\/` at test\.css:1:10/)
1223-
})
1224-
1225-
it('should include filename and line number for multi-line CSS errors', () => {
1226-
const multiLineCss = `/* Test file */
1227-
.test {
1228-
color: red;
1229-
*/
1230-
}`
1231-
expect(() => {
1232-
CSS.parse(multiLineCss, { from: 'styles.css' })
1233-
}).toThrow(/CssSyntaxError: Invalid declaration: `\*\/` at styles\.css:4:4/)
1234-
})
1235-
1236-
it('should include filename and line number for missing opening brace errors', () => {
1237-
const cssWithMissingBrace = `.foo {
1238-
color: red;
1239-
}
1240-
1241-
.bar
1242-
color: blue;
1243-
}`
1244-
expect(() => {
1245-
CSS.parse(cssWithMissingBrace, { from: 'broken.css' })
1246-
}).toThrow(/CssSyntaxError: Missing opening \{ at broken\.css:7:2/)
1247-
})
12481294

1249-
it('should include filename and line number for unterminated string errors', () => {
1250-
const cssWithUnterminatedString = `.foo {
1251-
content: "Hello world!
1252-
font-weight: bold;
1253-
}`
1254-
expect(() => {
1255-
CSS.parse(cssWithUnterminatedString, { from: 'string-error.css' })
1256-
}).toThrow(/CssSyntaxError: Unterminated string: "Hello world!" at string-error\.css:2:13/)
1295+
expect(() => parseWithLoc('.foo { bar }')).toThrowErrorMatchingInlineSnapshot(
1296+
`[CssSyntaxError: input.css: 1:7: Invalid declaration: \`bar\`]`,
1297+
)
12571298
})
12581299
})
12591300

@@ -1267,10 +1308,4 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
12671308
},
12681309
])
12691310
})
1270-
1271-
it('should not include filename when from option is not provided', () => {
1272-
expect(() => {
1273-
CSS.parse('.test { */ }')
1274-
}).toThrow(/CssSyntaxError: Invalid declaration: `\*\/`$/)
1275-
})
12761311
})

packages/tailwindcss/src/css-parser.ts

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
type Rule,
1111
} from './ast'
1212
import { createLineTable } from './source-maps/line-table'
13-
import type { Source } from './source-maps/source'
13+
import type { Source, SourceLocation } from './source-maps/source'
1414

1515
const BACKSLASH = 0x5c
1616
const SLASH = 0x2f
@@ -41,14 +41,23 @@ export interface ParseOptions {
4141
* CSS syntax error with source location information.
4242
*/
4343
export class CssSyntaxError extends Error {
44-
constructor(message: string, source: Source | null, position: number) {
45-
if (!source) {
46-
super(message)
47-
} else {
48-
const { line, column } = createLineTable(source.code).find(position)
49-
super(`${message} at ${source.file}:${line}:${column + 1}`)
44+
loc: SourceLocation | null
45+
46+
constructor(message: string, loc: SourceLocation | null) {
47+
if (loc) {
48+
let source = loc[0]
49+
let start = createLineTable(source.code).find(loc[1])
50+
message = `${source.file}: ${start.line}:${start.column}: ${message}`
5051
}
52+
53+
super(message)
54+
5155
this.name = 'CssSyntaxError'
56+
this.loc = loc
57+
58+
if (Error.captureStackTrace) {
59+
Error.captureStackTrace(this, CssSyntaxError)
60+
}
5261
}
5362
}
5463

@@ -285,7 +294,12 @@ export function parse(input: string, opts?: ParseOptions) {
285294
}
286295

287296
let declaration = parseDeclaration(buffer, colonIdx)
288-
if (!declaration) throw new CssSyntaxError(`Invalid custom property, expected a value`, source, start)
297+
if (!declaration) {
298+
throw new CssSyntaxError(
299+
`Invalid custom property, expected a value`,
300+
source ? [source, start, i] : null,
301+
)
302+
}
289303

290304
if (source) {
291305
declaration.src = [source, start, i]
@@ -350,7 +364,10 @@ export function parse(input: string, opts?: ParseOptions) {
350364
let declaration = parseDeclaration(buffer)
351365
if (!declaration) {
352366
if (buffer.length === 0) continue
353-
throw new CssSyntaxError(`Invalid declaration: \`${buffer.trim()}\``, source, bufferStart)
367+
throw new CssSyntaxError(
368+
`Invalid declaration: \`${buffer.trim()}\``,
369+
source ? [source, bufferStart, i] : null,
370+
)
354371
}
355372

356373
if (source) {
@@ -407,7 +424,7 @@ export function parse(input: string, opts?: ParseOptions) {
407424
closingBracketStack[closingBracketStack.length - 1] !== ')'
408425
) {
409426
if (closingBracketStack === '') {
410-
throw new CssSyntaxError('Missing opening {', source, i)
427+
throw new CssSyntaxError('Missing opening {', source ? [source, i, i] : null)
411428
}
412429

413430
closingBracketStack = closingBracketStack.slice(0, -1)
@@ -469,7 +486,12 @@ export function parse(input: string, opts?: ParseOptions) {
469486
// Attach the declaration to the parent.
470487
if (parent) {
471488
let node = parseDeclaration(buffer, colonIdx)
472-
if (!node) throw new CssSyntaxError(`Invalid declaration: \`${buffer.trim()}\``, source, bufferStart)
489+
if (!node) {
490+
throw new CssSyntaxError(
491+
`Invalid declaration: \`${buffer.trim()}\``,
492+
source ? [source, bufferStart, i] : null,
493+
)
494+
}
473495

474496
if (source) {
475497
node.src = [source, bufferStart, i]
@@ -508,7 +530,7 @@ export function parse(input: string, opts?: ParseOptions) {
508530
// `)`
509531
else if (currentChar === CLOSE_PAREN) {
510532
if (closingBracketStack[closingBracketStack.length - 1] !== ')') {
511-
throw new CssSyntaxError('Missing opening (', source, i)
533+
throw new CssSyntaxError('Missing opening (', source ? [source, i, i] : null)
512534
}
513535

514536
closingBracketStack = closingBracketStack.slice(0, -1)
@@ -550,10 +572,17 @@ export function parse(input: string, opts?: ParseOptions) {
550572
// have a leftover `parent`, then it means that we have an unterminated block.
551573
if (closingBracketStack.length > 0 && parent) {
552574
if (parent.kind === 'rule') {
553-
throw new CssSyntaxError(`Missing closing } at ${parent.selector}`, source, input.length)
575+
throw new CssSyntaxError(
576+
`Missing closing } at ${parent.selector}`,
577+
source ? [source, bufferStart, bufferStart] : null,
578+
)
554579
}
580+
555581
if (parent.kind === 'at-rule') {
556-
throw new CssSyntaxError(`Missing closing } at ${parent.name} ${parent.params}`, source, input.length)
582+
throw new CssSyntaxError(
583+
`Missing closing } at ${parent.name} ${parent.params}`,
584+
source ? [source, bufferStart, bufferStart] : null,
585+
)
557586
}
558587
}
559588

@@ -610,7 +639,12 @@ function parseDeclaration(
610639
)
611640
}
612641

613-
function parseString(input: string, startIdx: number, quoteChar: number, source: Source | null = null): number {
642+
function parseString(
643+
input: string,
644+
startIdx: number,
645+
quoteChar: number,
646+
source: Source | null = null,
647+
): number {
614648
let peekChar: number
615649

616650
// We need to ensure that the closing quote is the same as the opening
@@ -653,7 +687,8 @@ function parseString(input: string, startIdx: number, quoteChar: number, source:
653687
(input.charCodeAt(i + 1) === CARRIAGE_RETURN && input.charCodeAt(i + 2) === LINE_BREAK))
654688
) {
655689
throw new CssSyntaxError(
656-
`Unterminated string: ${input.slice(startIdx, i + 1) + String.fromCharCode(quoteChar)}`, source, startIdx
690+
`Unterminated string: ${input.slice(startIdx, i + 1) + String.fromCharCode(quoteChar)}`,
691+
source ? [source, startIdx, i + 1] : null,
657692
)
658693
}
659694

@@ -672,7 +707,8 @@ function parseString(input: string, startIdx: number, quoteChar: number, source:
672707
(peekChar === CARRIAGE_RETURN && input.charCodeAt(i + 1) === LINE_BREAK)
673708
) {
674709
throw new CssSyntaxError(
675-
`Unterminated string: ${input.slice(startIdx, i) + String.fromCharCode(quoteChar)}`, source, startIdx
710+
`Unterminated string: ${input.slice(startIdx, i) + String.fromCharCode(quoteChar)}`,
711+
source ? [source, startIdx, i + 1] : null,
676712
)
677713
}
678714
}

0 commit comments

Comments
 (0)