Tweak code a bit

This commit is contained in:
Jordan Pittman 2025-11-10 09:18:55 -05:00
parent 53509a8231
commit 6f94fc231e
2 changed files with 141 additions and 70 deletions

View file

@ -8,6 +8,12 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
return CSS.parse(string.replaceAll(/\r?\n/g, lineEndings === 'Windows' ? '\r\n' : '\n'))
}
function parseWithLoc(string: string) {
return CSS.parse(string.replaceAll(/\r?\n/g, lineEndings === 'Windows' ? '\r\n' : '\n'), {
from: 'input.css',
})
}
describe('comments', () => {
it('should parse a comment and ignore it', () => {
expect(
@ -1145,7 +1151,20 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
color: blue;
}
`),
).toThrowErrorMatchingInlineSnapshot(`[Error: Missing opening {]`)
).toThrowErrorMatchingInlineSnapshot(`[CssSyntaxError: Missing opening {]`)
expect(() =>
parseWithLoc(`
.foo {
color: red;
}
.bar
/* ^ Missing opening { */
color: blue;
}
`),
).toThrowErrorMatchingInlineSnapshot(`[CssSyntaxError: input.css: 9:10: Missing opening {]`)
})
it('should error when curly brackets are unbalanced (closing)', () => {
@ -1160,7 +1179,22 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
/* ^ Missing closing } */
`),
).toThrowErrorMatchingInlineSnapshot(`[Error: Missing closing } at .bar]`)
).toThrowErrorMatchingInlineSnapshot(`[CssSyntaxError: Missing closing } at .bar]`)
expect(() =>
parseWithLoc(`
.foo {
color: red;
}
.bar {
color: blue;
/* ^ Missing closing } */
`),
).toThrowErrorMatchingInlineSnapshot(
`[CssSyntaxError: input.css: 7:12: Missing closing } at .bar]`,
)
})
it('should error when an unterminated string is used', () => {
@ -1172,7 +1206,19 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
font-weight: bold;
}
`),
).toThrowErrorMatchingInlineSnapshot(`[Error: Unterminated string: "Hello world!"]`)
).toThrowErrorMatchingInlineSnapshot(`[CssSyntaxError: Unterminated string: "Hello world!"]`)
expect(() =>
parseWithLoc(css`
.foo {
content: "Hello world!
/* ^ missing " */
font-weight: bold;
}
`),
).toThrowErrorMatchingInlineSnapshot(
`[CssSyntaxError: input.css: 3:21: Unterminated string: "Hello world!"]`,
)
})
it('should error when an unterminated string is used with a `;`', () => {
@ -1184,18 +1230,38 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
font-weight: bold;
}
`),
).toThrowErrorMatchingInlineSnapshot(`[Error: Unterminated string: "Hello world!;"]`)
).toThrowErrorMatchingInlineSnapshot(`[CssSyntaxError: Unterminated string: "Hello world!;"]`)
expect(() =>
parseWithLoc(css`
.foo {
content: "Hello world!;
/* ^ missing " */
font-weight: bold;
}
`),
).toThrowErrorMatchingInlineSnapshot(
`[CssSyntaxError: input.css: 3:21: Unterminated string: "Hello world!;"]`,
)
})
it('should error when incomplete custom properties are used', () => {
expect(() => parse('--foo')).toThrowErrorMatchingInlineSnapshot(
`[Error: Invalid custom property, expected a value]`,
`[CssSyntaxError: Invalid custom property, expected a value]`,
)
expect(() => parseWithLoc('--foo')).toThrowErrorMatchingInlineSnapshot(
`[CssSyntaxError: input.css: 1:0: Invalid custom property, expected a value]`,
)
})
it('should error when incomplete custom properties are used inside rules', () => {
expect(() => parse('.foo { --bar }')).toThrowErrorMatchingInlineSnapshot(
`[Error: Invalid custom property, expected a value]`,
`[CssSyntaxError: Invalid custom property, expected a value]`,
)
expect(() => parseWithLoc('.foo { --bar }')).toThrowErrorMatchingInlineSnapshot(
`[CssSyntaxError: input.css: 1:7: Invalid custom property, expected a value]`,
)
})
@ -1207,53 +1273,28 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
/* ^ missing ' * /;
}
`),
).toThrowErrorMatchingInlineSnapshot(`[Error: Unterminated string: 'Hello world!']`)
).toThrowErrorMatchingInlineSnapshot(`[CssSyntaxError: Unterminated string: 'Hello world!']`)
expect(() =>
parseWithLoc(css`
.foo {
--bar: 'Hello world!
/* ^ missing ' * /;
}
`),
).toThrowErrorMatchingInlineSnapshot(
`[CssSyntaxError: input.css: 3:19: Unterminated string: 'Hello world!']`,
)
})
it('should error when a declaration is incomplete', () => {
expect(() => parse('.foo { bar }')).toThrowErrorMatchingInlineSnapshot(
`[Error: Invalid declaration: \`bar\`]`,
`[CssSyntaxError: Invalid declaration: \`bar\`]`,
)
})
it('should include filename and line number in error messages when from option is provided', () => {
expect(() => {
CSS.parse('.test { */ }', { from: 'test.css' })
}).toThrow(/CssSyntaxError: Invalid declaration: `\*\/` at test\.css:1:10/)
})
it('should include filename and line number for multi-line CSS errors', () => {
const multiLineCss = `/* Test file */
.test {
color: red;
*/
}`
expect(() => {
CSS.parse(multiLineCss, { from: 'styles.css' })
}).toThrow(/CssSyntaxError: Invalid declaration: `\*\/` at styles\.css:4:4/)
})
it('should include filename and line number for missing opening brace errors', () => {
const cssWithMissingBrace = `.foo {
color: red;
}
.bar
color: blue;
}`
expect(() => {
CSS.parse(cssWithMissingBrace, { from: 'broken.css' })
}).toThrow(/CssSyntaxError: Missing opening \{ at broken\.css:7:2/)
})
it('should include filename and line number for unterminated string errors', () => {
const cssWithUnterminatedString = `.foo {
content: "Hello world!
font-weight: bold;
}`
expect(() => {
CSS.parse(cssWithUnterminatedString, { from: 'string-error.css' })
}).toThrow(/CssSyntaxError: Unterminated string: "Hello world!" at string-error\.css:2:13/)
expect(() => parseWithLoc('.foo { bar }')).toThrowErrorMatchingInlineSnapshot(
`[CssSyntaxError: input.css: 1:7: Invalid declaration: \`bar\`]`,
)
})
})
@ -1267,10 +1308,4 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
},
])
})
it('should not include filename when from option is not provided', () => {
expect(() => {
CSS.parse('.test { */ }')
}).toThrow(/CssSyntaxError: Invalid declaration: `\*\/`$/)
})
})

View file

@ -10,7 +10,7 @@ import {
type Rule,
} from './ast'
import { createLineTable } from './source-maps/line-table'
import type { Source } from './source-maps/source'
import type { Source, SourceLocation } from './source-maps/source'
const BACKSLASH = 0x5c
const SLASH = 0x2f
@ -41,14 +41,23 @@ export interface ParseOptions {
* CSS syntax error with source location information.
*/
export class CssSyntaxError extends Error {
constructor(message: string, source: Source | null, position: number) {
if (!source) {
super(message)
} else {
const { line, column } = createLineTable(source.code).find(position)
super(`${message} at ${source.file}:${line}:${column + 1}`)
loc: SourceLocation | null
constructor(message: string, loc: SourceLocation | null) {
if (loc) {
let source = loc[0]
let start = createLineTable(source.code).find(loc[1])
message = `${source.file}: ${start.line}:${start.column}: ${message}`
}
super(message)
this.name = 'CssSyntaxError'
this.loc = loc
if (Error.captureStackTrace) {
Error.captureStackTrace(this, CssSyntaxError)
}
}
}
@ -285,7 +294,12 @@ export function parse(input: string, opts?: ParseOptions) {
}
let declaration = parseDeclaration(buffer, colonIdx)
if (!declaration) throw new CssSyntaxError(`Invalid custom property, expected a value`, source, start)
if (!declaration) {
throw new CssSyntaxError(
`Invalid custom property, expected a value`,
source ? [source, start, i] : null,
)
}
if (source) {
declaration.src = [source, start, i]
@ -350,7 +364,10 @@ export function parse(input: string, opts?: ParseOptions) {
let declaration = parseDeclaration(buffer)
if (!declaration) {
if (buffer.length === 0) continue
throw new CssSyntaxError(`Invalid declaration: \`${buffer.trim()}\``, source, bufferStart)
throw new CssSyntaxError(
`Invalid declaration: \`${buffer.trim()}\``,
source ? [source, bufferStart, i] : null,
)
}
if (source) {
@ -407,7 +424,7 @@ export function parse(input: string, opts?: ParseOptions) {
closingBracketStack[closingBracketStack.length - 1] !== ')'
) {
if (closingBracketStack === '') {
throw new CssSyntaxError('Missing opening {', source, i)
throw new CssSyntaxError('Missing opening {', source ? [source, i, i] : null)
}
closingBracketStack = closingBracketStack.slice(0, -1)
@ -469,7 +486,12 @@ export function parse(input: string, opts?: ParseOptions) {
// Attach the declaration to the parent.
if (parent) {
let node = parseDeclaration(buffer, colonIdx)
if (!node) throw new CssSyntaxError(`Invalid declaration: \`${buffer.trim()}\``, source, bufferStart)
if (!node) {
throw new CssSyntaxError(
`Invalid declaration: \`${buffer.trim()}\``,
source ? [source, bufferStart, i] : null,
)
}
if (source) {
node.src = [source, bufferStart, i]
@ -508,7 +530,7 @@ export function parse(input: string, opts?: ParseOptions) {
// `)`
else if (currentChar === CLOSE_PAREN) {
if (closingBracketStack[closingBracketStack.length - 1] !== ')') {
throw new CssSyntaxError('Missing opening (', source, i)
throw new CssSyntaxError('Missing opening (', source ? [source, i, i] : null)
}
closingBracketStack = closingBracketStack.slice(0, -1)
@ -550,10 +572,17 @@ export function parse(input: string, opts?: ParseOptions) {
// have a leftover `parent`, then it means that we have an unterminated block.
if (closingBracketStack.length > 0 && parent) {
if (parent.kind === 'rule') {
throw new CssSyntaxError(`Missing closing } at ${parent.selector}`, source, input.length)
throw new CssSyntaxError(
`Missing closing } at ${parent.selector}`,
source ? [source, bufferStart, bufferStart] : null,
)
}
if (parent.kind === 'at-rule') {
throw new CssSyntaxError(`Missing closing } at ${parent.name} ${parent.params}`, source, input.length)
throw new CssSyntaxError(
`Missing closing } at ${parent.name} ${parent.params}`,
source ? [source, bufferStart, bufferStart] : null,
)
}
}
@ -610,7 +639,12 @@ function parseDeclaration(
)
}
function parseString(input: string, startIdx: number, quoteChar: number, source: Source | null = null): number {
function parseString(
input: string,
startIdx: number,
quoteChar: number,
source: Source | null = null,
): number {
let peekChar: number
// 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:
(input.charCodeAt(i + 1) === CARRIAGE_RETURN && input.charCodeAt(i + 2) === LINE_BREAK))
) {
throw new CssSyntaxError(
`Unterminated string: ${input.slice(startIdx, i + 1) + String.fromCharCode(quoteChar)}`, source, startIdx
`Unterminated string: ${input.slice(startIdx, i + 1) + String.fromCharCode(quoteChar)}`,
source ? [source, startIdx, i + 1] : null,
)
}
@ -672,7 +707,8 @@ function parseString(input: string, startIdx: number, quoteChar: number, source:
(peekChar === CARRIAGE_RETURN && input.charCodeAt(i + 1) === LINE_BREAK)
) {
throw new CssSyntaxError(
`Unterminated string: ${input.slice(startIdx, i) + String.fromCharCode(quoteChar)}`, source, startIdx
`Unterminated string: ${input.slice(startIdx, i) + String.fromCharCode(quoteChar)}`,
source ? [source, startIdx, i + 1] : null,
)
}
}