chore: tidy up parser (#13045)

* simplify some parser logic, improve some compiler errors

* move logic into visitors

* more

* turns out we're doing a bunch of unnecessary work on closing tags

* tidy up

* changeset

* lint
pull/13057/head
Rich Harris 4 months ago committed by GitHub
parent 9cb9692aea
commit a1d1012e9d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: better compile errors for invalid tag names/placement

@ -8,6 +8,7 @@ import * as e from '../../errors.js';
import { create_fragment } from './utils/create.js'; import { create_fragment } from './utils/create.js';
import read_options from './read/options.js'; import read_options from './read/options.js';
import { is_reserved } from '../../../utils.js'; import { is_reserved } from '../../../utils.js';
import { disallow_children } from '../2-analyze/visitors/shared/special-element.js';
const regex_position_indicator = / \(\d+:\d+\)$/; const regex_position_indicator = / \(\d+:\d+\)$/;
@ -124,6 +125,9 @@ export class Parser {
const options = /** @type {SvelteOptionsRaw} */ (this.root.fragment.nodes[options_index]); const options = /** @type {SvelteOptionsRaw} */ (this.root.fragment.nodes[options_index]);
this.root.fragment.nodes.splice(options_index, 1); this.root.fragment.nodes.splice(options_index, 1);
this.root.options = read_options(options); this.root.options = read_options(options);
disallow_children(options);
// We need this for the old AST format // We need this for the old AST format
Object.defineProperty(this.root.options, '__raw__', { Object.defineProperty(this.root.options, '__raw__', {
value: options, value: options,

@ -12,12 +12,19 @@ import { create_fragment } from '../utils/create.js';
import { create_attribute, create_expression_metadata } from '../../nodes.js'; import { create_attribute, create_expression_metadata } from '../../nodes.js';
import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js'; import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js';
import { closing_tag_omitted } from '../../../../html-tree-validation.js'; import { closing_tag_omitted } from '../../../../html-tree-validation.js';
import { list } from '../../../utils/string.js';
// eslint-disable-next-line no-useless-escape const regex_invalid_unquoted_attribute_value = /^(\/>|[\s"'=<>`])/;
const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/; const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
const regex_closing_comment = /-->/;
/** Invalid attribute characters if the attribute is not surrounded by quotes */ const regex_component_name = /^(?:[A-Z]|[A-Za-z][A-Za-z0-9_$]*\.)/;
const regex_starts_with_invalid_attr_value = /^(\/>|[\s"'=<>`])/; const regex_valid_component_name =
/^(?:[A-Z][A-Za-z0-9_$.]*|[a-z][A-Za-z0-9_$]*\.[A-Za-z0-9_$])[A-Za-z0-9_$.]*$/;
const regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/;
const regex_token_ending_character = /[\s=/>"']/;
const regex_starts_with_quote_characters = /^["']/;
const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]+))/;
const regex_valid_tag_name = /^!?[a-zA-Z]{1,}:?[a-zA-Z0-9-]*/;
/** @type {Map<string, Compiler.ElementLike['type']>} */ /** @type {Map<string, Compiler.ElementLike['type']>} */
const root_only_meta_tags = new Map([ const root_only_meta_tags = new Map([
@ -37,47 +44,6 @@ const meta_tags = new Map([
['svelte:fragment', 'SvelteFragment'] ['svelte:fragment', 'SvelteFragment']
]); ]);
const valid_meta_tags = Array.from(meta_tags.keys());
const SELF = /^svelte:self(?=[\s/>])/;
const COMPONENT = /^svelte:component(?=[\s/>])/;
const SLOT = /^svelte:fragment(?=[\s/>])/;
const ELEMENT = /^svelte:element(?=[\s/>])/;
/** @param {Compiler.TemplateNode[]} stack */
function parent_is_head(stack) {
let i = stack.length;
while (i--) {
const { type } = stack[i];
if (type === 'SvelteHead') return true;
if (type === 'RegularElement' || type === 'Component') return false;
}
return false;
}
/** @param {Compiler.TemplateNode[]} stack */
function parent_is_shadowroot_template(stack) {
// https://developer.chrome.com/docs/css-ui/declarative-shadow-dom#building_a_declarative_shadow_root
let i = stack.length;
while (i--) {
if (
stack[i].type === 'RegularElement' &&
/** @type {Compiler.RegularElement} */ (stack[i]).attributes.some(
(a) => a.type === 'Attribute' && a.name === 'shadowrootmode'
)
) {
return true;
}
}
return false;
}
const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
const regex_closing_comment = /-->/;
const regex_component_name = /^(?:[A-Z]|[A-Za-z][A-Za-z0-9_$]*\.)/;
const regex_valid_component_name =
/^(?:[A-Z][A-Za-z0-9_$.]*|[a-z][A-Za-z0-9_$]*\.[A-Za-z0-9_$])[A-Za-z0-9_$.]*$/;
/** @param {Parser} parser */ /** @param {Parser} parser */
export default function element(parser) { export default function element(parser) {
const start = parser.index++; const start = parser.index++;
@ -100,31 +66,62 @@ export default function element(parser) {
} }
const is_closing_tag = parser.eat('/'); const is_closing_tag = parser.eat('/');
const name = parser.read_until(regex_whitespace_or_slash_or_closing_tag);
const name = read_tag_name(parser); if (is_closing_tag) {
parser.allow_whitespace();
parser.eat('>', true);
if (root_only_meta_tags.has(name)) { if (is_void(name)) {
if (is_closing_tag) { e.void_element_invalid_content(start);
if ( }
['svelte:options', 'svelte:window', 'svelte:body', 'svelte:document'].includes(name) &&
/** @type {Compiler.ElementLike} */ (parent).fragment.nodes.length
) {
e.svelte_meta_invalid_content(
/** @type {Compiler.ElementLike} */ (parent).fragment.nodes[0].start,
name
);
}
} else {
if (name in parser.meta_tags) {
e.svelte_meta_duplicate(start, name);
}
if (parent.type !== 'Root') { // close any elements that don't have their own closing tags, e.g. <div><p></div>
e.svelte_meta_invalid_placement(start, name); while (/** @type {Compiler.RegularElement} */ (parent).name !== name) {
if (parent.type !== 'RegularElement') {
if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) {
e.element_invalid_closing_tag_autoclosed(start, name, parser.last_auto_closed_tag.reason);
} else {
e.element_invalid_closing_tag(start, name);
}
} }
parser.meta_tags[name] = true; parent.end = start;
parser.pop();
parent = parser.current();
}
parent.end = parser.index;
parser.pop();
if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) {
parser.last_auto_closed_tag = undefined;
} }
return;
}
if (name.startsWith('svelte:') && !meta_tags.has(name)) {
const bounds = { start: start + 1, end: start + 1 + name.length };
e.svelte_meta_invalid_tag(bounds, list(Array.from(meta_tags.keys())));
}
if (!regex_valid_tag_name.test(name)) {
const bounds = { start: start + 1, end: start + 1 + name.length };
e.element_invalid_tag_name(bounds);
}
if (root_only_meta_tags.has(name)) {
if (name in parser.meta_tags) {
e.svelte_meta_duplicate(start, name);
}
if (parent.type !== 'Root') {
e.svelte_meta_invalid_placement(start, name);
}
parser.meta_tags[name] = true;
} }
const type = meta_tags.has(name) const type = meta_tags.has(name)
@ -175,38 +172,7 @@ export default function element(parser) {
parser.allow_whitespace(); parser.allow_whitespace();
if (is_closing_tag) { if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, name)) {
if (is_void(name)) {
e.void_element_invalid_content(start);
}
parser.eat('>', true);
// close any elements that don't have their own closing tags, e.g. <div><p></div>
while (/** @type {Compiler.RegularElement} */ (parent).name !== name) {
if (parent.type !== 'RegularElement') {
if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) {
e.element_invalid_closing_tag_autoclosed(start, name, parser.last_auto_closed_tag.reason);
} else {
e.element_invalid_closing_tag(start, name);
}
}
parent.end = start;
parser.pop();
parent = parser.current();
}
parent.end = parser.index;
parser.pop();
if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) {
parser.last_auto_closed_tag = undefined;
}
return;
} else if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, name)) {
parent.end = start; parent.end = start;
parser.pop(); parser.pop();
parser.last_auto_closed_tag = { parser.last_auto_closed_tag = {
@ -386,64 +352,34 @@ export default function element(parser) {
} }
} }
const regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/; /** @param {Compiler.TemplateNode[]} stack */
function parent_is_head(stack) {
/** @param {Parser} parser */ let i = stack.length;
function read_tag_name(parser) { while (i--) {
const start = parser.index; const { type } = stack[i];
if (type === 'SvelteHead') return true;
if (parser.read(SELF)) { if (type === 'RegularElement' || type === 'Component') return false;
// check we're inside a block, otherwise this
// will cause infinite recursion
let i = parser.stack.length;
let legal = false;
while (i--) {
const fragment = parser.stack[i];
if (
fragment.type === 'IfBlock' ||
fragment.type === 'EachBlock' ||
fragment.type === 'Component' ||
fragment.type === 'SnippetBlock'
) {
legal = true;
break;
}
}
if (!legal) {
e.svelte_self_invalid_placement(start);
}
return 'svelte:self';
}
if (parser.read(COMPONENT)) return 'svelte:component';
if (parser.read(ELEMENT)) return 'svelte:element';
if (parser.read(SLOT)) return 'svelte:fragment';
const name = parser.read_until(regex_whitespace_or_slash_or_closing_tag);
if (meta_tags.has(name)) return name;
if (name.startsWith('svelte:')) {
const list = `${valid_meta_tags.slice(0, -1).join(', ')} or ${valid_meta_tags[valid_meta_tags.length - 1]}`;
e.svelte_meta_invalid_tag(start, list);
} }
return false;
}
if (!valid_tag_name.test(name)) { /** @param {Compiler.TemplateNode[]} stack */
e.element_invalid_tag_name(start); function parent_is_shadowroot_template(stack) {
// https://developer.chrome.com/docs/css-ui/declarative-shadow-dom#building_a_declarative_shadow_root
let i = stack.length;
while (i--) {
if (
stack[i].type === 'RegularElement' &&
/** @type {Compiler.RegularElement} */ (stack[i]).attributes.some(
(a) => a.type === 'Attribute' && a.name === 'shadowrootmode'
)
) {
return true;
}
} }
return false;
return name;
} }
// eslint-disable-next-line no-useless-escape
const regex_token_ending_character = /[\s=\/>"']/;
const regex_starts_with_quote_characters = /^["']/;
const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]+))/;
/** /**
* @param {Parser} parser * @param {Parser} parser
* @returns {Compiler.Attribute | null} * @returns {Compiler.Attribute | null}
@ -692,7 +628,7 @@ function read_attribute_value(parser) {
() => { () => {
// handle common case of quote marks existing outside of regex for performance reasons // handle common case of quote marks existing outside of regex for performance reasons
if (quote_mark) return parser.match(quote_mark); if (quote_mark) return parser.match(quote_mark);
return !!parser.match_regex(regex_starts_with_invalid_attr_value); return !!parser.match_regex(regex_invalid_unquoted_attribute_value);
}, },
'in attribute value' 'in attribute value'
); );

@ -52,11 +52,14 @@ import { SlotElement } from './visitors/SlotElement.js';
import { SnippetBlock } from './visitors/SnippetBlock.js'; import { SnippetBlock } from './visitors/SnippetBlock.js';
import { SpreadAttribute } from './visitors/SpreadAttribute.js'; import { SpreadAttribute } from './visitors/SpreadAttribute.js';
import { StyleDirective } from './visitors/StyleDirective.js'; import { StyleDirective } from './visitors/StyleDirective.js';
import { SvelteBody } from './visitors/SvelteBody.js';
import { SvelteComponent } from './visitors/SvelteComponent.js'; import { SvelteComponent } from './visitors/SvelteComponent.js';
import { SvelteDocument } from './visitors/SvelteDocument.js';
import { SvelteElement } from './visitors/SvelteElement.js'; import { SvelteElement } from './visitors/SvelteElement.js';
import { SvelteFragment } from './visitors/SvelteFragment.js'; import { SvelteFragment } from './visitors/SvelteFragment.js';
import { SvelteHead } from './visitors/SvelteHead.js'; import { SvelteHead } from './visitors/SvelteHead.js';
import { SvelteSelf } from './visitors/SvelteSelf.js'; import { SvelteSelf } from './visitors/SvelteSelf.js';
import { SvelteWindow } from './visitors/SvelteWindow.js';
import { TaggedTemplateExpression } from './visitors/TaggedTemplateExpression.js'; import { TaggedTemplateExpression } from './visitors/TaggedTemplateExpression.js';
import { Text } from './visitors/Text.js'; import { Text } from './visitors/Text.js';
import { TitleElement } from './visitors/TitleElement.js'; import { TitleElement } from './visitors/TitleElement.js';
@ -158,11 +161,14 @@ const visitors = {
SnippetBlock, SnippetBlock,
SpreadAttribute, SpreadAttribute,
StyleDirective, StyleDirective,
SvelteHead, SvelteBody,
SvelteComponent,
SvelteDocument,
SvelteElement, SvelteElement,
SvelteFragment, SvelteFragment,
SvelteComponent, SvelteHead,
SvelteSelf, SvelteSelf,
SvelteWindow,
TaggedTemplateExpression, TaggedTemplateExpression,
Text, Text,
TitleElement, TitleElement,

@ -0,0 +1,12 @@
/** @import { SvelteBody } from '#compiler' */
/** @import { Context } from '../types' */
import { disallow_children } from './shared/special-element.js';
/**
* @param {SvelteBody} node
* @param {Context} context
*/
export function SvelteBody(node, context) {
disallow_children(node);
context.next();
}

@ -0,0 +1,12 @@
/** @import { SvelteDocument } from '#compiler' */
/** @import { Context } from '../types' */
import { disallow_children } from './shared/special-element.js';
/**
* @param {SvelteDocument} node
* @param {Context} context
*/
export function SvelteDocument(node, context) {
disallow_children(node);
context.next();
}

@ -1,11 +1,24 @@
/** @import { SvelteSelf } from '#compiler' */ /** @import { SvelteSelf } from '#compiler' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { visit_component } from './shared/component.js'; import { visit_component } from './shared/component.js';
import * as e from '../../../errors.js';
/** /**
* @param {SvelteSelf} node * @param {SvelteSelf} node
* @param {Context} context * @param {Context} context
*/ */
export function SvelteSelf(node, context) { export function SvelteSelf(node, context) {
const valid = context.path.some(
(node) =>
node.type === 'IfBlock' ||
node.type === 'EachBlock' ||
node.type === 'Component' ||
node.type === 'SnippetBlock'
);
if (!valid) {
e.svelte_self_invalid_placement(node);
}
visit_component(node, context); visit_component(node, context);
} }

@ -0,0 +1,12 @@
/** @import { SvelteWindow } from '#compiler' */
/** @import { Context } from '../types' */
import { disallow_children } from './shared/special-element.js';
/**
* @param {SvelteWindow} node
* @param {Context} context
*/
export function SvelteWindow(node, context) {
disallow_children(node);
context.next();
}

@ -0,0 +1,16 @@
/** @import { SvelteBody, SvelteDocument, SvelteOptionsRaw, SvelteWindow } from '#compiler' */
import * as e from '../../../../errors.js';
/**
* @param {SvelteBody | SvelteDocument | SvelteOptionsRaw | SvelteWindow} node
*/
export function disallow_children(node) {
const { nodes } = node.fragment;
if (nodes.length > 0) {
const first = nodes[0];
const last = nodes[nodes.length - 1];
e.svelte_meta_invalid_content({ start: first.start, end: last.end }, node.name);
}
}

@ -4,6 +4,6 @@ export default test({
error: { error: {
code: 'svelte_meta_invalid_content', code: 'svelte_meta_invalid_content',
message: '<svelte:options> cannot have children', message: '<svelte:options> cannot have children',
position: [16, 16] position: [16, 24]
} }
}); });

@ -5,6 +5,6 @@ export default test({
code: 'svelte_self_invalid_placement', code: 'svelte_self_invalid_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: [0, 14]
} }
}); });

@ -5,6 +5,6 @@ export default test({
code: 'svelte_meta_invalid_tag', code: 'svelte_meta_invalid_tag',
message: message:
'Valid `<svelte:...>` tag names are svelte:head, svelte:options, svelte:window, svelte:document, svelte:body, svelte:element, svelte:component, svelte:self or svelte:fragment', '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, 32]
} }
}); });

@ -4,6 +4,6 @@ export default test({
error: { error: {
code: 'svelte_meta_invalid_content', code: 'svelte_meta_invalid_content',
message: '<svelte:window> cannot have children', message: '<svelte:window> cannot have children',
position: [15, 15] position: [15, 23]
} }
}); });

Loading…
Cancel
Save