chore: fix compiler-errors test suite (#9754)

- fix compiler-errors test suite
- handle css nth-selector syntax (fixes #9765)

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
pull/9772/head
Rich Harris 2 years ago committed by GitHub
parent fd4a52c894
commit ede5dab230
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: fix compiler errors test suite

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: handle css nth-selector syntax

@ -24,9 +24,11 @@ const internal = {
const parse = { const parse = {
/** @param {string} name */ /** @param {string} name */
'unclosed-element': (name) => `<${name}> was left open`, 'unclosed-element': (name) => `<${name}> was left open`,
'unclosed-block': () => `block was left open`, 'unclosed-block': () => `Block was left open`,
'unexpected-block-close': () => `Unexpected block closing tag`, 'unexpected-block-close': () => `Unexpected block closing tag`,
'unexpected-eof': () => `Unexpected end of input`, /** @param {string} [expected] */
'unexpected-eof': (expected) =>
`Unexpected end of input` + (expected ? ` (expected ${expected})` : ''),
/** @param {string} message */ /** @param {string} message */
'js-parse-error': (message) => message, 'js-parse-error': (message) => message,
/** @param {string} token */ /** @param {string} token */
@ -39,17 +41,15 @@ const parse = {
'invalid-script-context': () => 'invalid-script-context': () =>
`If the context attribute is supplied, its value must be "module"`, `If the context attribute is supplied, its value must be "module"`,
'invalid-elseif': () => `'elseif' should be 'else if'`, 'invalid-elseif': () => `'elseif' should be 'else if'`,
/** 'invalid-continuing-block-placement': () =>
* @param {string} child `{:...} block is invalid at this position (did you forget to close the preceeding element or block?)`,
* @param {string} parent
*/
'invalid-block-parent': (child, parent) =>
`Expected to close ${parent} before seeing ${child} block`,
/** /**
* @param {string} child * @param {string} child
* @param {string} parent * @param {string} parent
*/ */
'invalid-block-missing-parent': (child, parent) => `${child} block must be a child of ${parent}`, 'invalid-block-missing-parent': (child, parent) => `${child} block must be a child of ${parent}`,
/** @param {string} name */
'duplicate-block-part': (name) => `${name} cannot appear more than once within a block`,
'expected-block-type': () => `Expected 'if', 'each', 'await', 'key' or 'snippet'`, 'expected-block-type': () => `Expected 'if', 'each', 'await', 'key' or 'snippet'`,
'expected-identifier': () => `Expected an identifier`, 'expected-identifier': () => `Expected an identifier`,
'invalid-debug': () => `{@debug ...} arguments must be identifiers, not arbitrary expressions`, 'invalid-debug': () => `{@debug ...} arguments must be identifiers, not arbitrary expressions`,
@ -98,12 +98,9 @@ const css = {
'invalid-css-empty-declaration': () => `Declaration cannot be empty`, 'invalid-css-empty-declaration': () => `Declaration cannot be empty`,
'invalid-css-global-placement': () => 'invalid-css-global-placement': () =>
`:global(...) can be at the start or end of a selector sequence, but not in the middle`, `:global(...) can be at the start or end of a selector sequence, but not in the middle`,
'invalid-css-global-selector': () => `:global(...) must contain exactly one selector`, 'invalid-css-global-selector': () => `:global(...) must contain exactly one selector`,
'invalid-css-global-selector-list': () => 'invalid-css-global-selector-list': () =>
`:global(...) cannot be used to modify a selector, or be modified by another selector`, `:global(...) cannot be used to modify a selector, or be modified by another selector`,
'invalid-css-selector': () => `Invalid selector`, 'invalid-css-selector': () => `Invalid selector`,
'invalid-css-identifier': () => 'Expected a valid CSS identifier' 'invalid-css-identifier': () => 'Expected a valid CSS identifier'
}; };

@ -77,8 +77,10 @@ export class Parser {
const current = this.current(); const current = this.current();
if (current.type === 'RegularElement') { if (current.type === 'RegularElement') {
current.end = current.start + 1;
error(current, 'unclosed-element', current.name); error(current, 'unclosed-element', current.name);
} else { } else {
current.end = current.start + 1;
error(current, 'unclosed-block'); error(current, 'unclosed-block');
} }
} }
@ -145,7 +147,7 @@ export class Parser {
if (required) { if (required) {
if (this.index === this.template.length) { if (this.index === this.template.length) {
error(this.index, 'unexpected-eof'); error(this.index, 'unexpected-eof', str);
} else { } else {
error(this.index, 'expected-token', str); error(this.index, 'expected-token', str);
} }

@ -6,6 +6,7 @@ const REGEX_ATTRIBUTE_FLAGS = /^[a-zA-Z]+/; // only `i` and `s` are valid today,
const REGEX_COMBINATOR_WHITESPACE = /^\s*(\+|~|>|\|\|)\s*/; const REGEX_COMBINATOR_WHITESPACE = /^\s*(\+|~|>|\|\|)\s*/;
const REGEX_COMBINATOR = /^(\+|~|>|\|\|)/; const REGEX_COMBINATOR = /^(\+|~|>|\|\|)/;
const REGEX_PERCENTAGE = /^\d+(\.\d+)?%/; const REGEX_PERCENTAGE = /^\d+(\.\d+)?%/;
const REGEX_NTH_OF = /^(even|odd|(-?[0-9]?n?(\s*\+\s*[0-9]+)?))(\s+of\s+)?/;
const REGEX_WHITESPACE_OR_COLON = /[\s:]/; const REGEX_WHITESPACE_OR_COLON = /[\s:]/;
const REGEX_BRACE_OR_SEMICOLON = /[{;]/; const REGEX_BRACE_OR_SEMICOLON = /[{;]/;
const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/; const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/;
@ -234,6 +235,8 @@ function read_selector(parser, inside_pseudo_class = false) {
if (parser.eat('(')) { if (parser.eat('(')) {
args = read_selector_list(parser, true); args = read_selector_list(parser, true);
parser.eat(')', true); parser.eat(')', true);
} else if (name === 'global') {
error(parser.index, 'invalid-css-global-selector');
} }
children.push({ children.push({
@ -291,6 +294,13 @@ function read_selector(parser, inside_pseudo_class = false) {
start, start,
end: parser.index end: parser.index
}); });
} else if (parser.match_regex(REGEX_NTH_OF)) {
children.push({
type: 'Nth',
value: /** @type {string} */ (parser.read(REGEX_NTH_OF)),
start,
end: parser.index
});
} else { } else {
let name = read_identifier(parser); let name = read_identifier(parser);
if (parser.match('|')) { if (parser.match('|')) {

@ -202,11 +202,13 @@ export default function tag(parser) {
let attribute; let attribute;
while ((attribute = read(parser))) { while ((attribute = read(parser))) {
if ( if (attribute.type === 'Attribute' || attribute.type === 'BindDirective') {
(attribute.type === 'Attribute' || attribute.type === 'BindDirective') && if (unique_names.includes(attribute.name)) {
unique_names.includes(attribute.name) error(attribute.start, 'duplicate-attribute');
) { // <svelte:element bind:this this=..> is allowed
error(attribute.start, 'duplicate-attribute'); } else if (attribute.name !== 'this') {
unique_names.push(attribute.name);
}
} }
element.attributes.push(attribute); element.attributes.push(attribute);
@ -635,13 +637,14 @@ function read_attribute_value(parser) {
'in attribute value' 'in attribute value'
); );
} catch (/** @type {any} e */ e) { } catch (/** @type {any} e */ e) {
if (e.code === 'parse-error') { if (e.code === 'js-parse-error') {
// if the attribute value didn't close + self-closing tag // if the attribute value didn't close + self-closing tag
// eg: `<Component test={{a:1} />` // eg: `<Component test={{a:1} />`
// acorn may throw a `Unterminated regular expression` because of `/>` // acorn may throw a `Unterminated regular expression` because of `/>`
if (parser.template.slice(e.pos - 1, e.pos + 1) === '/>') { const pos = e.position?.[0];
parser.index = e.pos; if (pos !== undefined && parser.template.slice(pos - 1, pos + 1) === '/>') {
error(e.pos, 'unclosed-attribute-value', quote_mark || '}'); parser.index = pos;
error(pos, 'unclosed-attribute-value', quote_mark || '}');
} }
} }
throw e; throw e;

@ -315,7 +315,7 @@ function next(parser) {
const block = parser.current(); // TODO type should not be TemplateNode, that's much too broad const block = parser.current(); // TODO type should not be TemplateNode, that's much too broad
if (block.type === 'IfBlock') { if (block.type === 'IfBlock') {
if (!parser.eat('else')) error(start, 'expected-token', 'else'); if (!parser.eat('else')) error(start, 'expected-token', '{:else} or {:else if}');
if (parser.eat('if')) error(start, 'invalid-elseif'); if (parser.eat('if')) error(start, 'invalid-elseif');
parser.allow_whitespace(); parser.allow_whitespace();
@ -359,7 +359,7 @@ function next(parser) {
} }
if (block.type === 'EachBlock') { if (block.type === 'EachBlock') {
if (!parser.eat('else')) error(start, 'expected-token', 'else'); if (!parser.eat('else')) error(start, 'expected-token', '{:else}');
parser.allow_whitespace(); parser.allow_whitespace();
parser.eat('}', true); parser.eat('}', true);
@ -375,7 +375,7 @@ function next(parser) {
if (block.type === 'AwaitBlock') { if (block.type === 'AwaitBlock') {
if (parser.eat('then')) { if (parser.eat('then')) {
if (block.then) { if (block.then) {
error(start, 'TODO', 'duplicate then'); error(start, 'duplicate-block-part', '{:then}');
} }
if (!parser.eat('}')) { if (!parser.eat('}')) {
@ -394,7 +394,7 @@ function next(parser) {
if (parser.eat('catch')) { if (parser.eat('catch')) {
if (block.catch) { if (block.catch) {
error(start, 'TODO', 'duplicate catch'); error(start, 'duplicate-block-part', '{:catch}');
} }
if (!parser.eat('}')) { if (!parser.eat('}')) {
@ -413,6 +413,8 @@ function next(parser) {
error(start, 'expected-token', '{:then ...} or {:catch ...}'); error(start, 'expected-token', '{:then ...} or {:catch ...}');
} }
error(start, 'invalid-continuing-block-placement');
} }
/** @param {import('../index.js').Parser} parser */ /** @param {import('../index.js').Parser} parser */

@ -306,7 +306,7 @@ function block_might_apply_to_node(block, node) {
while (i--) { while (i--) {
const selector = block.selectors[i]; const selector = block.selectors[i];
if (selector.type === 'Percentage') continue; if (selector.type === 'Percentage' || selector.type === 'Nth') continue;
const name = selector.name.replace(regex_backslash_and_following_character, '$1'); const name = selector.name.replace(regex_backslash_and_following_character, '$1');

@ -67,6 +67,11 @@ export interface Percentage extends BaseNode {
value: string; value: string;
} }
export interface Nth extends BaseNode {
type: 'Nth';
value: string;
}
export type SimpleSelector = export type SimpleSelector =
| TypeSelector | TypeSelector
| IdSelector | IdSelector
@ -74,7 +79,8 @@ export type SimpleSelector =
| AttributeSelector | AttributeSelector
| PseudoElementSelector | PseudoElementSelector
| PseudoClassSelector | PseudoClassSelector
| Percentage; | Percentage
| Nth;
export interface Combinator extends BaseNode { export interface Combinator extends BaseNode {
type: 'Combinator'; type: 'Combinator';

@ -3,7 +3,7 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'missing-attribute-value', code: 'missing-attribute-value',
message: 'Expected value for the attribute', message: 'Expected attribute value',
position: [12, 12] position: [12, 12]
} }
}); });

@ -2,8 +2,8 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'invalid-catch-placement', code: 'expected-token',
message: 'Expected to close {#each} block before seeing {:catch} block', message: 'Expected token {:else}',
position: [41, 41] position: [35, 35]
} }
}); });

@ -2,8 +2,9 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'invalid-catch-placement', code: 'invalid-continuing-block-placement',
message: 'Cannot have an {:catch} block outside an {#await ...} block', message:
position: [7, 7] '{:...} block is invalid at this position (did you forget to close the preceeding element or block?)',
position: [1, 1]
} }
}); });

@ -4,6 +4,6 @@ export default test({
error: { error: {
code: 'invalid-state-location', code: 'invalid-state-location',
message: '$state() can only be used as a variable declaration initializer or a class field', message: '$state() can only be used as a variable declaration initializer or a class field',
position: process.platform === 'win32' ? [35, 43] : [33, 41] position: [33, 41]
} }
}); });

@ -2,8 +2,8 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'unclosed-comment', code: 'unexpected-eof',
message: 'comment was left open, expected -->', message: 'Unexpected end of input (expected -->)',
position: [24, 24] position: [24, 24]
} }
}); });

@ -2,8 +2,8 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'css-syntax-error', code: 'invalid-css-global-selector',
message: ':global() must contain a selector', message: ':global(...) must contain exactly one selector',
position: [9, 9] position: [16, 16]
} }
}); });

@ -2,8 +2,8 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'css-syntax-error', code: 'invalid-css-identifier',
message: '"{" is expected', message: 'Expected a valid CSS identifier',
position: [24, 24] position: [25, 25]
} }
}); });

@ -2,8 +2,8 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'invalid-else-placement', code: 'expected-token',
message: 'Expected to close {#await} block before seeing {:else} block', message: 'Expected token {:then ...} or {:catch ...}',
position: [29, 29] position: [24, 24]
} }
}); });

@ -2,8 +2,9 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'invalid-else-placement', code: 'invalid-continuing-block-placement',
message: 'Cannot have an {:else} block outside an {#if ...} or {#each ...} block', message:
position: [11, 11] '{:...} block is invalid at this position (did you forget to close the preceeding element or block?)',
position: [6, 6]
} }
}); });

@ -2,8 +2,9 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'invalid-else-placement', code: 'invalid-continuing-block-placement',
message: 'Expected to close <li> tag before seeing {:else} block', message:
position: [23, 23] '{:...} block is invalid at this position (did you forget to close the preceeding element or block?)',
position: [18, 18]
} }
}); });

@ -2,8 +2,9 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'invalid-elseif-placement', code: 'invalid-continuing-block-placement',
message: 'Expected to close <p> tag before seeing {:else if ...} block', message:
position: [25, 25] '{:...} block is invalid at this position (did you forget to close the preceeding element or block?)',
position: [17, 17]
} }
}); });

@ -2,8 +2,8 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'invalid-elseif-placement', code: 'expected-token',
message: 'Expected to close {#await} block before seeing {:else if ...} block', message: 'Expected token {:then ...} or {:catch ...}',
position: [34, 34] position: [26, 26]
} }
}); });

@ -2,8 +2,8 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'invalid-elseif-placement', code: 'expected-token',
message: 'Cannot have an {:else if ...} block outside an {#if ...} block', message: 'Expected token {:then ...} or {:catch ...}',
position: [35, 35] position: [27, 27]
} }
}); });

@ -3,7 +3,7 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'empty-directive-name', code: 'empty-directive-name',
message: 'Class name cannot be empty', message: 'ClassDirective name cannot be empty',
position: [10, 10] position: [10, 10]
} }
}); });

@ -3,7 +3,7 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'empty-directive-name', code: 'empty-directive-name',
message: 'Action name cannot be empty', message: 'UseDirective name cannot be empty',
position: [8, 8] position: [8, 8]
} }
}); });

@ -4,6 +4,6 @@ export default test({
error: { error: {
code: 'invalid-derived-export', code: 'invalid-derived-export',
message: 'Cannot export derived state', message: 'Cannot export derived state',
position: process.platform === 'win32' ? [26, 68] : [24, 66] position: [24, 66]
} }
}); });

@ -4,6 +4,6 @@ export default test({
error: { error: {
code: 'invalid-state-export', code: 'invalid-state-export',
message: 'Cannot export state if it is reassigned', message: 'Cannot export state if it is reassigned',
position: process.platform === 'win32' ? [50, 90] : [46, 86] position: [46, 86]
} }
}); });

@ -2,7 +2,7 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'parse-error', code: 'js-parse-error',
message: 'Assigning to rvalue', message: 'Assigning to rvalue',
position: [1, 1] position: [1, 1]
} }

@ -2,8 +2,8 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'duplicate-style', code: 'duplicate-style-element',
message: 'You can only have one top-level <style> tag per component', message: 'A component can have a single top-level <style> element',
position: [58, 58] position: [58, 58]
} }
}); });

@ -2,7 +2,8 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: '', code: 'invalid-state-export',
message: 'Cannot export value created with $state' message: 'Cannot export state if it is reassigned',
position: [28, 53]
} }
}); });

@ -0,0 +1,3 @@
export const x = $state(0);
export let y = $state(0);
y = 1;

@ -2,8 +2,8 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'unclosed-script', code: 'unexpected-eof',
message: '<script> must have a closing tag', message: 'Unexpected end of input',
position: [32, 32] position: [32, 32]
} }
}); });

@ -2,8 +2,8 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'unclosed-script', code: 'unclosed-element',
message: '<script> must have a closing tag', message: '<script> was left open',
position: [32, 32] position: [32, 32]
} }
}); });

@ -4,7 +4,7 @@ export default test({
error: { error: {
code: 'invalid-self-placement', code: 'invalid-self-placement',
message: message:
'<svelte:self> components can only exist inside {#if} blocks, {#each} blocks,, {#snippet} blocks or slots passed to components', '<svelte:self> components can only exist inside {#if} blocks, {#each} blocks, {#snippet} blocks or slots passed to components',
position: [1, 1] position: [1, 1]
} }
}); });

@ -2,8 +2,8 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'unclosed-style', code: 'expected-token',
message: '<style> must have a closing tag', message: 'Expected token </style',
position: [31, 31] position: [31, 31]
} }
}); });

@ -2,8 +2,8 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'unclosed-style', code: 'invalid-css-identifier',
message: '<style> must have a closing tag', message: 'Expected a valid CSS identifier',
position: [31, 31] position: [9, 9]
} }
}); });

@ -2,9 +2,9 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'invalid-tag-name', code: 'invalid-svelte-tag',
message: message:
'Valid <svelte:...> tag names are svelte:head, svelte:options, svelte:window, svelte:document, svelte:body, svelte:self, svelte:component, svelte:fragment or svelte:element', 'Valid <svelte:...> tag names are svelte:head, svelte:options, svelte:window, svelte:document, svelte:body, svelte:element, svelte:component, svelte:self or svelte:fragment',
position: [10, 10] position: [10, 10]
} }
}); });

@ -2,8 +2,9 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'invalid-then-placement', code: 'invalid-continuing-block-placement',
message: 'Expected to close <li> tag before seeing {:then} block', message:
position: [26, 26] '{:...} block is invalid at this position (did you forget to close the preceeding element or block?)',
position: [21, 21]
} }
}); });

@ -2,8 +2,9 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'invalid-then-placement', code: 'invalid-continuing-block-placement',
message: 'Cannot have an {:then} block outside an {#await ...} block', message:
position: [6, 6] '{:...} block is invalid at this position (did you forget to close the preceeding element or block?)',
position: [1, 1]
} }
}); });

@ -3,7 +3,7 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'unclosed-attribute-value', code: 'unclosed-attribute-value',
message: 'Expected to close the attribute value with }', message: 'Expected closing } character',
position: [25, 25] position: [19, 19]
} }
}); });

@ -4,6 +4,6 @@ export default test({
error: { error: {
code: 'unclosed-block', code: 'unclosed-block',
message: 'Block was left open', message: 'Block was left open',
position: [0, 0] position: [0, 1]
} }
}); });

@ -4,6 +4,6 @@ export default test({
error: { error: {
code: 'unclosed-element', code: 'unclosed-element',
message: '<div> was left open', message: '<div> was left open',
position: [0, 0] position: [0, 1]
} }
}); });

@ -2,8 +2,8 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'invalid-closing-tag', code: 'invalid-closing-tag-after-autoclose',
message: '</p> attempted to close <p> that was already automatically closed by <pre>', message: '</p> attempted to close element that was already automatically closed by <pre>',
position: [24, 24] position: [24, 24]
} }
}); });

@ -3,7 +3,7 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'invalid-void-content', code: 'invalid-void-content',
message: '<input> is a void element and cannot have children, or a closing tag', message: 'Void elements cannot have children or closing tags',
position: [23, 23] position: [23, 23]
} }
}); });

@ -2,7 +2,7 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'invalid-window-content', code: 'invalid-element-content',
message: '<svelte:window> cannot have children', message: '<svelte:window> cannot have children',
position: [15, 15] position: [15, 15]
} }

@ -2,8 +2,8 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'duplicate-window', code: 'duplicate-svelte-element',
message: 'A component can only have one <svelte:window> tag', message: 'A component can only have one <svelte:window> element',
position: [17, 17] position: [17, 17]
} }
}); });

@ -2,7 +2,7 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'invalid-window-placement', code: 'invalid-svelte-element-placement',
message: '<svelte:window> tags cannot be inside elements or blocks', message: '<svelte:window> tags cannot be inside elements or blocks',
position: [11, 11] position: [11, 11]
} }

@ -2,7 +2,7 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'invalid-window-placement', code: 'invalid-svelte-element-placement',
message: '<svelte:window> tags cannot be inside elements or blocks', message: '<svelte:window> tags cannot be inside elements or blocks',
position: [7, 7] position: [7, 7]
} }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save