Skip to content

Commit 402a322

Browse files
authored
chore: more validation errors (#9723)
* invalid directive on component * duplicate animation * invalid animation * no const assignment * expected token * invalid-attribute-name * fixes * invalid event modifier * component name * slot validation * fix test * const validation + fix double declaration bug * omg this validation is skipped in svelte 4, remove it entirely then * gah * unskip * contenteditable * invalid css selector * css global selector + css parser fixes * export default * dynamic element * each block * html tag * logic block * reactive declaration * duplicate script * namespace * module context * slot * svelte fragment * textarea * title * transition * window bindings * changeset * svelte head, let directive, tweaks
1 parent d19e622 commit 402a322

File tree

209 files changed

+525
-928
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

209 files changed

+525
-928
lines changed

.changeset/gentle-sheep-hug.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+
chore: more validation errors

packages/svelte/src/compiler/errors.js

Lines changed: 70 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,12 @@ const special_elements = {
132132
'invalid-customElement-shadow-attribute': () => '"shadow" must be either "open" or "none"',
133133
'unknown-svelte-option-attribute': /** @param {string} name */ (name) =>
134134
`<svelte:options> unknown attribute '${name}'`,
135+
'illegal-svelte-head-attribute': () => '<svelte:head> cannot have attributes nor directives',
135136
'invalid-svelte-fragment-attribute': () =>
136137
`<svelte:fragment> can only have a slot attribute and (optionally) a let: directive`,
137138
'invalid-svelte-fragment-slot': () => `<svelte:fragment> slot attribute must have a static value`,
139+
'invalid-svelte-fragment-placement': () =>
140+
`<svelte:fragment> must be the direct child of a component`,
138141
/** @param {string} name */
139142
'invalid-svelte-element-placement': (name) =>
140143
`<${name}> tags cannot be inside elements or blocks`,
@@ -211,31 +214,75 @@ const elements = {
211214
* @param {string} node
212215
* @param {string} parent
213216
*/
214-
'invalid-node-placement': (node, parent) => `${node} is invalid inside <${parent}>`
217+
'invalid-node-placement': (node, parent) => `${node} is invalid inside <${parent}>`,
218+
'illegal-title-attribute': () => '<title> cannot have attributes nor directives',
219+
'invalid-title-content': () => '<title> can only contain text and {tags}'
215220
};
216221

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

222227
/** @satisfies {Errors} */
223228
const attributes = {
224229
'empty-attribute-shorthand': () => `Attribute shorthand cannot be empty`,
225230
'duplicate-attribute': () => `Attributes need to be unique`,
226231
'invalid-event-attribute-value': () =>
227-
`Event attribute must be a JavaScript expression, not a string`
232+
`Event attribute must be a JavaScript expression, not a string`,
233+
/** @param {string} name */
234+
'invalid-attribute-name': (name) => `'${name}' is not a valid attribute name`,
235+
/** @param {'no-each' | 'each-key' | 'child'} type */
236+
'invalid-animation': (type) =>
237+
type === 'no-each'
238+
? `An element that uses the animate directive must be the immediate child of a keyed each block`
239+
: type === 'each-key'
240+
? `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?`
241+
: `An element that uses the animate directive must be the sole child of a keyed each block`,
242+
'duplicate-animation': () => `An element can only have one 'animate' directive`,
243+
/** @param {string[] | undefined} [modifiers] */
244+
'invalid-event-modifier': (modifiers) =>
245+
modifiers
246+
? `Valid event modifiers are ${modifiers.slice(0, -1).join(', ')} or ${modifiers.slice(-1)}`
247+
: `Event modifiers other than 'once' can only be used on DOM elements`,
248+
/**
249+
* @param {string} modifier1
250+
* @param {string} modifier2
251+
*/
252+
'invalid-event-modifier-combination': (modifier1, modifier2) =>
253+
`The '${modifier1}' and '${modifier2}' modifiers cannot be used together`,
254+
/**
255+
* @param {string} directive1
256+
* @param {string} directive2
257+
*/
258+
'duplicate-transition': (directive1, directive2) => {
259+
/** @param {string} _directive */
260+
function describe(_directive) {
261+
return _directive === 'transition' ? "a 'transition'" : `an '${_directive}'`;
262+
}
263+
264+
return directive1 === directive2
265+
? `An element can only have one '${directive1}' directive`
266+
: `An element cannot have both ${describe(directive1)} directive and ${describe(
267+
directive2
268+
)} directive`;
269+
},
270+
'invalid-let-directive-placement': () => 'let directive at invalid position'
228271
};
229272

230273
/** @satisfies {Errors} */
231274
const slots = {
232275
'invalid-slot-element-attribute': () => `<slot> can only receive attributes, not directives`,
233276
'invalid-slot-attribute': () => `slot attribute must be a static value`,
234-
'invalid-slot-name': () => `slot attribute must be a static value`,
277+
/** @param {boolean} is_default */
278+
'invalid-slot-name': (is_default) =>
279+
is_default
280+
? `default is a reserved word — it cannot be used as a slot name`
281+
: `slot attribute must be a static value`,
235282
'invalid-slot-placement': () =>
236283
`Element with a slot='...' attribute must be a child of a component or a descendant of a custom element`,
237-
'duplicate-slot-name': /** @param {string} name @param {string} component */ (name, component) =>
238-
`Duplicate slot name '${name}' in <${component}>`,
284+
/** @param {string} name @param {string} component */
285+
'duplicate-slot-name': (name, component) => `Duplicate slot name '${name}' in <${component}>`,
239286
'invalid-default-slot-content': () =>
240287
`Found default slot content alongside an explicit slot="default"`
241288
};
@@ -256,13 +303,20 @@ const bindings = {
256303
'invalid-type-attribute': () =>
257304
`'type' attribute must be a static text value if input uses two-way binding`,
258305
'invalid-multiple-attribute': () =>
259-
`'multiple' attribute must be static if select uses two-way binding`
306+
`'multiple' attribute must be static if select uses two-way binding`,
307+
'missing-contenteditable-attribute': () =>
308+
`'contenteditable' attribute is required for textContent, innerHTML and innerText two-way bindings`,
309+
'dynamic-contenteditable-attribute': () =>
310+
`'contenteditable' attribute cannot be dynamic if element uses two-way binding`
260311
};
261312

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

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

336+
/** @satisfies {Errors} */
337+
const const_tag = {
338+
'invalid-const-placement': () =>
339+
`{@const} must be the immediate child of {#if}, {:else if}, {:else}, {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>`
340+
};
341+
282342
/** @satisfies {Errors} */
283343
const errors = {
284344
...internal,
@@ -293,7 +353,8 @@ const errors = {
293353
...bindings,
294354
...variables,
295355
...compiler_options,
296-
...legacy_reactivity
356+
...legacy_reactivity,
357+
...const_tag
297358

298359
// missing_contenteditable_attribute: {
299360
// code: 'missing-contenteditable-attribute',
@@ -304,34 +365,11 @@ const errors = {
304365
// code: 'dynamic-contenteditable-attribute',
305366
// message: "'contenteditable' attribute cannot be dynamic if element uses two-way binding"
306367
// },
307-
// invalid_event_modifier_combination: /**
308-
// * @param {string} modifier1
309-
// * @param {string} modifier2
310-
// */ (modifier1, modifier2) => ({
311-
// code: 'invalid-event-modifier',
312-
// message: `The '${modifier1}' and '${modifier2}' modifiers cannot be used together`
313-
// }),
314-
// invalid_event_modifier_legacy: /** @param {string} modifier */ (modifier) => ({
315-
// code: 'invalid-event-modifier',
316-
// message: `The '${modifier}' modifier cannot be used in legacy mode`
317-
// }),
318-
// invalid_event_modifier: /** @param {string} valid */ (valid) => ({
319-
// code: 'invalid-event-modifier',
320-
// message: `Valid event modifiers are ${valid}`
321-
// }),
322-
// invalid_event_modifier_component: {
323-
// code: 'invalid-event-modifier',
324-
// message: "Event modifiers other than 'once' can only be used on DOM elements"
325-
// },
326368
// textarea_duplicate_value: {
327369
// code: 'textarea-duplicate-value',
328370
// message:
329371
// 'A <textarea> can have either a value attribute or (equivalently) child content, but not both'
330372
// },
331-
// illegal_attribute: /** @param {string} name */ (name) => ({
332-
// code: 'illegal-attribute',
333-
// message: `'${name}' is not a valid attribute name`
334-
// }),
335373
// invalid_attribute_head: {
336374
// code: 'invalid-attribute',
337375
// message: '<svelte:head> should not have any attributes or directives'
@@ -340,10 +378,6 @@ const errors = {
340378
// code: 'invalid-action',
341379
// message: 'Actions can only be applied to DOM elements, not components'
342380
// },
343-
// invalid_animation: {
344-
// code: 'invalid-animation',
345-
// message: 'Animations can only be applied to DOM elements, not components'
346-
// },
347381
// invalid_class: {
348382
// code: 'invalid-class',
349383
// message: 'Classes can only be applied to DOM elements, not components'
@@ -364,22 +398,10 @@ const errors = {
364398
// code: 'dynamic-slot-name',
365399
// message: '<slot> name cannot be dynamic'
366400
// },
367-
// invalid_slot_name: {
368-
// code: 'invalid-slot-name',
369-
// message: 'default is a reserved word — it cannot be used as a slot name'
370-
// },
371401
// invalid_slot_attribute_value_missing: {
372402
// code: 'invalid-slot-attribute',
373403
// message: 'slot attribute value is missing'
374404
// },
375-
// invalid_slotted_content_fragment: {
376-
// code: 'invalid-slotted-content',
377-
// message: '<svelte:fragment> must be a child of a component'
378-
// },
379-
// illegal_attribute_title: {
380-
// code: 'illegal-attribute',
381-
// message: '<title> cannot have attributes'
382-
// },
383405
// illegal_structure_title: {
384406
// code: 'illegal-structure',
385407
// message: '<title> can only contain text and {tags}'
@@ -428,10 +450,6 @@ const errors = {
428450
// code: 'illegal-variable-declaration',
429451
// message: 'Cannot declare same variable name which is imported inside <script context="module">'
430452
// },
431-
// css_invalid_global: {
432-
// code: 'css-invalid-global',
433-
// message: ':global(...) can be at the start or end of a selector sequence, but not in the middle'
434-
// },
435453
// css_invalid_global_selector: {
436454
// code: 'css-invalid-global-selector',
437455
// message: ':global(...) must contain a single selector'
@@ -445,55 +463,15 @@ const errors = {
445463
// code: 'css-invalid-selector',
446464
// message: `Invalid selector "${selector}"`
447465
// }),
448-
// duplicate_animation: {
449-
// code: 'duplicate-animation',
450-
// message: "An element can only have one 'animate' directive"
451-
// },
452-
// invalid_animation_immediate: {
453-
// code: 'invalid-animation',
454-
// message:
455-
// 'An element that uses the animate directive must be the immediate child of a keyed each block'
456-
// },
457-
// invalid_animation_key: {
458-
// code: 'invalid-animation',
459-
// message:
460-
// '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?'
461-
// },
462-
// invalid_animation_sole: {
463-
// code: 'invalid-animation',
464-
// message:
465-
// 'An element that uses the animate directive must be the sole child of a keyed each block'
466-
// },
467-
// invalid_animation_dynamic_element: {
468-
// code: 'invalid-animation',
469-
// message: '<svelte:element> cannot have a animate directive'
470-
// },
471466
// invalid_directive_value: {
472467
// code: 'invalid-directive-value',
473468
// message:
474469
// 'Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)'
475470
// },
476-
// invalid_const_placement: {
477-
// code: 'invalid-const-placement',
478-
// message:
479-
// '{@const} must be the immediate child of {#if}, {:else if}, {:else}, {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>'
480-
// },
481-
// invalid_const_declaration: /** @param {string} name */ (name) => ({
482-
// code: 'invalid-const-declaration',
483-
// message: `'${name}' has already been declared`
484-
// }),
485-
// invalid_const_update: /** @param {string} name */ (name) => ({
486-
// code: 'invalid-const-update',
487-
// message: `'${name}' is declared using {@const ...} and is read-only`
488-
// }),
489471
// cyclical_const_tags: /** @param {string[]} cycle */ (cycle) => ({
490472
// code: 'cyclical-const-tags',
491473
// message: `Cyclical dependency detected: ${cycle.join(' → ')}`
492474
// }),
493-
// invalid_component_style_directive: {
494-
// code: 'invalid-component-style-directive',
495-
// message: 'Style directives cannot be used on components'
496-
// },
497475
// invalid_var_declaration: {
498476
// code: 'invalid_var_declaration',
499477
// message: '"var" scope should not extend outside the reactive block'

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,13 @@ export class Parser {
156156

157157
/** @param {string} str */
158158
match(str) {
159-
return this.template.slice(this.index, this.index + str.length) === str;
159+
const length = str.length;
160+
if (length === 1) {
161+
// more performant than slicing
162+
return this.template[this.index] === str;
163+
}
164+
165+
return this.template.slice(this.index, this.index + length) === str;
160166
}
161167

162168
/**

0 commit comments

Comments
 (0)