Skip to content

Commit a1d1012

Browse files
authored
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
1 parent 9cb9692 commit a1d1012

File tree

13 files changed

+173
-157
lines changed

13 files changed

+173
-157
lines changed

.changeset/happy-dolls-joke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: better compile errors for invalid tag names/placement

packages/svelte/src/compiler/phases/1-parse/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import * as e from '../../errors.js';
88
import { create_fragment } from './utils/create.js';
99
import read_options from './read/options.js';
1010
import { is_reserved } from '../../../utils.js';
11+
import { disallow_children } from '../2-analyze/visitors/shared/special-element.js';
1112

1213
const regex_position_indicator = / \(\d+:\d+\)$/;
1314

@@ -124,6 +125,9 @@ export class Parser {
124125
const options = /** @type {SvelteOptionsRaw} */ (this.root.fragment.nodes[options_index]);
125126
this.root.fragment.nodes.splice(options_index, 1);
126127
this.root.options = read_options(options);
128+
129+
disallow_children(options);
130+
127131
// We need this for the old AST format
128132
Object.defineProperty(this.root.options, '__raw__', {
129133
value: options,

packages/svelte/src/compiler/phases/1-parse/state/element.js

Lines changed: 87 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,19 @@ import { create_fragment } from '../utils/create.js';
1212
import { create_attribute, create_expression_metadata } from '../../nodes.js';
1313
import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js';
1414
import { closing_tag_omitted } from '../../../../html-tree-validation.js';
15+
import { list } from '../../../utils/string.js';
1516

16-
// eslint-disable-next-line no-useless-escape
17-
const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
18-
19-
/** Invalid attribute characters if the attribute is not surrounded by quotes */
20-
const regex_starts_with_invalid_attr_value = /^(\/>|[\s"'=<>`])/;
17+
const regex_invalid_unquoted_attribute_value = /^(\/>|[\s"'=<>`])/;
18+
const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
19+
const regex_closing_comment = /-->/;
20+
const regex_component_name = /^(?:[A-Z]|[A-Za-z][A-Za-z0-9_$]*\.)/;
21+
const regex_valid_component_name =
22+
/^(?:[A-Z][A-Za-z0-9_$.]*|[a-z][A-Za-z0-9_$]*\.[A-Za-z0-9_$])[A-Za-z0-9_$.]*$/;
23+
const regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/;
24+
const regex_token_ending_character = /[\s=/>"']/;
25+
const regex_starts_with_quote_characters = /^["']/;
26+
const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]+))/;
27+
const regex_valid_tag_name = /^!?[a-zA-Z]{1,}:?[a-zA-Z0-9-]*/;
2128

2229
/** @type {Map<string, Compiler.ElementLike['type']>} */
2330
const root_only_meta_tags = new Map([
@@ -37,47 +44,6 @@ const meta_tags = new Map([
3744
['svelte:fragment', 'SvelteFragment']
3845
]);
3946

40-
const valid_meta_tags = Array.from(meta_tags.keys());
41-
42-
const SELF = /^svelte:self(?=[\s/>])/;
43-
const COMPONENT = /^svelte:component(?=[\s/>])/;
44-
const SLOT = /^svelte:fragment(?=[\s/>])/;
45-
const ELEMENT = /^svelte:element(?=[\s/>])/;
46-
47-
/** @param {Compiler.TemplateNode[]} stack */
48-
function parent_is_head(stack) {
49-
let i = stack.length;
50-
while (i--) {
51-
const { type } = stack[i];
52-
if (type === 'SvelteHead') return true;
53-
if (type === 'RegularElement' || type === 'Component') return false;
54-
}
55-
return false;
56-
}
57-
58-
/** @param {Compiler.TemplateNode[]} stack */
59-
function parent_is_shadowroot_template(stack) {
60-
// https://developer.chrome.com/docs/css-ui/declarative-shadow-dom#building_a_declarative_shadow_root
61-
let i = stack.length;
62-
while (i--) {
63-
if (
64-
stack[i].type === 'RegularElement' &&
65-
/** @type {Compiler.RegularElement} */ (stack[i]).attributes.some(
66-
(a) => a.type === 'Attribute' && a.name === 'shadowrootmode'
67-
)
68-
) {
69-
return true;
70-
}
71-
}
72-
return false;
73-
}
74-
75-
const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
76-
const regex_closing_comment = /-->/;
77-
const regex_component_name = /^(?:[A-Z]|[A-Za-z][A-Za-z0-9_$]*\.)/;
78-
const regex_valid_component_name =
79-
/^(?:[A-Z][A-Za-z0-9_$.]*|[a-z][A-Za-z0-9_$]*\.[A-Za-z0-9_$])[A-Za-z0-9_$.]*$/;
80-
8147
/** @param {Parser} parser */
8248
export default function element(parser) {
8349
const start = parser.index++;
@@ -100,31 +66,62 @@ export default function element(parser) {
10066
}
10167

10268
const is_closing_tag = parser.eat('/');
69+
const name = parser.read_until(regex_whitespace_or_slash_or_closing_tag);
10370

104-
const name = read_tag_name(parser);
71+
if (is_closing_tag) {
72+
parser.allow_whitespace();
73+
parser.eat('>', true);
10574

106-
if (root_only_meta_tags.has(name)) {
107-
if (is_closing_tag) {
108-
if (
109-
['svelte:options', 'svelte:window', 'svelte:body', 'svelte:document'].includes(name) &&
110-
/** @type {Compiler.ElementLike} */ (parent).fragment.nodes.length
111-
) {
112-
e.svelte_meta_invalid_content(
113-
/** @type {Compiler.ElementLike} */ (parent).fragment.nodes[0].start,
114-
name
115-
);
116-
}
117-
} else {
118-
if (name in parser.meta_tags) {
119-
e.svelte_meta_duplicate(start, name);
120-
}
75+
if (is_void(name)) {
76+
e.void_element_invalid_content(start);
77+
}
12178

122-
if (parent.type !== 'Root') {
123-
e.svelte_meta_invalid_placement(start, name);
79+
// close any elements that don't have their own closing tags, e.g. <div><p></div>
80+
while (/** @type {Compiler.RegularElement} */ (parent).name !== name) {
81+
if (parent.type !== 'RegularElement') {
82+
if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) {
83+
e.element_invalid_closing_tag_autoclosed(start, name, parser.last_auto_closed_tag.reason);
84+
} else {
85+
e.element_invalid_closing_tag(start, name);
86+
}
12487
}
12588

126-
parser.meta_tags[name] = true;
89+
parent.end = start;
90+
parser.pop();
91+
92+
parent = parser.current();
93+
}
94+
95+
parent.end = parser.index;
96+
parser.pop();
97+
98+
if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) {
99+
parser.last_auto_closed_tag = undefined;
127100
}
101+
102+
return;
103+
}
104+
105+
if (name.startsWith('svelte:') && !meta_tags.has(name)) {
106+
const bounds = { start: start + 1, end: start + 1 + name.length };
107+
e.svelte_meta_invalid_tag(bounds, list(Array.from(meta_tags.keys())));
108+
}
109+
110+
if (!regex_valid_tag_name.test(name)) {
111+
const bounds = { start: start + 1, end: start + 1 + name.length };
112+
e.element_invalid_tag_name(bounds);
113+
}
114+
115+
if (root_only_meta_tags.has(name)) {
116+
if (name in parser.meta_tags) {
117+
e.svelte_meta_duplicate(start, name);
118+
}
119+
120+
if (parent.type !== 'Root') {
121+
e.svelte_meta_invalid_placement(start, name);
122+
}
123+
124+
parser.meta_tags[name] = true;
128125
}
129126

130127
const type = meta_tags.has(name)
@@ -175,38 +172,7 @@ export default function element(parser) {
175172

176173
parser.allow_whitespace();
177174

178-
if (is_closing_tag) {
179-
if (is_void(name)) {
180-
e.void_element_invalid_content(start);
181-
}
182-
183-
parser.eat('>', true);
184-
185-
// close any elements that don't have their own closing tags, e.g. <div><p></div>
186-
while (/** @type {Compiler.RegularElement} */ (parent).name !== name) {
187-
if (parent.type !== 'RegularElement') {
188-
if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) {
189-
e.element_invalid_closing_tag_autoclosed(start, name, parser.last_auto_closed_tag.reason);
190-
} else {
191-
e.element_invalid_closing_tag(start, name);
192-
}
193-
}
194-
195-
parent.end = start;
196-
parser.pop();
197-
198-
parent = parser.current();
199-
}
200-
201-
parent.end = parser.index;
202-
parser.pop();
203-
204-
if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) {
205-
parser.last_auto_closed_tag = undefined;
206-
}
207-
208-
return;
209-
} else if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, name)) {
175+
if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, name)) {
210176
parent.end = start;
211177
parser.pop();
212178
parser.last_auto_closed_tag = {
@@ -386,64 +352,34 @@ export default function element(parser) {
386352
}
387353
}
388354

389-
const regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/;
390-
391-
/** @param {Parser} parser */
392-
function read_tag_name(parser) {
393-
const start = parser.index;
394-
395-
if (parser.read(SELF)) {
396-
// check we're inside a block, otherwise this
397-
// will cause infinite recursion
398-
let i = parser.stack.length;
399-
let legal = false;
400-
401-
while (i--) {
402-
const fragment = parser.stack[i];
403-
if (
404-
fragment.type === 'IfBlock' ||
405-
fragment.type === 'EachBlock' ||
406-
fragment.type === 'Component' ||
407-
fragment.type === 'SnippetBlock'
408-
) {
409-
legal = true;
410-
break;
411-
}
412-
}
413-
414-
if (!legal) {
415-
e.svelte_self_invalid_placement(start);
416-
}
417-
418-
return 'svelte:self';
419-
}
420-
421-
if (parser.read(COMPONENT)) return 'svelte:component';
422-
if (parser.read(ELEMENT)) return 'svelte:element';
423-
424-
if (parser.read(SLOT)) return 'svelte:fragment';
425-
426-
const name = parser.read_until(regex_whitespace_or_slash_or_closing_tag);
427-
428-
if (meta_tags.has(name)) return name;
429-
430-
if (name.startsWith('svelte:')) {
431-
const list = `${valid_meta_tags.slice(0, -1).join(', ')} or ${valid_meta_tags[valid_meta_tags.length - 1]}`;
432-
e.svelte_meta_invalid_tag(start, list);
355+
/** @param {Compiler.TemplateNode[]} stack */
356+
function parent_is_head(stack) {
357+
let i = stack.length;
358+
while (i--) {
359+
const { type } = stack[i];
360+
if (type === 'SvelteHead') return true;
361+
if (type === 'RegularElement' || type === 'Component') return false;
433362
}
363+
return false;
364+
}
434365

435-
if (!valid_tag_name.test(name)) {
436-
e.element_invalid_tag_name(start);
366+
/** @param {Compiler.TemplateNode[]} stack */
367+
function parent_is_shadowroot_template(stack) {
368+
// https://developer.chrome.com/docs/css-ui/declarative-shadow-dom#building_a_declarative_shadow_root
369+
let i = stack.length;
370+
while (i--) {
371+
if (
372+
stack[i].type === 'RegularElement' &&
373+
/** @type {Compiler.RegularElement} */ (stack[i]).attributes.some(
374+
(a) => a.type === 'Attribute' && a.name === 'shadowrootmode'
375+
)
376+
) {
377+
return true;
378+
}
437379
}
438-
439-
return name;
380+
return false;
440381
}
441382

442-
// eslint-disable-next-line no-useless-escape
443-
const regex_token_ending_character = /[\s=\/>"']/;
444-
const regex_starts_with_quote_characters = /^["']/;
445-
const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]+))/;
446-
447383
/**
448384
* @param {Parser} parser
449385
* @returns {Compiler.Attribute | null}
@@ -692,7 +628,7 @@ function read_attribute_value(parser) {
692628
() => {
693629
// handle common case of quote marks existing outside of regex for performance reasons
694630
if (quote_mark) return parser.match(quote_mark);
695-
return !!parser.match_regex(regex_starts_with_invalid_attr_value);
631+
return !!parser.match_regex(regex_invalid_unquoted_attribute_value);
696632
},
697633
'in attribute value'
698634
);

packages/svelte/src/compiler/phases/2-analyze/index.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,14 @@ import { SlotElement } from './visitors/SlotElement.js';
5252
import { SnippetBlock } from './visitors/SnippetBlock.js';
5353
import { SpreadAttribute } from './visitors/SpreadAttribute.js';
5454
import { StyleDirective } from './visitors/StyleDirective.js';
55+
import { SvelteBody } from './visitors/SvelteBody.js';
5556
import { SvelteComponent } from './visitors/SvelteComponent.js';
57+
import { SvelteDocument } from './visitors/SvelteDocument.js';
5658
import { SvelteElement } from './visitors/SvelteElement.js';
5759
import { SvelteFragment } from './visitors/SvelteFragment.js';
5860
import { SvelteHead } from './visitors/SvelteHead.js';
5961
import { SvelteSelf } from './visitors/SvelteSelf.js';
62+
import { SvelteWindow } from './visitors/SvelteWindow.js';
6063
import { TaggedTemplateExpression } from './visitors/TaggedTemplateExpression.js';
6164
import { Text } from './visitors/Text.js';
6265
import { TitleElement } from './visitors/TitleElement.js';
@@ -158,11 +161,14 @@ const visitors = {
158161
SnippetBlock,
159162
SpreadAttribute,
160163
StyleDirective,
161-
SvelteHead,
164+
SvelteBody,
165+
SvelteComponent,
166+
SvelteDocument,
162167
SvelteElement,
163168
SvelteFragment,
164-
SvelteComponent,
169+
SvelteHead,
165170
SvelteSelf,
171+
SvelteWindow,
166172
TaggedTemplateExpression,
167173
Text,
168174
TitleElement,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/** @import { SvelteBody } from '#compiler' */
2+
/** @import { Context } from '../types' */
3+
import { disallow_children } from './shared/special-element.js';
4+
5+
/**
6+
* @param {SvelteBody} node
7+
* @param {Context} context
8+
*/
9+
export function SvelteBody(node, context) {
10+
disallow_children(node);
11+
context.next();
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/** @import { SvelteDocument } from '#compiler' */
2+
/** @import { Context } from '../types' */
3+
import { disallow_children } from './shared/special-element.js';
4+
5+
/**
6+
* @param {SvelteDocument} node
7+
* @param {Context} context
8+
*/
9+
export function SvelteDocument(node, context) {
10+
disallow_children(node);
11+
context.next();
12+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
/** @import { SvelteSelf } from '#compiler' */
22
/** @import { Context } from '../types' */
33
import { visit_component } from './shared/component.js';
4+
import * as e from '../../../errors.js';
45

56
/**
67
* @param {SvelteSelf} node
78
* @param {Context} context
89
*/
910
export function SvelteSelf(node, context) {
11+
const valid = context.path.some(
12+
(node) =>
13+
node.type === 'IfBlock' ||
14+
node.type === 'EachBlock' ||
15+
node.type === 'Component' ||
16+
node.type === 'SnippetBlock'
17+
);
18+
19+
if (!valid) {
20+
e.svelte_self_invalid_placement(node);
21+
}
22+
1023
visit_component(node, context);
1124
}

0 commit comments

Comments
 (0)