You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte/packages/svelte/src/compiler/errors.js

549 lines
23 KiB

/** @typedef {{ start?: number, end?: number }} NodeLike */
/** @typedef {Record<string, (...args: any[]) => string>} Errors */
/**
* @param {Array<string | number>} items
* @param {string} conjunction
*/
function list(items, conjunction = 'or') {
if (items.length === 1) return items[0];
return `${items.slice(0, -1).join(', ')} ${conjunction} ${items[items.length - 1]}`;
}
/** @satisfies {Errors} */
const internal = {
/** @param {string} message */
TODO: (message) => `TODO ${message}`,
/** @param {string} message */
INTERNAL: (message) =>
`Internal compiler error: ${message}. Please report this to https://github.com/sveltejs/svelte/issues`
};
/** @satisfies {Errors} */
const parse = {
/** @param {string} name */
'unclosed-element': (name) => `<${name}> was left open`,
'unclosed-block': () => `Block was left open`,
'unexpected-block-close': () => `Unexpected block closing tag`,
/** @param {string} [expected] */
'unexpected-eof': (expected) =>
`Unexpected end of input` + (expected ? ` (expected ${expected})` : ''),
/** @param {string} message */
'js-parse-error': (message) => message,
/** @param {string} token */
'expected-token': (token) => `Expected token ${token}`,
/** @param {string} word */
'unexpected-reserved-word': (word) =>
`'${word}' is a reserved word in JavaScript and cannot be used here`,
'missing-whitespace': () => `Expected whitespace`,
'expected-pattern': () => `Expected identifier or destructure pattern`,
'invalid-script-context': () =>
`If the context attribute is supplied, its value must be "module"`,
'invalid-elseif': () => `'elseif' should be 'else if'`,
'invalid-continuing-block-placement': () =>
`{:...} block is invalid at this position (did you forget to close the preceeding element or block?)`,
/**
* @param {string} child
* @param {string} parent
*/
'invalid-block-missing-parent': (child, parent) => `${child} block must be a child of ${parent}`,
/** @param {string} name */
'duplicate-block-part': (name) => `${name} cannot appear more than once within a block`,
'expected-block-type': () => `Expected 'if', 'each', 'await', 'key' or 'snippet'`,
'expected-identifier': () => `Expected an identifier`,
'invalid-debug': () => `{@debug ...} arguments must be identifiers, not arbitrary expressions`,
'invalid-const': () => `{@const ...} must be an assignment`,
/**
* @param {string} location
* @param {string} name
*/
'invalid-block-placement': (location, name) => `{#${name} ...} block cannot be ${location}`,
/**
* @param {string} location
* @param {string} name
*/
'invalid-tag-placement': (location, name) => `{@${name} ...} tag cannot be ${location}`,
'missing-attribute-value': () => `Expected attribute value`,
/** @param {string} delimiter */
'unclosed-attribute-value': (delimiter) => `Expected closing ${delimiter} character`,
'invalid-directive-value': () =>
`Directive value must be a JavaScript expression enclosed in curly braces`,
/** @param {string} type */
'empty-directive-name': (type) => `${type} name cannot be empty`,
/** @param {string} name */
'invalid-closing-tag': (name) => `</${name}> attempted to close an element that was not open`,
/**
* @param {string} name
* @param {string} reason
*/
'invalid-closing-tag-after-autoclose': (name, reason) =>
`</${name}> attempted to close element that was already automatically closed by <${reason}> (cannot nest <${reason}> inside <${name}>)`,
'invalid-dollar-binding': () =>
`The $ name is reserved, and cannot be used for variables and imports`,
'invalid-dollar-prefix': () =>
`The $ prefix is reserved, and cannot be used for variables and imports`,
'invalid-dollar-global': () =>
`The $ name is reserved. To reference a global variable called $, use globalThis.$`,
'illegal-subscription': () => `Cannot reference store value inside <script context="module">`,
'duplicate-style-element': () => `A component can have a single top-level <style> element`,
'duplicate-script-element': () =>
`A component can have a single top-level <script> element and/or a single top-level <script context="module"> element`,
'invalid-render-expression': () => '{@render ...} tags can only contain call expressions',
'invalid-render-arguments': () => 'expected at most one argument',
'invalid-render-call': () =>
'Calling a snippet function using apply, bind or call is not allowed',
'invalid-render-spread-argument': () => 'cannot use spread arguments in {@render ...} tags',
'invalid-snippet-rest-parameter': () =>
'snippets do not support rest parameters; use an array instead'
};
/** @satisfies {Errors} */
const css = {
/** @param {string} message */
'css-parse-error': (message) => message,
'invalid-css-empty-declaration': () => `Declaration cannot be empty`,
'invalid-css-global-block-list': () =>
`A :global {...} block cannot be part of a selector list with more than one item`,
'invalid-css-global-block-modifier': () =>
`A :global {...} block cannot modify an existing selector`,
/** @param {string} name */
'invalid-css-global-block-combinator': (name) =>
`A :global {...} block cannot follow a ${name} combinator`,
'invalid-css-global-block-declaration': () =>
`A :global {...} block can only contain rules, not declarations`,
'invalid-css-global-placement': () =>
`:global(...) can be at the start or end of a selector sequence, but not in the middle`,
'invalid-css-global-selector': () => `:global(...) must contain exactly one selector`,
'invalid-css-global-selector-list': () =>
`:global(...) must not contain type or universal selectors when used in a compound selector`,
'invalid-css-type-selector-placement': () =>
`:global(...) must not be followed with a type selector`,
'invalid-css-selector': () => `Invalid selector`,
'invalid-css-identifier': () => 'Expected a valid CSS identifier',
'invalid-nesting-selector': () => `Nesting selectors can only be used inside a rule`,
'invalid-css-declaration': () => 'Declaration cannot be empty'
};
/** @satisfies {Errors} */
const special_elements = {
'invalid-svelte-option-attribute': () => `<svelte:options> can only receive static attributes`,
'invalid-svelte-option-namespace': () =>
`Unsupported <svelte:option> value for "namespace". Valid values are "html", "svg" or "foreign".`,
'tag-option-deprecated': () => `"tag" option is deprecated — use "customElement" instead`,
'invalid-svelte-option-runes': () =>
`Unsupported <svelte:option> value for "runes". Valid values are true or false.`,
'invalid-svelte-option-accessors': () =>
'Unsupported <svelte:option> value for "accessors". Valid values are true or false.',
'invalid-svelte-option-preserveWhitespace': () =>
'Unsupported <svelte:option> value for "preserveWhitespace". Valid values are true or false.',
'invalid-svelte-option-immutable': () =>
'Unsupported <svelte:option> value for "immutable". Valid values are true or false.',
'invalid-tag-property': () => 'tag name must be two or more words joined by the "-" character',
'invalid-svelte-option-customElement': () =>
'"customElement" must be a string literal defining a valid custom element name or an object of the form ' +
'{ tag: string; shadow?: "open" | "none"; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }',
'invalid-customElement-props-attribute': () =>
'"props" must be a statically analyzable object literal of the form ' +
'"{ [key: string]: { attribute?: string; reflect?: boolean; type?: "String" | "Boolean" | "Number" | "Array" | "Object" }"',
'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`,
/** @param {string} name */
'duplicate-svelte-element': (name) => `A component can only have one <${name}> element`,
'invalid-self-placement': () =>
`<svelte:self> components can only exist inside {#if} blocks, {#each} blocks, {#snippet} blocks or slots passed to components`,
'missing-svelte-element-definition': () => `<svelte:element> must have a 'this' attribute`,
'missing-svelte-component-definition': () => `<svelte:component> must have a 'this' attribute`,
'invalid-svelte-element-definition': () => `Invalid element definition — must be an {expression}`,
'invalid-svelte-component-definition': () =>
`Invalid component definition — must be an {expression}`,
/**
* @param {string[]} tags
* @param {string | null} match
*/
'invalid-svelte-tag': (tags, match) =>
`Valid <svelte:...> tag names are ${list(tags)}${match ? ' (did you mean ' + match + '?)' : ''}`,
'conflicting-slot-usage': () =>
`Cannot use <slot> syntax and {@render ...} tags in the same component. Migrate towards {@render ...} tags completely.`
};
/** @satisfies {Errors} */
const runes = {
'invalid-legacy-props': () => `Cannot use $$props in runes mode`,
'invalid-legacy-rest-props': () => `Cannot use $$restProps in runes mode`,
'invalid-legacy-reactive-statement': () =>
`$: is not allowed in runes mode, use $derived or $effect instead`,
'invalid-legacy-export': () => `Cannot use \`export let\` in runes mode — use $props instead`,
/** @param {string} rune */
'invalid-rune-usage': (rune) => `Cannot use ${rune} rune in non-runes mode`,
'invalid-state-export': () =>
`Cannot export state from a module if it is reassigned. Either export a function returning the state value or only mutate the state value's properties`,
'invalid-derived-export': () =>
`Cannot export derived state from a module. To expose the current derived value, export a function returning its value`,
'invalid-props-id': () => `$props() can only be used with an object destructuring pattern`,
'invalid-props-pattern': () =>
`$props() assignment must not contain nested properties or computed keys`,
'invalid-props-location': () =>
`$props() can only be used at the top level of components as a variable declaration initializer`,
'invalid-bindable-location': () => `$bindable() can only be used inside a $props() declaration`,
/** @param {string} rune */
'invalid-state-location': (rune) =>
`${rune}(...) can only be used as a variable declaration initializer or a class field`,
'invalid-effect-location': () => `$effect() can only be used as an expression statement`,
'invalid-host-location': () =>
`$host() can only be used inside custom element component instances`,
/**
* @param {boolean} is_binding
* @param {boolean} show_details
*/
'invalid-const-assignment': (is_binding, show_details) =>
`Invalid ${is_binding ? 'binding' : 'assignment'} to const variable${
show_details
? ' ($derived values, let: directives, :then/:catch variables and @const declarations count as const)'
: ''
}`,
'invalid-derived-assignment': () => `Invalid assignment to derived state`,
'invalid-derived-binding': () => `Invalid binding to derived state`,
/**
* @param {string} rune
* @param {Array<number | string>} args
*/
'invalid-rune-args-length': (rune, args) =>
`${rune} can only be called with ${list(args, 'or')} ${
args.length === 1 && args[0] === 1 ? 'argument' : 'arguments'
}`,
/** @param {string} name */
'invalid-runes-mode-import': (name) => `${name} cannot be used in runes mode`,
'duplicate-props-rune': () => `Cannot use $props() more than once`,
'invalid-each-assignment': () =>
`Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. 'array[i] = value' instead of 'entry = value')`,
'invalid-snippet-assignment': () => `Cannot reassign or bind to snippet parameter`,
'invalid-derived-call': () => `$derived.call(...) has been replaced with $derived.by(...)`,
'conflicting-property-name': () =>
`Cannot have a property and a component export with the same name`
};
/** @satisfies {Errors} */
const elements = {
'invalid-textarea-content': () =>
`A <textarea> can have either a value attribute or (equivalently) child content, but not both`,
'invalid-void-content': () => `Void elements cannot have children or closing tags`,
/** @param {string} name */
'invalid-element-content': (name) => `<${name}> cannot have children`,
'invalid-tag-name': () => 'Expected valid tag name',
/**
* @param {string} node
* @param {string} 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': () => `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`,
/** @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',
'invalid-style-directive-modifier': () =>
`Invalid 'style:' modifier. Valid modifiers are: 'important'`,
'invalid-sequence-expression': () =>
`Sequence expressions are not allowed as attribute/directive values in runes mode, unless wrapped in parentheses`
};
/** @satisfies {Errors} */
const slots = {
'invalid-slot-element-attribute': () =>
`<slot> can only receive attributes and (optionally) let directives`,
'invalid-slot-attribute': () => `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`,
/** @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"`,
'conflicting-children-snippet': () =>
`Cannot use explicit children snippet at the same time as implicit children content. Remove either the non-whitespace content or the children snippet block`
};
/** @satisfies {Errors} */
const bindings = {
'invalid-binding-expression': () => `Can only bind to an Identifier or MemberExpression`,
'invalid-binding-value': () => `Can only bind to state or props`,
/**
* @param {string} binding
* @param {string} [elements]
* @param {string} [post]
*/
'invalid-binding': (binding, elements, post = '') =>
(elements
? `'${binding}' binding can only be used with ${elements}`
: `'${binding}' is not a valid binding`) + post,
'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`,
'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}`,
/** @param {string} name */
'duplicate-declaration': (name) => `'${name}' has already been declared`,
'default-export': () => `A component cannot have a default export`,
'illegal-variable-declaration': () =>
'Cannot declare same variable name which is imported inside <script context="module">',
'illegal-store-subscription': () =>
'Cannot subscribe to stores that are not declared at the top level of the component'
};
/** @satisfies {Errors} */
const legacy_reactivity = {
'cyclical-reactive-declaration': /** @param {string[]} cycle */ (cycle) =>
`Cyclical dependency detected: ${cycle.join(' → ')}`
};
/** @satisfies {Errors} */
const compiler_options = {
/** @param {string} msg */
'invalid-compiler-option': (msg) => `Invalid compiler option: ${msg}`,
/** @param {string} msg */
'removed-compiler-option': (msg) => `Invalid compiler option: ${msg}`
};
/** @satisfies {Errors} */
const const_tag = {
'invalid-const-placement': () =>
`{@const} must be the immediate child of {#snippet}, {#if}, {:else if}, {:else}, {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>`
};
/** @satisfies {Errors} */
const errors = {
...internal,
...parse,
...css,
...special_elements,
...runes,
...elements,
...components,
...attributes,
...slots,
...bindings,
...variables,
...compiler_options,
...legacy_reactivity,
...const_tag
// missing_contenteditable_attribute: {
// code: 'missing-contenteditable-attribute',
// message:
// "'contenteditable' attribute is required for textContent, innerHTML and innerText two-way bindings"
// },
// textarea_duplicate_value: {
// code: 'textarea-duplicate-value',
// message:
// 'A <textarea> can have either a value attribute or (equivalently) child content, but not both'
// },
// invalid_attribute_head: {
// code: 'invalid-attribute',
// message: '<svelte:head> should not have any attributes or directives'
// },
// invalid_action: {
// code: 'invalid-action',
// message: 'Actions 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'
// },
// invalid_transition: {
// code: 'invalid-transition',
// message: 'Transitions can only be applied to DOM elements, not components'
// },
// invalid_let: {
// code: 'invalid-let',
// message: 'let directive value must be an identifier or an object/array pattern'
// },
// invalid_slot_directive: {
// code: 'invalid-slot-directive',
// message: '<slot> cannot have directives'
// },
// dynamic_slot_name: {
// code: 'dynamic-slot-name',
// message: '<slot> name cannot be dynamic'
// },
// invalid_slot_attribute_value_missing: {
// code: 'invalid-slot-attribute',
// message: 'slot attribute value is missing'
// },
// illegal_structure_title: {
// code: 'illegal-structure',
// message: '<title> can only contain text and {tags}'
// },
// duplicate_transition: /**
// * @param {string} directive
// * @param {string} parent_directive
// */ (directive, parent_directive) => {
// /** @param {string} _directive */
// function describe(_directive) {
// return _directive === 'transition' ? "a 'transition'" : `an '${_directive}'`;
// }
// const message =
// directive === parent_directive
// ? `An element can only have one '${directive}' directive`
// : `An element cannot have both ${describe(parent_directive)} directive and ${describe(
// directive
// )} directive`;
// return {
// code: 'duplicate-transition',
// message
// };
// },
// default_export: {
// code: 'default-export',
// message: 'A component cannot have a default export'
// },
// illegal_declaration: {
// code: 'illegal-declaration',
// message: 'The $ prefix is reserved, and cannot be used for variable and import names'
// },
// 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]`)'
// },
};
// interface is duplicated between here (used internally) and ./interfaces.js
// (exposed publicly), and I'm not sure how to avoid that
export class CompileError extends Error {
name = 'CompileError';
/** @type {import('#compiler').CompileError['filename']} */
filename = undefined;
/** @type {import('#compiler').CompileError['position']} */
position = undefined;
/** @type {import('#compiler').CompileError['start']} */
start = undefined;
/** @type {import('#compiler').CompileError['end']} */
end = undefined;
/**
*
* @param {string} code
* @param {string} message
* @param {[number, number] | undefined} position
*/
constructor(code, message, position) {
super(message);
this.code = code;
this.position = position;
}
toString() {
let out = `${this.name}: ${this.message}`;
out += `\n(${this.code})`;
if (this.filename) {
out += `\n${this.filename}`;
if (this.start) {
out += `${this.start.line}:${this.start.column}`;
}
}
return out;
}
}
/**
* @template {Exclude<keyof typeof errors, 'TODO'>} T
* @param {NodeLike | number | null} node
* @param {T} code
* @param {Parameters<typeof errors[T]>} args
* @returns {never}
*/
export function error(node, code, ...args) {
const fn = errors[code];
// @ts-expect-error
const message = fn(...args);
const start = typeof node === 'number' ? node : node?.start;
const end = typeof node === 'number' ? node : node?.end;
throw new CompileError(
code,
message,
start !== undefined && end !== undefined ? [start, end] : undefined
);
}