Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
cbd442b
invalid directive on component
dummdidumm Nov 30, 2023
ecedf7b
duplicate animation
dummdidumm Nov 30, 2023
a763756
invalid animation
dummdidumm Nov 30, 2023
7323d74
no const assignment
dummdidumm Nov 30, 2023
40b4e09
expected token
dummdidumm Nov 30, 2023
dd2c3d7
invalid-attribute-name
dummdidumm Nov 30, 2023
1518f1a
fixes
dummdidumm Nov 30, 2023
db94d18
invalid event modifier
dummdidumm Nov 30, 2023
8df058e
component name
dummdidumm Nov 30, 2023
03309a7
slot validation
dummdidumm Nov 30, 2023
7a032f2
fix test
dummdidumm Nov 30, 2023
6bd6ee6
const validation + fix double declaration bug
dummdidumm Nov 30, 2023
1e31c7a
omg this validation is skipped in svelte 4, remove it entirely then
dummdidumm Nov 30, 2023
33821c9
gah
dummdidumm Nov 30, 2023
c84dd59
unskip
dummdidumm Nov 30, 2023
b9b7d5d
contenteditable
dummdidumm Nov 30, 2023
43a5e0c
invalid css selector
dummdidumm Nov 30, 2023
5548332
css global selector + css parser fixes
dummdidumm Dec 1, 2023
b41bd43
export default
dummdidumm Dec 1, 2023
2d22f83
dynamic element
dummdidumm Dec 1, 2023
432339f
each block
dummdidumm Dec 1, 2023
850553b
html tag
dummdidumm Dec 1, 2023
3de7ff8
logic block
dummdidumm Dec 1, 2023
f2878d7
reactive declaration
dummdidumm Dec 1, 2023
155d0dd
duplicate script
dummdidumm Dec 1, 2023
bb231ed
namespace
dummdidumm Dec 1, 2023
655ba8a
module context
dummdidumm Dec 1, 2023
95c392f
slot
dummdidumm Dec 1, 2023
3321b52
svelte fragment
dummdidumm Dec 1, 2023
ecbff43
textarea
dummdidumm Dec 1, 2023
3d5f5ba
title
dummdidumm Dec 1, 2023
f1452a2
transition
dummdidumm Dec 1, 2023
7e60f80
window bindings
dummdidumm Dec 1, 2023
a1965cb
changeset
dummdidumm Dec 1, 2023
657130b
svelte head, let directive, tweaks
dummdidumm Dec 4, 2023
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/gentle-sheep-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

chore: more validation errors
162 changes: 70 additions & 92 deletions packages/svelte/src/compiler/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,12 @@ const special_elements = {
'invalid-customElement-shadow-attribute': () => '"shadow" must be either "open" or "none"',
'unknown-svelte-option-attribute': /** @param {string} name */ (name) =>
`<svelte:options> unknown attribute '${name}'`,
'illegal-svelte-head-attribute': () => '<svelte:head> cannot have attributes nor directives',
'invalid-svelte-fragment-attribute': () =>
`<svelte:fragment> can only have a slot attribute and (optionally) a let: directive`,
'invalid-svelte-fragment-slot': () => `<svelte:fragment> slot attribute must have a static value`,
'invalid-svelte-fragment-placement': () =>
`<svelte:fragment> must be the direct child of a component`,
/** @param {string} name */
'invalid-svelte-element-placement': (name) =>
`<${name}> tags cannot be inside elements or blocks`,
Expand Down Expand Up @@ -211,31 +214,75 @@ const elements = {
* @param {string} node
* @param {string} parent
*/
'invalid-node-placement': (node, parent) => `${node} is invalid inside <${parent}>`
'invalid-node-placement': (node, parent) => `${node} is invalid inside <${parent}>`,
'illegal-title-attribute': () => '<title> cannot have attributes nor directives',
'invalid-title-content': () => '<title> can only contain text and {tags}'
};

/** @satisfies {Errors} */
const components = {
'invalid-component-directive': () => `Directive is not valid on components`
'invalid-component-directive': () => `This type of directive is not valid on components`
};

/** @satisfies {Errors} */
const attributes = {
'empty-attribute-shorthand': () => `Attribute shorthand cannot be empty`,
'duplicate-attribute': () => `Attributes need to be unique`,
'invalid-event-attribute-value': () =>
`Event attribute must be a JavaScript expression, not a string`
`Event attribute must be a JavaScript expression, not a string`,
/** @param {string} name */
'invalid-attribute-name': (name) => `'${name}' is not a valid attribute name`,
/** @param {'no-each' | 'each-key' | 'child'} type */
'invalid-animation': (type) =>
type === 'no-each'
? `An element that uses the animate directive must be the immediate child of a keyed each block`
: type === 'each-key'
? `An element that uses the animate directive must be used inside a keyed each block. Did you forget to add a key to your each block?`
: `An element that uses the animate directive must be the sole child of a keyed each block`,
'duplicate-animation': () => `An element can only have one 'animate' directive`,
/** @param {string[] | undefined} [modifiers] */
'invalid-event-modifier': (modifiers) =>
modifiers
? `Valid event modifiers are ${modifiers.slice(0, -1).join(', ')} or ${modifiers.slice(-1)}`
: `Event modifiers other than 'once' can only be used on DOM elements`,
/**
* @param {string} modifier1
* @param {string} modifier2
*/
'invalid-event-modifier-combination': (modifier1, modifier2) =>
`The '${modifier1}' and '${modifier2}' modifiers cannot be used together`,
/**
* @param {string} directive1
* @param {string} directive2
*/
'duplicate-transition': (directive1, directive2) => {
/** @param {string} _directive */
function describe(_directive) {
return _directive === 'transition' ? "a 'transition'" : `an '${_directive}'`;
}

return directive1 === directive2
? `An element can only have one '${directive1}' directive`
: `An element cannot have both ${describe(directive1)} directive and ${describe(
directive2
)} directive`;
},
'invalid-let-directive-placement': () => 'let directive at invalid position'
};

/** @satisfies {Errors} */
const slots = {
'invalid-slot-element-attribute': () => `<slot> can only receive attributes, not directives`,
'invalid-slot-attribute': () => `slot attribute must be a static value`,
'invalid-slot-name': () => `slot attribute must be a static value`,
/** @param {boolean} is_default */
'invalid-slot-name': (is_default) =>
is_default
? `default is a reserved word — it cannot be used as a slot name`
: `slot attribute must be a static value`,
'invalid-slot-placement': () =>
`Element with a slot='...' attribute must be a child of a component or a descendant of a custom element`,
'duplicate-slot-name': /** @param {string} name @param {string} component */ (name, component) =>
`Duplicate slot name '${name}' in <${component}>`,
/** @param {string} name @param {string} component */
'duplicate-slot-name': (name, component) => `Duplicate slot name '${name}' in <${component}>`,
'invalid-default-slot-content': () =>
`Found default slot content alongside an explicit slot="default"`
};
Expand All @@ -256,13 +303,20 @@ const bindings = {
'invalid-type-attribute': () =>
`'type' attribute must be a static text value if input uses two-way binding`,
'invalid-multiple-attribute': () =>
`'multiple' attribute must be static if select uses two-way binding`
`'multiple' attribute must be static if select uses two-way binding`,
'missing-contenteditable-attribute': () =>
`'contenteditable' attribute is required for textContent, innerHTML and innerText two-way bindings`,
'dynamic-contenteditable-attribute': () =>
`'contenteditable' attribute cannot be dynamic if element uses two-way binding`
};

/** @satisfies {Errors} */
const variables = {
'illegal-global': /** @param {string} name */ (name) =>
`${name} is an illegal variable name. To reference a global variable called ${name}, use globalThis.${name}`
`${name} is an illegal variable name. To reference a global variable called ${name}, use globalThis.${name}`,
/** @param {string} name */
'duplicate-declaration': (name) => `'${name}' has already been declared`,
'default-export': () => `A component cannot have a default export`
};

/** @satisfies {Errors} */
Expand All @@ -279,6 +333,12 @@ const compiler_options = {
'removed-compiler-option': (msg) => `Invalid compiler option: ${msg}`
};

/** @satisfies {Errors} */
const const_tag = {
'invalid-const-placement': () =>
`{@const} must be the immediate child of {#if}, {:else if}, {:else}, {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>`
};

/** @satisfies {Errors} */
const errors = {
...internal,
Expand All @@ -293,7 +353,8 @@ const errors = {
...bindings,
...variables,
...compiler_options,
...legacy_reactivity
...legacy_reactivity,
...const_tag

// missing_contenteditable_attribute: {
// code: 'missing-contenteditable-attribute',
Expand All @@ -304,34 +365,11 @@ const errors = {
// code: 'dynamic-contenteditable-attribute',
// message: "'contenteditable' attribute cannot be dynamic if element uses two-way binding"
// },
// invalid_event_modifier_combination: /**
// * @param {string} modifier1
// * @param {string} modifier2
// */ (modifier1, modifier2) => ({
// code: 'invalid-event-modifier',
// message: `The '${modifier1}' and '${modifier2}' modifiers cannot be used together`
// }),
// invalid_event_modifier_legacy: /** @param {string} modifier */ (modifier) => ({
// code: 'invalid-event-modifier',
// message: `The '${modifier}' modifier cannot be used in legacy mode`
// }),
// invalid_event_modifier: /** @param {string} valid */ (valid) => ({
// code: 'invalid-event-modifier',
// message: `Valid event modifiers are ${valid}`
// }),
// invalid_event_modifier_component: {
// code: 'invalid-event-modifier',
// message: "Event modifiers other than 'once' can only be used on DOM elements"
// },
// textarea_duplicate_value: {
// code: 'textarea-duplicate-value',
// message:
// 'A <textarea> can have either a value attribute or (equivalently) child content, but not both'
// },
// illegal_attribute: /** @param {string} name */ (name) => ({
// code: 'illegal-attribute',
// message: `'${name}' is not a valid attribute name`
// }),
// invalid_attribute_head: {
// code: 'invalid-attribute',
// message: '<svelte:head> should not have any attributes or directives'
Expand All @@ -340,10 +378,6 @@ const errors = {
// code: 'invalid-action',
// message: 'Actions can only be applied to DOM elements, not components'
// },
// invalid_animation: {
// code: 'invalid-animation',
// message: 'Animations can only be applied to DOM elements, not components'
// },
// invalid_class: {
// code: 'invalid-class',
// message: 'Classes can only be applied to DOM elements, not components'
Expand All @@ -364,22 +398,10 @@ const errors = {
// code: 'dynamic-slot-name',
// message: '<slot> name cannot be dynamic'
// },
// invalid_slot_name: {
// code: 'invalid-slot-name',
// message: 'default is a reserved word — it cannot be used as a slot name'
// },
// invalid_slot_attribute_value_missing: {
// code: 'invalid-slot-attribute',
// message: 'slot attribute value is missing'
// },
// invalid_slotted_content_fragment: {
// code: 'invalid-slotted-content',
// message: '<svelte:fragment> must be a child of a component'
// },
// illegal_attribute_title: {
// code: 'illegal-attribute',
// message: '<title> cannot have attributes'
// },
// illegal_structure_title: {
// code: 'illegal-structure',
// message: '<title> can only contain text and {tags}'
Expand Down Expand Up @@ -428,10 +450,6 @@ const errors = {
// code: 'illegal-variable-declaration',
// message: 'Cannot declare same variable name which is imported inside <script context="module">'
// },
// css_invalid_global: {
// code: 'css-invalid-global',
// message: ':global(...) can be at the start or end of a selector sequence, but not in the middle'
// },
// css_invalid_global_selector: {
// code: 'css-invalid-global-selector',
// message: ':global(...) must contain a single selector'
Expand All @@ -445,55 +463,15 @@ const errors = {
// code: 'css-invalid-selector',
// message: `Invalid selector "${selector}"`
// }),
// duplicate_animation: {
// code: 'duplicate-animation',
// message: "An element can only have one 'animate' directive"
// },
// invalid_animation_immediate: {
// code: 'invalid-animation',
// message:
// 'An element that uses the animate directive must be the immediate child of a keyed each block'
// },
// invalid_animation_key: {
// code: 'invalid-animation',
// message:
// 'An element that uses the animate directive must be used inside a keyed each block. Did you forget to add a key to your each block?'
// },
// invalid_animation_sole: {
// code: 'invalid-animation',
// message:
// 'An element that uses the animate directive must be the sole child of a keyed each block'
// },
// invalid_animation_dynamic_element: {
// code: 'invalid-animation',
// message: '<svelte:element> cannot have a animate directive'
// },
// invalid_directive_value: {
// code: 'invalid-directive-value',
// message:
// 'Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)'
// },
// invalid_const_placement: {
// code: 'invalid-const-placement',
// message:
// '{@const} must be the immediate child of {#if}, {:else if}, {:else}, {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>'
// },
// invalid_const_declaration: /** @param {string} name */ (name) => ({
// code: 'invalid-const-declaration',
// message: `'${name}' has already been declared`
// }),
// invalid_const_update: /** @param {string} name */ (name) => ({
// code: 'invalid-const-update',
// message: `'${name}' is declared using {@const ...} and is read-only`
// }),
// cyclical_const_tags: /** @param {string[]} cycle */ (cycle) => ({
// code: 'cyclical-const-tags',
// message: `Cyclical dependency detected: ${cycle.join(' → ')}`
// }),
// invalid_component_style_directive: {
// code: 'invalid-component-style-directive',
// message: 'Style directives cannot be used on components'
// },
// invalid_var_declaration: {
// code: 'invalid_var_declaration',
// message: '"var" scope should not extend outside the reactive block'
Expand Down
8 changes: 7 additions & 1 deletion packages/svelte/src/compiler/phases/1-parse/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,13 @@ export class Parser {

/** @param {string} str */
match(str) {
return this.template.slice(this.index, this.index + str.length) === str;
const length = str.length;
if (length === 1) {
// more performant than slicing
return this.template[this.index] === str;
}

return this.template.slice(this.index, this.index + length) === str;
}

/**
Expand Down
Loading