Skip to content

chore: tidy up parser #13045

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/happy-dolls-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: better compile errors for invalid tag names/placement
4 changes: 4 additions & 0 deletions packages/svelte/src/compiler/phases/1-parse/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as e from '../../errors.js';
import { create_fragment } from './utils/create.js';
import read_options from './read/options.js';
import { is_reserved } from '../../../utils.js';
import { disallow_children } from '../2-analyze/visitors/shared/special-element.js';

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

Expand Down Expand Up @@ -124,6 +125,9 @@ export class Parser {
const options = /** @type {SvelteOptionsRaw} */ (this.root.fragment.nodes[options_index]);
this.root.fragment.nodes.splice(options_index, 1);
this.root.options = read_options(options);

disallow_children(options);

// We need this for the old AST format
Object.defineProperty(this.root.options, '__raw__', {
value: options,
Expand Down
238 changes: 87 additions & 151 deletions packages/svelte/src/compiler/phases/1-parse/state/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,19 @@ import { create_fragment } from '../utils/create.js';
import { create_attribute, create_expression_metadata } from '../../nodes.js';
import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js';
import { closing_tag_omitted } from '../../../../html-tree-validation.js';
import { list } from '../../../utils/string.js';

// eslint-disable-next-line no-useless-escape
const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;

/** Invalid attribute characters if the attribute is not surrounded by quotes */
const regex_starts_with_invalid_attr_value = /^(\/>|[\s"'=<>`])/;
const regex_invalid_unquoted_attribute_value = /^(\/>|[\s"'=<>`])/;
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_$.]*$/;
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']>} */
const root_only_meta_tags = new Map([
Expand All @@ -37,47 +44,6 @@ const meta_tags = new Map([
['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 */
export default function element(parser) {
const start = parser.index++;
Expand All @@ -100,31 +66,62 @@ export default function element(parser) {
}

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_closing_tag) {
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 (is_void(name)) {
e.void_element_invalid_content(start);
}

if (parent.type !== 'Root') {
e.svelte_meta_invalid_placement(start, name);
// 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);
}
}

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)
Expand Down Expand Up @@ -175,38 +172,7 @@ export default function element(parser) {

parser.allow_whitespace();

if (is_closing_tag) {
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)) {
if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, name)) {
parent.end = start;
parser.pop();
parser.last_auto_closed_tag = {
Expand Down Expand Up @@ -386,64 +352,34 @@ export default function element(parser) {
}
}

const regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/;

/** @param {Parser} parser */
function read_tag_name(parser) {
const start = parser.index;

if (parser.read(SELF)) {
// 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);
/** @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;
}

if (!valid_tag_name.test(name)) {
e.element_invalid_tag_name(start);
/** @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 name;
return false;
}

// 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
* @returns {Compiler.Attribute | null}
Expand Down Expand Up @@ -692,7 +628,7 @@ function read_attribute_value(parser) {
() => {
// handle common case of quote marks existing outside of regex for performance reasons
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'
);
Expand Down
10 changes: 8 additions & 2 deletions packages/svelte/src/compiler/phases/2-analyze/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,14 @@ import { SlotElement } from './visitors/SlotElement.js';
import { SnippetBlock } from './visitors/SnippetBlock.js';
import { SpreadAttribute } from './visitors/SpreadAttribute.js';
import { StyleDirective } from './visitors/StyleDirective.js';
import { SvelteBody } from './visitors/SvelteBody.js';
import { SvelteComponent } from './visitors/SvelteComponent.js';
import { SvelteDocument } from './visitors/SvelteDocument.js';
import { SvelteElement } from './visitors/SvelteElement.js';
import { SvelteFragment } from './visitors/SvelteFragment.js';
import { SvelteHead } from './visitors/SvelteHead.js';
import { SvelteSelf } from './visitors/SvelteSelf.js';
import { SvelteWindow } from './visitors/SvelteWindow.js';
import { TaggedTemplateExpression } from './visitors/TaggedTemplateExpression.js';
import { Text } from './visitors/Text.js';
import { TitleElement } from './visitors/TitleElement.js';
Expand Down Expand Up @@ -158,11 +161,14 @@ const visitors = {
SnippetBlock,
SpreadAttribute,
StyleDirective,
SvelteHead,
SvelteBody,
SvelteComponent,
SvelteDocument,
SvelteElement,
SvelteFragment,
SvelteComponent,
SvelteHead,
SvelteSelf,
SvelteWindow,
TaggedTemplateExpression,
Text,
TitleElement,
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
/** @import { SvelteSelf } from '#compiler' */
/** @import { Context } from '../types' */
import { visit_component } from './shared/component.js';
import * as e from '../../../errors.js';

/**
* @param {SvelteSelf} node
* @param {Context} 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);
}
Loading
Loading