[chore] move compiler warnings/errors to dedicate files (#6503)

pull/6508/head
Simon H 3 years ago committed by GitHub
parent f2ca8bc64b
commit b662c7fd41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -34,6 +34,8 @@ import { apply_preprocessor_sourcemap } from '../utils/mapped_code';
import Element from './nodes/Element';
import { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping/dist/types/types';
import { clone } from '../utils/clone';
import compiler_warnings from './compiler_warnings';
import compiler_errors from './compiler_errors';
interface ComponentOptions {
namespace?: string;
@ -161,10 +163,7 @@ export default class Component {
const svelteOptions = ast.html.children.find(
child => child.name === 'svelte:options'
) || { start: 0, end: 0 };
this.warn(svelteOptions, {
code: 'custom-element-no-tag',
message: 'No custom element \'tag\' option was specified. To automatically register a custom element, specify a name with a hyphen in it, e.g. <svelte:options tag="my-thing"/>. To hide this warning, use <svelte:options tag={null}/>'
});
this.warn(svelteOptions, compiler_warnings.custom_element_no_tag);
}
this.tag = this.component_options.tag || compile_options.tag;
} else {
@ -478,18 +477,12 @@ export default class Component {
extract_exports(node) {
if (node.type === 'ExportDefaultDeclaration') {
this.error(node, {
code: 'default-export',
message: 'A component cannot have a default export'
});
this.error(node, compiler_errors.default_export);
}
if (node.type === 'ExportNamedDeclaration') {
if (node.source) {
this.error(node, {
code: 'not-implemented',
message: 'A component currently cannot have an export ... from'
});
this.error(node, compiler_errors.not_implemented);
}
if (node.declaration) {
if (node.declaration.type === 'VariableDeclaration') {
@ -498,10 +491,7 @@ export default class Component {
const variable = this.var_lookup.get(name);
variable.export_name = name;
if (variable.writable && !(variable.referenced || variable.referenced_from_script || variable.subscribable)) {
this.warn(declarator, {
code: 'unused-export-let',
message: `${this.name.name} has unused export property '${name}'. If it is for external reference only, please consider using \`export const ${name}\``
});
this.warn(declarator, compiler_warnings.unused_export_let(this.name.name, name));
}
});
});
@ -521,10 +511,7 @@ export default class Component {
variable.export_name = specifier.exported.name;
if (variable.writable && !(variable.referenced || variable.referenced_from_script || variable.subscribable)) {
this.warn(specifier, {
code: 'unused-export-let',
message: `${this.name.name} has unused export property '${specifier.exported.name}'. If it is for external reference only, please consider using \`export const ${specifier.exported.name}\``
});
this.warn(specifier, compiler_warnings.unused_export_let(this.name.name, specifier.exported.name));
}
}
});
@ -555,10 +542,7 @@ export default class Component {
walk(script.content, {
enter(node: Node) {
if (node.type === 'LabeledStatement' && node.label.name === '$') {
component.warn(node as any, {
code: 'module-script-reactive-declaration',
message: '$: has no effect in a module script'
});
component.warn(node as any, compiler_warnings.module_script_reactive_declaration);
}
}
});
@ -568,10 +552,7 @@ export default class Component {
scope.declarations.forEach((node, name) => {
if (name[0] === '$') {
this.error(node as any, {
code: 'illegal-declaration',
message: 'The $ prefix is reserved, and cannot be used for variable and import names'
});
this.error(node as any, compiler_errors.illegal_declaration);
}
const writable = node.type === 'VariableDeclaration' && (node.kind === 'var' || node.kind === 'let');
@ -586,10 +567,7 @@ export default class Component {
globals.forEach((node, name) => {
if (name[0] === '$') {
this.error(node as any, {
code: 'illegal-subscription',
message: 'Cannot reference store value inside <script context="module">'
});
this.error(node as any, compiler_errors.illegal_subscription);
} else {
this.add_var({
name,
@ -647,10 +625,7 @@ export default class Component {
instance_scope.declarations.forEach((node, name) => {
if (name[0] === '$') {
this.error(node as any, {
code: 'illegal-declaration',
message: 'The $ prefix is reserved, and cannot be used for variable and import names'
});
this.error(node as any, compiler_errors.illegal_declaration);
}
const writable = node.type === 'VariableDeclaration' && (node.kind === 'var' || node.kind === 'let');
@ -684,10 +659,7 @@ export default class Component {
});
} else if (name[0] === '$') {
if (name === '$' || name[1] === '$') {
this.error(node as any, {
code: 'illegal-global',
message: `${name} is an illegal variable name`
});
this.error(node as any, compiler_errors.illegal_global(name));
}
this.add_var({
@ -870,10 +842,7 @@ export default class Component {
node.label.name === '$' &&
parent.type !== 'Program'
) {
this.warn(node as any, {
code: 'non-top-level-reactive-declaration',
message: '$: has no effect outside of the top-level'
});
this.warn(node as any, compiler_warnings.non_top_level_reactive_declaration);
}
if (is_reference(node, parent)) {
@ -887,10 +856,7 @@ export default class Component {
if (name[1] !== '$' && scope.has(name.slice(1)) && scope.find_owner(name.slice(1)) !== this.instance_scope) {
if (!((/Function/.test(parent.type) && prop === 'params') || (parent.type === 'VariableDeclarator' && prop === 'id'))) {
this.error(node as any, {
code: 'contextual-store',
message: 'Stores must be declared at the top level of the component (this may change in a future version of Svelte)'
});
this.error(node as any, compiler_errors.contextual_store);
}
}
}
@ -955,10 +921,7 @@ export default class Component {
if (variable.export_name) {
// TODO is this still true post-#3539?
component.error(declarator as any, {
code: 'destructured-prop',
message: 'Cannot declare props in destructured declaration'
});
component.error(declarator as any, compiler_errors.destructured_prop);
}
if (variable.subscribable) {
@ -1248,10 +1211,7 @@ export default class Component {
variable.is_reactive_dependency = true;
if (variable.module) {
should_add_as_dependency = false;
component.warn(node as any, {
code: 'module-script-reactive-declaration',
message: `"${name}" is declared in a module script and will not be reactive`
});
component.warn(node as any, compiler_warnings.module_script_variable_reactive_declaration(name));
}
}
const is_writable_or_mutated =
@ -1316,10 +1276,7 @@ export default class Component {
if (cycle && cycle.length) {
const declarationList = lookup.get(cycle[0]);
const declaration = declarationList[0];
this.error(declaration.node, {
code: 'cyclical-reactive-declaration',
message: `Cyclical dependency detected: ${cycle.join(' → ')}`
});
this.error(declaration.node, compiler_errors.cyclical_reactive_declaration(cycle));
}
const add_declaration = declaration => {
@ -1342,10 +1299,7 @@ export default class Component {
warn_if_undefined(name: string, node, template_scope: TemplateScope) {
if (name[0] === '$') {
if (name === '$' || name[1] === '$' && !is_reserved_keyword(name)) {
this.error(node, {
code: 'illegal-global',
message: `${name} is an illegal variable name`
});
this.error(node, compiler_errors.illegal_global(name));
}
this.has_reactive_assignments = true; // TODO does this belong here?
@ -1359,15 +1313,7 @@ export default class Component {
if (template_scope && template_scope.names.has(name)) return;
if (globals.has(name) && node.type !== 'InlineComponent') return;
let message = `'${name}' is not defined`;
if (!this.ast.instance) {
message += `. Consider adding a <script> block with 'export let ${name}' to declare a prop`;
}
this.warn(node, {
code: 'missing-declaration',
message
});
this.warn(node, compiler_warnings.missing_declaration(name, !!this.ast.instance));
}
push_ignores(ignores) {
@ -1395,7 +1341,7 @@ function process_component_options(component: Component, nodes) {
const node = nodes.find(node => node.name === 'svelte:options');
function get_value(attribute, code, message) {
function get_value(attribute, {code, message}) {
const { value } = attribute;
const chunk = value[0];
@ -1421,26 +1367,18 @@ function process_component_options(component: Component, nodes) {
switch (name) {
case 'tag': {
const code = 'invalid-tag-attribute';
const message = "'tag' must be a string literal";
const tag = get_value(attribute, code, message);
const tag = get_value(attribute, compiler_errors.invalid_tag_attribute);
if (typeof tag !== 'string' && tag !== null) {
component.error(attribute, { code, message });
component.error(attribute, compiler_errors.invalid_tag_attribute);
}
if (tag && !/^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/.test(tag)) {
component.error(attribute, {
code: 'invalid-tag-property',
message: "tag name must be two or more words joined by the '-' character"
});
component.error(attribute, compiler_errors.invalid_tag_property);
}
if (tag && !component.compile_options.customElement) {
component.warn(attribute, {
code: 'missing-custom-element-compile-options',
message: "The 'tag' option is used when generating a custom element. Did you forget the 'customElement: true' compile option?"
});
component.warn(attribute, compiler_warnings.missing_custom_element_compile_options);
}
component_options.tag = tag;
@ -1448,27 +1386,15 @@ function process_component_options(component: Component, nodes) {
}
case 'namespace': {
const code = 'invalid-namespace-attribute';
const message = "The 'namespace' attribute must be a string literal representing a valid namespace";
const ns = get_value(attribute, code, message);
const ns = get_value(attribute, compiler_errors.invalid_namespace_attribute);
if (typeof ns !== 'string') {
component.error(attribute, { code, message });
component.error(attribute, compiler_errors.invalid_namespace_attribute);
}
if (valid_namespaces.indexOf(ns) === -1) {
const match = fuzzymatch(ns, valid_namespaces);
if (match) {
component.error(attribute, {
code: 'invalid-namespace-property',
message: `Invalid namespace '${ns}' (did you mean '${match}'?)`
});
} else {
component.error(attribute, {
code: 'invalid-namespace-property',
message: `Invalid namespace '${ns}'`
});
}
component.error(attribute, compiler_errors.invalid_namespace_property(ns, match));
}
component_options.namespace = ns;
@ -1478,12 +1404,10 @@ function process_component_options(component: Component, nodes) {
case 'accessors':
case 'immutable':
case 'preserveWhitespace': {
const code = `invalid-${name}-value`;
const message = `${name} attribute must be true or false`;
const value = get_value(attribute, code, message);
const value = get_value(attribute, compiler_errors.invalid_attribute_value(name));
if (typeof value !== 'boolean') {
component.error(attribute, { code, message });
component.error(attribute, compiler_errors.invalid_attribute_value(name));
}
component_options[name] = value;
@ -1491,16 +1415,10 @@ function process_component_options(component: Component, nodes) {
}
default:
component.error(attribute, {
code: 'invalid-options-attribute',
message: '<svelte:options> unknown attribute'
});
component.error(attribute, compiler_errors.invalid_options_attribute_unknown);
}
} else {
component.error(attribute, {
code: 'invalid-options-attribute',
message: "<svelte:options> can only have static 'tag', 'namespace', 'accessors', 'immutable' and 'preserveWhitespace' attributes"
});
component.error(attribute, compiler_errors.invalid_options_attribute);
}
});
}

@ -0,0 +1,253 @@
// All compiler errors should be listed and accessed from here
/**
* @internal
*/
export default {
invalid_binding_elements: (element: string, binding: string) => ({
code: 'invalid-binding',
message: `'${binding}' is not a valid binding on <${element}> elements`
}),
invalid_binding_element_with: (elements: string, binding: string) => ({
code: 'invalid-binding',
message: `'${binding}' binding can only be used with ${elements}`
}),
invalid_binding_on: (binding: string, element: string, post?: string) => ({
code: 'invalid-binding',
message: `'${binding}' is not a valid binding on ${element}` + (post || '')
}),
invalid_binding_foreign: (binding: string) => ({
code: 'invalid-binding',
message: `'${binding}' is not a valid binding. Foreign elements only support bind:this`
}),
invalid_binding_no_checkbox: (binding: string, is_radio: boolean) => ({
code: 'invalid-binding',
message: `'${binding}' binding can only be used with <input type="checkbox">` + (is_radio ? ' — for <input type="radio">, use \'group\' binding' : '')
}),
invalid_binding: (binding: string) => ({
code: 'invalid-binding',
message: `'${binding}' is not a valid binding`
}),
invalid_binding_window: (parts: string[]) => ({
code: 'invalid-binding',
message: `Bindings on <svelte:window> must be to top-level properties, e.g. '${parts[parts.length - 1]}' rather than '${parts.join('.')}'`
}),
invalid_binding_let: {
code: 'invalid-binding',
message: 'Cannot bind to a variable declared with the let: directive'
},
invalid_binding_await: {
code: 'invalid-binding',
message: 'Cannot bind to a variable declared with {#await ... then} or {:catch} blocks'
},
invalid_binding_writibale: {
code: 'invalid-binding',
message: 'Cannot bind to a variable which is not writable'
},
binding_undeclared: (name: string) => ({
code: 'binding-undeclared',
message: `${name} is not declared`
}),
invalid_type: {
code: 'invalid-type',
message: '\'type\' attribute cannot be dynamic if input uses two-way binding'
},
missing_type: {
code: 'missing-type',
message: '\'type\' attribute must be specified'
},
dynamic_multiple_attribute: {
code: 'dynamic-multiple-attribute',
message: '\'multiple\' attribute cannot be dynamic if select uses two-way binding'
},
missing_contenteditable_attribute: {
code: 'missing-contenteditable-attribute',
message: '\'contenteditable\' attribute is required for textContent and innerHTML two-way bindings'
},
dynamic_contenteditable_attribute: {
code: 'dynamic-contenteditable-attribute',
message: '\'contenteditable\' attribute cannot be dynamic if element uses two-way binding'
},
invalid_event_modifier_combination: (modifier1: string, modifier2: string) => ({
code: 'invalid-event-modifier',
message: `The '${modifier1}' and '${modifier2}' modifiers cannot be used together`
}),
invalid_event_modifier_legacy: (modifier: string) => ({
code: 'invalid-event-modifier',
message: `The '${modifier}' modifier cannot be used in legacy mode`
}),
invalid_event_modifier: (valid: string) => ({
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: (name: string) => ({
code: 'illegal-attribute',
message: `'${name}' is not a valid attribute name`
}),
invalid_slot_attribute: {
code: 'invalid-slot-attribute',
message: 'slot attribute cannot have a dynamic value'
},
duplicate_slot_attribute: (name: string) => ({
code: 'duplicate-slot-attribute',
message: `Duplicate '${name}' slot`
}),
invalid_slotted_content: {
code: 'invalid-slotted-content',
message: 'Element with a slot=\'...\' attribute must be a child of a component or a descendant of a custom element'
},
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_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}'
},
duplicate_transition: (directive: string, parent_directive: string) => {
function describe(_directive: string) {
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
};
},
contextual_store: {
code: 'contextual-store',
message: 'Stores must be declared at the top level of the component (this may change in a future version of Svelte)'
},
default_export: {
code: 'default-export',
message: 'A component cannot have a default export'
},
not_implemented: {
code: 'not-implemented',
message: 'A component currently cannot have an export ... from'
},
illegal_declaration: {
code: 'illegal-declaration',
message: 'The $ prefix is reserved, and cannot be used for variable and import names'
},
illegal_subscription: {
code: 'illegal-subscription',
message: 'Cannot reference store value inside <script context="module">'
},
illegal_global: (name: string) => ({
code: 'illegal-global',
message: `${name} is an illegal variable name`
}),
destructured_prop: {
code: 'destructured-prop',
message: 'Cannot declare props in destructured declaration'
},
cyclical_reactive_declaration: (cycle: string[]) => ({
code: 'cyclical-reactive-declaration',
message: `Cyclical dependency detected: ${cycle.join(' → ')}`
}),
invalid_tag_property: {
code: 'invalid-tag-property',
message: "tag name must be two or more words joined by the '-' character"
},
invalid_tag_attribute: {
code: 'invalid-tag-attribute',
message: "'tag' must be a string literal"
},
invalid_namespace_property: (namespace: string, suggestion?: string) => ({
code: 'invalid-namespace-property',
message: `Invalid namespace '${namespace}'` + (suggestion ? ` (did you mean '${suggestion}'?)` : '')
}),
invalid_namespace_attribute: {
code: 'invalid-namespace-attribute',
message: "The 'namespace' attribute must be a string literal representing a valid namespace"
},
invalid_attribute_value: (name: string) => ({
code: `invalid-${name}-value`,
message: `${name} attribute must be true or false`
}),
invalid_options_attribute_unknown: {
code: 'invalid-options-attribute',
message: '<svelte:options> unknown attribute'
},
invalid_options_attribute: {
code: 'invalid-options-attribute',
message: "<svelte:options> can only have static 'tag', 'namespace', 'accessors', 'immutable' and 'preserveWhitespace' attributes"
},
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'
},
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_sole: {
code: 'invalid-animation',
message: 'An element that uses the animate directive must be the sole child of a keyed each block'
},
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]`)'
}
};

@ -0,0 +1,143 @@
// All compiler warnings should be listed and accessed from here
/**
* @internal
*/
export default {
custom_element_no_tag: {
code: 'custom-element-no-tag',
message: 'No custom element \'tag\' option was specified. To automatically register a custom element, specify a name with a hyphen in it, e.g. <svelte:options tag="my-thing"/>. To hide this warning, use <svelte:options tag={null}/>'
},
unused_export_let: (component: string, property: string) => ({
code: 'unused-export-let',
message: `${component} has unused export property '${property}'. If it is for external reference only, please consider using \`export const ${property}\``
}),
module_script_reactive_declaration: {
code: 'module-script-reactive-declaration',
message: '$: has no effect in a module script'
},
non_top_level_reactive_declaration: {
code: 'non-top-level-reactive-declaration',
message: '$: has no effect outside of the top-level'
},
module_script_variable_reactive_declaration: (name: string) => ({
code: 'module-script-reactive-declaration',
message: `"${name}" is declared in a module script and will not be reactive`
}),
missing_declaration: (name: string, has_script: boolean) => ({
code: 'missing-declaration',
message: `'${name}' is not defined` + (has_script ? '' : `. Consider adding a <script> block with 'export let ${name}' to declare a prop`)
}),
missing_custom_element_compile_options: {
code: 'missing-custom-element-compile-options',
message: "The 'tag' option is used when generating a custom element. Did you forget the 'customElement: true' compile option?"
},
css_unused_selector: (selector: string) => ({
code: 'css-unused-selector',
message: `Unused CSS selector "${selector}"`
}),
empty_block: {
code: 'empty-block',
message: 'Empty block'
},
reactive_component: (name: string) => ({
code: 'reactive-component',
message: `<${name}/> will not be reactive if ${name} changes. Use <svelte:component this={${name}}/> if you want this reactivity.`
}),
component_name_lowercase: (name: string) => ({
code: 'component-name-lowercase',
message: `<${name}> will be treated as an HTML element unless it begins with a capital letter`
}),
avoid_is: {
code: 'avoid-is',
message: 'The \'is\' attribute is not supported cross-browser and should be avoided'
},
invalid_html_attribute: (name: string, suggestion: string) => ({
code: 'invalid-html-attribute',
message: `'${name}' is not a valid HTML attribute. Did you mean '${suggestion}'?`
}),
a11y_aria_attributes: (name: string) => ({
code: 'a11y-aria-attributes',
message: `A11y: <${name}> should not have aria-* attributes`
}),
a11y_unknown_aria_attribute: (attribute: string, suggestion?: string) => ({
code: 'a11y-unknown-aria-attribute',
message: `A11y: Unknown aria attribute 'aria-${attribute}'` + (suggestion ? ` (did you mean '${suggestion}'?)` : '')
}),
a11y_hidden: (name: string) => ({
code: 'a11y-hidden',
message: `A11y: <${name}> element should not be hidden`
}),
a11y_misplaced_role: (name: string) => ({
code: 'a11y-misplaced-role',
message: `A11y: <${name}> should not have role attribute`
}),
a11y_unknown_role: (role: string | boolean, suggestion?: string) => ({
code: 'a11y-unknown-role',
message: `A11y: Unknown role '${role}'` + (suggestion ? ` (did you mean '${suggestion}'?)` : '')
}),
a11y_accesskey: {
code: 'a11y-accesskey',
message: 'A11y: Avoid using accesskey'
},
a11y_autofocus: {
code: 'a11y-autofocus',
message: 'A11y: Avoid using autofocus'
},
a11y_misplaced_scope: {
code: 'a11y-misplaced-scope',
message: 'A11y: The scope attribute should only be used with <th> elements'
},
a11y_positive_tabindex: {
code: 'a11y-positive-tabindex',
message: 'A11y: avoid tabindex values above zero'
},
a11y_invalid_attribute: (href_attribute: string, href_value: string) => ({
code: 'a11y-invalid-attribute',
message: `A11y: '${href_value}' is not a valid ${href_attribute} attribute`
}),
a11y_missing_attribute: (name: string, article: string, sequence: string) => ({
code: 'a11y-missing-attribute',
message: `A11y: <${name}> element should have ${article} ${sequence} attribute`
}),
a11y_img_redundant_alt: {
code: 'a11y-img-redundant-alt',
message: 'A11y: Screenreaders already announce <img> elements as an image.'
},
a11y_label_has_associated_control: {
code: 'a11y-label-has-associated-control',
message: 'A11y: A form label must be associated with a control.'
},
a11y_media_has_caption: {
code: 'a11y-media-has-caption',
message: 'A11y: <video> elements must have a <track kind="captions">'
},
a11y_distracting_elements: (name: string) => ({
code: 'a11y-distracting-elements',
message: `A11y: Avoid <${name}> elements`
}),
a11y_structure_immediate: {
code: 'a11y-structure',
message: 'A11y: <figcaption> must be an immediate child of <figure>'
},
a11y_structure_first_or_last: {
code: 'a11y-structure',
message: 'A11y: <figcaption> must be first or last child of <figure>'
},
a11y_mouse_events_have_key_events: (event: string, accompanied_by: string) => ({
code: 'a11y-mouse-events-have-key-events',
message: `A11y: on:${event} must be accompanied by on:${accompanied_by}`
}),
a11y_missing_content: (name: string) => ({
code: 'a11y-missing-content',
message: `A11y: <${name}> element should have child content`
}),
redundant_event_modifier_for_touch: {
code: 'redundant-event-modifier',
message: 'Touch event handlers that don\'t use the \'event\' object are passive by default'
},
redundant_event_modifier_passive: {
code: 'redundant-event-modifier',
message: 'The passive modifier only works with wheel and touch events'
}
};

@ -8,6 +8,7 @@ import { INode } from '../nodes/interfaces';
import EachBlock from '../nodes/EachBlock';
import IfBlock from '../nodes/IfBlock';
import AwaitBlock from '../nodes/AwaitBlock';
import compiler_errors from '../compiler_errors';
enum BlockAppliesToNode {
NotPossible,
@ -137,10 +138,7 @@ export default class Selector {
for (let i = start; i < end; i += 1) {
if (this.blocks[i].global) {
component.error(this.blocks[i].selectors[0], {
code: 'css-invalid-global',
message: ':global(...) can be at the start or end of a selector sequence, but not in the middle'
});
component.error(this.blocks[i].selectors[0], compiler_errors.css_invalid_global);
}
}
@ -148,10 +146,7 @@ export default class Selector {
for (const selector of block.selectors) {
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
if (/[^\\],(?!([^([]+[^\\]|[^([\\])[)\]])/.test(selector.children[0].value)) {
component.error(selector, {
code: 'css-invalid-global-selector',
message: ':global(...) must contain a single selector'
});
component.error(selector, compiler_errors.css_invalid_global_selector);
}
}
}

@ -6,6 +6,7 @@ import { Ast, CssHashGetter } from '../../interfaces';
import Component from '../Component';
import { CssNode } from './interfaces';
import hash from '../utils/hash';
import compiler_warnings from '../compiler_warnings';
function remove_css_prefix(name: string): string {
return name.replace(/^-((webkit)|(moz)|(o)|(ms))-/, '');
@ -448,10 +449,7 @@ export default class Stylesheet {
warn_on_unused_selectors(component: Component) {
this.children.forEach(child => {
child.warn_on_unused_selector((selector: Selector) => {
component.warn(selector.node, {
code: 'css-unused-selector',
message: `Unused CSS selector "${this.source.slice(selector.node.start, selector.node.end)}"`
});
component.warn(selector.node, compiler_warnings.css_unused_selector(this.source.slice(selector.node.start, selector.node.end)));
});
});
}

@ -5,6 +5,7 @@ import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import Element from './Element';
import EachBlock from './EachBlock';
import compiler_errors from '../compiler_errors';
export default class Animation extends Node {
type: 'Animation';
@ -20,19 +21,13 @@ export default class Animation extends Node {
component.add_reference(info.name.split('.')[0]);
if (parent.animation) {
component.error(this, {
code: 'duplicate-animation',
message: "An element can only have one 'animate' directive"
});
component.error(this, compiler_errors.duplicate_animation);
}
const block = parent.parent;
if (!block || block.type !== 'EachBlock' || !block.key) {
// TODO can we relax the 'immediate child' rule?
component.error(this, {
code: 'invalid-animation',
message: 'An element that uses the animate directive must be the immediate child of a keyed each block'
});
component.error(this, compiler_errors.invalid_animation_immediate);
}
(block as EachBlock).has_animation = true;

@ -10,6 +10,7 @@ import Element from './Element';
import InlineComponent from './InlineComponent';
import Window from './Window';
import { clone } from '../../utils/clone';
import compiler_errors from '../compiler_errors';
// TODO this should live in a specific binding
const read_only_media_attributes = new Set([
@ -35,10 +36,7 @@ export default class Binding extends Node {
super(component, parent, scope, info);
if (info.expression.type !== 'Identifier' && info.expression.type !== 'MemberExpression') {
component.error(info, {
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]`)'
});
component.error(info, compiler_errors.invalid_directive_value);
}
this.name = info.name;
@ -51,16 +49,10 @@ export default class Binding extends Node {
// make sure we track this as a mutable ref
if (scope.is_let(name)) {
component.error(this, {
code: 'invalid-binding',
message: 'Cannot bind to a variable declared with the let: directive'
});
component.error(this, compiler_errors.invalid_binding_let);
} else if (scope.names.has(name)) {
if (scope.is_await(name)) {
component.error(this, {
code: 'invalid-binding',
message: 'Cannot bind to a variable declared with {#await ... then} or {:catch} blocks'
});
component.error(this, compiler_errors.invalid_binding_await);
}
scope.dependencies_for_name.get(name).forEach(name => {
@ -73,19 +65,13 @@ export default class Binding extends Node {
const variable = component.var_lookup.get(name);
if (!variable || variable.global) {
component.error(this.expression.node as any, {
code: 'binding-undeclared',
message: `${name} is not declared`
});
component.error(this.expression.node as any, compiler_errors.binding_undeclared(name));
}
variable[this.expression.node.type === 'MemberExpression' ? 'mutated' : 'reassigned'] = true;
if (info.expression.type === 'Identifier' && !variable.writable) {
component.error(this.expression.node as any, {
code: 'invalid-binding',
message: 'Cannot bind to a variable which is not writable'
});
component.error(this.expression.node as any, compiler_errors.invalid_binding_writibale);
}
}

@ -8,6 +8,7 @@ import { Context, unpack_destructuring } from './shared/Context';
import { Node } from 'estree';
import Component from '../Component';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
export default class EachBlock extends AbstractBlock {
type: 'EachBlock';
@ -61,10 +62,7 @@ export default class EachBlock extends AbstractBlock {
if (this.has_animation) {
if (this.children.length !== 1) {
const child = this.children.find(child => !!(child as Element).animation);
component.error((child as Element).animation, {
code: 'invalid-animation',
message: 'An element that uses the animate directive must be the sole child of a keyed each block'
});
component.error((child as Element).animation, compiler_errors.invalid_animation_sole);
}
}

@ -17,6 +17,8 @@ import Let from './Let';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import Component from '../Component';
import compiler_warnings from '../compiler_warnings';
import compiler_errors from '../compiler_errors';
const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|svg|switch|symbol|text|textPath|tref|tspan|unknown|use|view|vkern)$/;
@ -139,10 +141,7 @@ export default class Element extends Node {
if (info.children.length > 0) {
const value_attribute = info.attributes.find(node => node.name === 'value');
if (value_attribute) {
component.error(value_attribute, {
code: 'textarea-duplicate-value',
message: 'A <textarea> can have either a value attribute or (equivalently) child content, but not both'
});
component.error(value_attribute, compiler_errors.textarea_duplicate_value);
}
// this is an egregious hack, but it's the easiest way to get <textarea>
@ -246,10 +245,7 @@ export default class Element extends Node {
validate() {
if (this.component.var_lookup.has(this.name) && this.component.var_lookup.get(this.name).imported) {
this.component.warn(this, {
code: 'component-name-lowercase',
message: `<${this.name}> will be treated as an HTML element unless it begins with a capital letter`
});
this.component.warn(this, compiler_warnings.component_name_lowercase(this.name));
}
this.validate_attributes();
@ -276,34 +272,22 @@ export default class Element extends Node {
// Errors
if (/(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/.test(name)) {
component.error(attribute, {
code: 'illegal-attribute',
message: `'${name}' is not a valid attribute name`
});
component.error(attribute, compiler_errors.illegal_attribute(name));
}
if (name === 'slot') {
if (!attribute.is_static) {
component.error(attribute, {
code: 'invalid-slot-attribute',
message: 'slot attribute cannot have a dynamic value'
});
component.error(attribute, compiler_errors.invalid_slot_attribute);
}
if (component.slot_outlets.has(name)) {
component.error(attribute, {
code: 'duplicate-slot-attribute',
message: `Duplicate '${name}' slot`
});
component.error(attribute, compiler_errors.duplicate_slot_attribute(name));
component.slot_outlets.add(name);
}
if (!(parent.type === 'SlotTemplate' || within_custom_element(parent))) {
component.error(attribute, {
code: 'invalid-slotted-content',
message: 'Element with a slot=\'...\' attribute must be a child of a component or a descendant of a custom element'
});
component.error(attribute, compiler_errors.invalid_slotted_content);
}
}
@ -311,17 +295,11 @@ export default class Element extends Node {
if (this.namespace !== namespaces.foreign) {
if (name === 'is') {
component.warn(attribute, {
code: 'avoid-is',
message: 'The \'is\' attribute is not supported cross-browser and should be avoided'
});
component.warn(attribute, compiler_warnings.avoid_is);
}
if (react_attributes.has(attribute.name)) {
component.warn(attribute, {
code: 'invalid-html-attribute',
message: `'${attribute.name}' is not a valid HTML attribute. Did you mean '${react_attributes.get(attribute.name)}'?`
});
component.warn(attribute, compiler_warnings.invalid_html_attribute(attribute.name, react_attributes.get(attribute.name)));
}
}
});
@ -339,29 +317,17 @@ export default class Element extends Node {
if (name.startsWith('aria-')) {
if (invisible_elements.has(this.name)) {
// aria-unsupported-elements
component.warn(attribute, {
code: 'a11y-aria-attributes',
message: `A11y: <${this.name}> should not have aria-* attributes`
});
component.warn(attribute, compiler_warnings.a11y_aria_attributes(this.name));
}
const type = name.slice(5);
if (!aria_attribute_set.has(type)) {
const match = fuzzymatch(type, aria_attributes);
let message = `A11y: Unknown aria attribute 'aria-${type}'`;
if (match) message += ` (did you mean '${match}'?)`;
component.warn(attribute, {
code: 'a11y-unknown-aria-attribute',
message
});
component.warn(attribute, compiler_warnings.a11y_unknown_aria_attribute(type, match));
}
if (name === 'aria-hidden' && /^h[1-6]$/.test(this.name)) {
component.warn(attribute, {
code: 'a11y-hidden',
message: `A11y: <${this.name}> element should not be hidden`
});
component.warn(attribute, compiler_warnings.a11y_hidden(this.name));
}
}
@ -369,10 +335,7 @@ export default class Element extends Node {
if (name === 'role') {
if (invisible_elements.has(this.name)) {
// aria-unsupported-elements
component.warn(attribute, {
code: 'a11y-misplaced-role',
message: `A11y: <${this.name}> should not have role attribute`
});
component.warn(attribute, compiler_warnings.a11y_misplaced_role(this.name));
}
const value = attribute.get_static_value();
@ -380,38 +343,23 @@ export default class Element extends Node {
if (value && !aria_role_set.has(value)) {
// @ts-ignore
const match = fuzzymatch(value, aria_roles);
let message = `A11y: Unknown role '${value}'`;
if (match) message += ` (did you mean '${match}'?)`;
component.warn(attribute, {
code: 'a11y-unknown-role',
message
});
component.warn(attribute, compiler_warnings.a11y_unknown_role(value, match));
}
}
// no-access-key
if (name === 'accesskey') {
component.warn(attribute, {
code: 'a11y-accesskey',
message: 'A11y: Avoid using accesskey'
});
component.warn(attribute, compiler_warnings.a11y_accesskey);
}
// no-autofocus
if (name === 'autofocus') {
component.warn(attribute, {
code: 'a11y-autofocus',
message: 'A11y: Avoid using autofocus'
});
component.warn(attribute, compiler_warnings.a11y_autofocus);
}
// scope
if (name === 'scope' && this.name !== 'th') {
component.warn(attribute, {
code: 'a11y-misplaced-scope',
message: 'A11y: The scope attribute should only be used with <th> elements'
});
component.warn(attribute, compiler_warnings.a11y_misplaced_scope);
}
// tabindex-no-positive
@ -419,10 +367,7 @@ export default class Element extends Node {
const value = attribute.get_static_value();
// @ts-ignore todo is tabindex=true correct case?
if (!isNaN(value) && +value > 0) {
component.warn(attribute, {
code: 'a11y-positive-tabindex',
message: 'A11y: avoid tabindex values above zero'
});
component.warn(attribute, compiler_warnings.a11y_positive_tabindex);
}
}
});
@ -452,20 +397,14 @@ export default class Element extends Node {
const href_value = href_attribute.get_static_value();
if (href_value === '' || href_value === '#' || /^\W*javascript:/i.test(href_value)) {
component.warn(href_attribute, {
code: 'a11y-invalid-attribute',
message: `A11y: '${href_value}' is not a valid ${href_attribute.name} attribute`
});
component.warn(href_attribute, compiler_warnings.a11y_invalid_attribute(href_attribute.name, href_value));
}
} else {
const id_attribute_valid = id_attribute && id_attribute.get_static_value() !== '';
const name_attribute_valid = name_attribute && name_attribute.get_static_value() !== '';
if (!id_attribute_valid && !name_attribute_valid) {
component.warn(this, {
code: 'a11y-missing-attribute',
message: 'A11y: <a> element should have an href attribute'
});
component.warn(this, compiler_warnings.a11y_missing_attribute('a', 'an', 'href'));
}
}
} else {
@ -501,10 +440,7 @@ export default class Element extends Node {
const alt_value = alt_attribute.get_static_value();
if (/\b(image|picture|photo)\b/i.test(alt_value)) {
component.warn(this, {
code: 'a11y-img-redundant-alt',
message: 'A11y: Screenreaders already announce <img> elements as an image.'
});
component.warn(this, compiler_warnings.a11y_img_redundant_alt);
}
}
}
@ -512,10 +448,7 @@ export default class Element extends Node {
if (this.name === 'label') {
const has_input_child = this.children.some(i => (i instanceof Element && a11y_labelable.has(i.name) ));
if (!attribute_map.has('for') && !has_input_child) {
component.warn(this, {
code: 'a11y-label-has-associated-control',
message: 'A11y: A form label must be associated with a control.'
});
component.warn(this, compiler_warnings.a11y_label_has_associated_control);
}
}
@ -531,19 +464,13 @@ export default class Element extends Node {
}
if (!has_caption) {
component.warn(this, {
code: 'a11y-media-has-caption',
message: 'A11y: <video> elements must have a <track kind="captions">'
});
component.warn(this, compiler_warnings.a11y_media_has_caption);
}
}
if (a11y_distracting_elements.has(this.name)) {
// no-distracting-elements
component.warn(this, {
code: 'a11y-distracting-elements',
message: `A11y: Avoid <${this.name}> elements`
});
component.warn(this, compiler_warnings.a11y_distracting_elements(this.name));
}
if (this.name === 'figcaption') {
@ -562,10 +489,7 @@ export default class Element extends Node {
}
if (!is_figure_parent) {
component.warn(this, {
code: 'a11y-structure',
message: 'A11y: <figcaption> must be an immediate child of <figure>'
});
component.warn(this, compiler_warnings.a11y_structure_immediate);
}
}
@ -579,35 +503,23 @@ export default class Element extends Node {
const index = children.findIndex(child => (child as Element).name === 'figcaption');
if (index !== -1 && (index !== 0 && index !== children.length - 1)) {
component.warn(children[index], {
code: 'a11y-structure',
message: 'A11y: <figcaption> must be first or last child of <figure>'
});
component.warn(children[index], compiler_warnings.a11y_structure_first_or_last);
}
}
if (handlers_map.has('mouseover') && !handlers_map.has('focus')) {
component.warn(this, {
code: 'a11y-mouse-events-have-key-events',
message: 'A11y: on:mouseover must be accompanied by on:focus'
});
component.warn(this, compiler_warnings.a11y_mouse_events_have_key_events('mouseover', 'focus'));
}
if (handlers_map.has('mouseout') && !handlers_map.has('blur')) {
component.warn(this, {
code: 'a11y-mouse-events-have-key-events',
message: 'A11y: on:mouseout must be accompanied by on:blur'
});
component.warn(this, compiler_warnings.a11y_mouse_events_have_key_events('mouseout', 'blur'));
}
}
validate_bindings_foreign() {
this.bindings.forEach(binding => {
if (binding.name !== 'this') {
this.component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding. Foreign elements only support bind:this`
});
this.component.error(binding, compiler_errors.invalid_binding_foreign(binding.name));
}
});
}
@ -623,19 +535,13 @@ export default class Element extends Node {
if (!attribute) return null;
if (!attribute.is_static) {
component.error(attribute, {
code: 'invalid-type',
message: '\'type\' attribute cannot be dynamic if input uses two-way binding'
});
component.error(attribute, compiler_errors.invalid_type);
}
const value = attribute.get_static_value();
if (value === true) {
component.error(attribute, {
code: 'missing-type',
message: '\'type\' attribute must be specified'
});
component.error(attribute, compiler_errors.missing_type);
}
return value;
@ -650,10 +556,7 @@ export default class Element extends Node {
this.name !== 'textarea' &&
this.name !== 'select'
) {
component.error(binding, {
code: 'invalid-binding',
message: `'value' is not a valid binding on <${this.name}> elements`
});
component.error(binding, compiler_errors.invalid_binding_elements(this.name, 'value'));
}
if (this.name === 'select') {
@ -662,68 +565,45 @@ export default class Element extends Node {
);
if (attribute && !attribute.is_static) {
component.error(attribute, {
code: 'dynamic-multiple-attribute',
message: '\'multiple\' attribute cannot be dynamic if select uses two-way binding'
});
component.error(attribute, compiler_errors.dynamic_multiple_attribute);
}
} else {
check_type_attribute();
}
} else if (name === 'checked' || name === 'indeterminate') {
if (this.name !== 'input') {
component.error(binding, {
code: 'invalid-binding',
message: `'${name}' is not a valid binding on <${this.name}> elements`
});
component.error(binding, compiler_errors.invalid_binding_elements(this.name, name));
}
const type = check_type_attribute();
if (type !== 'checkbox') {
let message = `'${name}' binding can only be used with <input type="checkbox">`;
if (type === 'radio') message += ' — for <input type="radio">, use \'group\' binding';
component.error(binding, { code: 'invalid-binding', message });
component.error(binding, compiler_errors.invalid_binding_no_checkbox(name, type === 'radio'));
}
} else if (name === 'group') {
if (this.name !== 'input') {
component.error(binding, {
code: 'invalid-binding',
message: `'group' is not a valid binding on <${this.name}> elements`
});
component.error(binding, compiler_errors.invalid_binding_elements(this.name, 'group'));
}
const type = check_type_attribute();
if (type !== 'checkbox' && type !== 'radio') {
component.error(binding, {
code: 'invalid-binding',
message: '\'group\' binding can only be used with <input type="checkbox"> or <input type="radio">'
});
component.error(binding, compiler_errors.invalid_binding_element_with('<input type="checkbox"> or <input type="radio">', 'group'));
}
} else if (name === 'files') {
if (this.name !== 'input') {
component.error(binding, {
code: 'invalid-binding',
message: `'files' is not a valid binding on <${this.name}> elements`
});
component.error(binding, compiler_errors.invalid_binding_elements(this.name, 'files'));
}
const type = check_type_attribute();
if (type !== 'file') {
component.error(binding, {
code: 'invalid-binding',
message: '\'files\' binding can only be used with <input type="file">'
});
component.error(binding, compiler_errors.invalid_binding_element_with('<input type="file">', 'files'));
}
} else if (name === 'open') {
if (this.name !== 'details') {
component.error(binding, {
code: 'invalid-binding',
message: `'${name}' binding can only be used with <details>`
});
component.error(binding, compiler_errors.invalid_binding_element_with('<details>', name));
}
} else if (
name === 'currentTime' ||
@ -739,37 +619,22 @@ export default class Element extends Node {
name === 'ended'
) {
if (this.name !== 'audio' && this.name !== 'video') {
component.error(binding, {
code: 'invalid-binding',
message: `'${name}' binding can only be used with <audio> or <video>`
});
component.error(binding, compiler_errors.invalid_binding_element_with('audio> or <video>', name));
}
} else if (
name === 'videoHeight' ||
name === 'videoWidth'
) {
if (this.name !== 'video') {
component.error(binding, {
code: 'invalid-binding',
message: `'${name}' binding can only be used with <video>`
});
component.error(binding, compiler_errors.invalid_binding_element_with('<video>', name));
}
} else if (dimensions.test(name)) {
if (this.name === 'svg' && (name === 'offsetWidth' || name === 'offsetHeight')) {
component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding on <svg>. Use '${name.replace('offset', 'client')}' instead`
});
component.error(binding, compiler_errors.invalid_binding_on(binding.name, `<svg>. Use '${name.replace('offset', 'client')}' instead`));
} else if (svg.test(this.name)) {
component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding on SVG elements`
});
component.error(binding, compiler_errors.invalid_binding_on(binding.name, 'SVG elements'));
} else if (is_void(this.name)) {
component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding on void elements like <${this.name}>. Use a wrapper element instead`
});
component.error(binding, compiler_errors.invalid_binding_on(binding.name, `void elements like <${this.name}>. Use a wrapper element instead`));
}
} else if (
name === 'textContent' ||
@ -780,21 +645,12 @@ export default class Element extends Node {
);
if (!contenteditable) {
component.error(binding, {
code: 'missing-contenteditable-attribute',
message: '\'contenteditable\' attribute is required for textContent and innerHTML two-way bindings'
});
component.error(binding, compiler_errors.missing_contenteditable_attribute);
} else if (contenteditable && !contenteditable.is_static) {
component.error(contenteditable, {
code: 'dynamic-contenteditable-attribute',
message: '\'contenteditable\' attribute cannot be dynamic if element uses two-way binding'
});
component.error(contenteditable, compiler_errors.dynamic_contenteditable_attribute);
}
} else if (name !== 'this') {
component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding`
});
component.error(binding, compiler_errors.invalid_binding(binding.name));
}
});
}
@ -807,10 +663,7 @@ export default class Element extends Node {
) return;
if (this.children.length === 0) {
this.component.warn(this, {
code: 'a11y-missing-content',
message: `A11y: <${this.name}> element should have child content`
});
this.component.warn(this, compiler_warnings.a11y_missing_content(this.name));
}
}
@ -819,50 +672,32 @@ export default class Element extends Node {
this.handlers.forEach(handler => {
if (handler.modifiers.has('passive') && handler.modifiers.has('preventDefault')) {
component.error(handler, {
code: 'invalid-event-modifier',
message: 'The \'passive\' and \'preventDefault\' modifiers cannot be used together'
});
component.error(handler, compiler_errors.invalid_event_modifier_combination('passive', 'preventDefault'));
}
if (handler.modifiers.has('passive') && handler.modifiers.has('nonpassive')) {
component.error(handler, {
code: 'invalid-event-modifier',
message: 'The \'passive\' and \'nonpassive\' modifiers cannot be used together'
});
component.error(handler, compiler_errors.invalid_event_modifier_combination('passive', 'nonpassive'));
}
handler.modifiers.forEach(modifier => {
if (!valid_modifiers.has(modifier)) {
component.error(handler, {
code: 'invalid-event-modifier',
message: `Valid event modifiers are ${list(Array.from(valid_modifiers))}`
});
component.error(handler, compiler_errors.invalid_event_modifier(list(Array.from(valid_modifiers))));
}
if (modifier === 'passive') {
if (passive_events.has(handler.name)) {
if (handler.can_make_passive) {
component.warn(handler, {
code: 'redundant-event-modifier',
message: 'Touch event handlers that don\'t use the \'event\' object are passive by default'
});
component.warn(handler, compiler_warnings.redundant_event_modifier_for_touch);
}
} else {
component.warn(handler, {
code: 'redundant-event-modifier',
message: 'The passive modifier only works with wheel and touch events'
});
component.warn(handler, compiler_warnings.redundant_event_modifier_passive);
}
}
if (component.compile_options.legacy && (modifier === 'once' || modifier === 'passive')) {
// TODO this could be supported, but it would need a few changes to
// how event listeners work
component.error(handler, {
code: 'invalid-event-modifier',
message: `The '${modifier}' modifier cannot be used in legacy mode`
});
component.error(handler, compiler_errors.invalid_event_modifier_legacy(modifier));
}
});
@ -925,10 +760,7 @@ function should_have_attribute(
attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}` :
attributes[0];
node.component.warn(node, {
code: 'a11y-missing-attribute',
message: `A11y: <${name}> element should have ${article} ${sequence} attribute`
});
node.component.warn(node, compiler_warnings.a11y_missing_attribute(name, article, sequence));
}
function within_custom_element(parent: INode) {

@ -4,6 +4,7 @@ import hash from '../utils/hash';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
export default class Head extends Node {
type: 'Head';
@ -14,10 +15,7 @@ export default class Head extends Node {
super(component, parent, scope, info);
if (info.attributes.length) {
component.error(info.attributes[0], {
code: 'invalid-attribute',
message: '<svelte:head> should not have any attributes or directives'
});
component.error(info.attributes[0], compiler_errors.invalid_attribute_head);
}
this.children = map_children(component, parent, scope, info.children.filter(child => {

@ -9,6 +9,7 @@ import Let from './Let';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
export default class InlineComponent extends Node {
type: 'InlineComponent';
@ -41,10 +42,7 @@ export default class InlineComponent extends Node {
/* eslint-disable no-fallthrough */
switch (node.type) {
case 'Action':
component.error(node, {
code: 'invalid-action',
message: 'Actions can only be applied to DOM elements, not components'
});
component.error(node, compiler_errors.invalid_action);
case 'Attribute':
if (node.name.startsWith('--')) {
@ -61,10 +59,7 @@ export default class InlineComponent extends Node {
break;
case 'Class':
component.error(node, {
code: 'invalid-class',
message: 'Classes can only be applied to DOM elements, not components'
});
component.error(node, compiler_errors.invalid_class);
case 'EventHandler':
this.handlers.push(new EventHandler(component, this, scope, node));
@ -75,10 +70,7 @@ export default class InlineComponent extends Node {
break;
case 'Transition':
component.error(node, {
code: 'invalid-transition',
message: 'Transitions can only be applied to DOM elements, not components'
});
component.error(node, compiler_errors.invalid_transition);
default:
throw new Error(`Not implemented: ${node.type}`);
@ -103,10 +95,7 @@ export default class InlineComponent extends Node {
this.handlers.forEach(handler => {
handler.modifiers.forEach(modifier => {
if (modifier !== 'once') {
component.error(handler, {
code: 'invalid-event-modifier',
message: "Event modifiers other than 'once' can only be used on DOM elements"
});
component.error(handler, compiler_errors.invalid_event_modifier_component);
}
});
});

@ -4,6 +4,7 @@ import { walk } from 'estree-walker';
import { BasePattern, Identifier } from 'estree';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
const applicable = new Set(['Identifier', 'ObjectExpression', 'ArrayExpression', 'Property']);
@ -26,10 +27,7 @@ export default class Let extends Node {
walk(info.expression, {
enter(node: Identifier|BasePattern) {
if (!applicable.has(node.type)) {
component.error(node as any, {
code: 'invalid-let',
message: 'let directive value must be an identifier or an object/array pattern'
});
component.error(node as any, compiler_errors.invalid_let);
}
if (node.type === 'Identifier') {

@ -4,6 +4,7 @@ import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
export default class Slot extends Element {
type: 'Element';
@ -17,26 +18,17 @@ export default class Slot extends Element {
info.attributes.forEach(attr => {
if (attr.type !== 'Attribute' && attr.type !== 'Spread') {
component.error(attr, {
code: 'invalid-slot-directive',
message: '<slot> cannot have directives'
});
component.error(attr, compiler_errors.invalid_slot_directive);
}
if (attr.name === 'name') {
if (attr.value.length !== 1 || attr.value[0].type !== 'Text') {
component.error(attr, {
code: 'dynamic-slot-name',
message: '<slot> name cannot be dynamic'
});
component.error(attr, compiler_errors.dynamic_slot_name);
}
this.slot_name = attr.value[0].data;
if (this.slot_name === 'default') {
component.error(attr, {
code: 'invalid-slot-name',
message: 'default is a reserved word — it cannot be used as a slot name'
});
component.error(attr, compiler_errors.invalid_slot_name);
}
}

@ -5,6 +5,7 @@ import Node from './shared/Node';
import Let from './Let';
import Attribute from './Attribute';
import { INode } from './interfaces';
import compiler_errors from '../compiler_errors';
export default class SlotTemplate extends Node {
type: 'SlotTemplate';
@ -45,17 +46,11 @@ export default class SlotTemplate extends Node {
if (node.name === 'slot') {
this.slot_attribute = new Attribute(component, this, scope, node);
if (!this.slot_attribute.is_static) {
component.error(node, {
code: 'invalid-slot-attribute',
message: 'slot attribute cannot have a dynamic value'
});
component.error(node, compiler_errors.invalid_slot_attribute);
}
const value = this.slot_attribute.get_static_value();
if (typeof value === 'boolean') {
component.error(node, {
code: 'invalid-slot-attribute',
message: 'slot attribute value is missing'
});
component.error(node, compiler_errors.invalid_slot_attribute_value_missing);
}
this.slot_template_name = value as string;
break;
@ -73,10 +68,7 @@ export default class SlotTemplate extends Node {
validate_slot_template_placement() {
if (this.parent.type !== 'InlineComponent') {
this.component.error(this, {
code: 'invalid-slotted-content',
message: '<svelte:fragment> must be a child of a component'
});
this.component.error(this, compiler_errors.invalid_slotted_content_fragment);
}
}
}

@ -3,6 +3,7 @@ import map_children, { Children } from './shared/map_children';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
export default class Title extends Node {
type: 'Title';
@ -14,18 +15,12 @@ export default class Title extends Node {
this.children = map_children(component, parent, scope, info.children);
if (info.attributes.length > 0) {
component.error(info.attributes[0], {
code: 'illegal-attribute',
message: '<title> cannot have attributes'
});
component.error(info.attributes[0], compiler_errors.illegal_attribute_title);
}
info.children.forEach(child => {
if (child.type !== 'Text' && child.type !== 'MustacheTag') {
component.error(child, {
code: 'illegal-structure',
message: '<title> can only contain text and {tags}'
});
component.error(child, compiler_errors.illegal_structure_title);
}
});

@ -4,6 +4,7 @@ import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import Element from './Element';
import compiler_errors from '../compiler_errors';
export default class Transition extends Node {
type: 'Transition';
@ -25,15 +26,7 @@ export default class Transition extends Node {
if ((info.intro && parent.intro) || (info.outro && parent.outro)) {
const parent_transition = (parent.intro || parent.outro);
const message = this.directive === parent_transition.directive
? `An element can only have one '${this.directive}' directive`
: `An element cannot have both ${describe(parent_transition)} directive and ${describe(this)} directive`;
component.error(info, {
code: 'duplicate-transition',
message
});
component.error(info, compiler_errors.duplicate_transition(this.directive, parent_transition.directive));
}
this.expression = info.expression
@ -41,9 +34,3 @@ export default class Transition extends Node {
: null;
}
}
function describe(transition: Transition) {
return transition.directive === 'transition'
? "a 'transition'"
: `an '${transition.directive}'`;
}

@ -8,6 +8,7 @@ import Action from './Action';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
const valid_bindings = [
'innerWidth',
@ -36,10 +37,7 @@ export default class Window extends Node {
const { parts } = flatten_reference(node.expression);
// TODO is this constraint necessary?
component.error(node.expression, {
code: 'invalid-binding',
message: `Bindings on <svelte:window> must be to top-level properties, e.g. '${parts[parts.length - 1]}' rather than '${parts.join('.')}'`
});
component.error(node.expression, compiler_errors.invalid_binding_window(parts));
}
if (!~valid_bindings.indexOf(node.name)) {
@ -49,18 +47,10 @@ export default class Window extends Node {
fuzzymatch(node.name, valid_bindings)
);
const message = `'${node.name}' is not a valid binding on <svelte:window>`;
if (match) {
component.error(node, {
code: 'invalid-binding',
message: `${message} (did you mean '${match}'?)`
});
component.error(node, compiler_errors.invalid_binding_on(node.name, '<svelte:window>', ` (did you mean '${match}'?)`));
} else {
component.error(node, {
code: 'invalid-binding',
message: `${message} — valid bindings are ${list(valid_bindings)}`
});
component.error(node, compiler_errors.invalid_binding_on(node.name, '<svelte:window>', ` — valid bindings are ${list(valid_bindings)}`));
}
}

@ -2,6 +2,7 @@ import Block from '../../render_dom/Block';
import Component from '../../Component';
import Node from './Node';
import { INode } from '../interfaces';
import compiler_warnings from '../../compiler_warnings';
export default class AbstractBlock extends Node {
block: Block;
@ -17,10 +18,7 @@ export default class AbstractBlock extends Node {
const child = this.children[0];
if (!child || (child.type === 'Text' && !/[^ \r\n\f\v\t]/.test(child.data))) {
this.component.warn(this, {
code: 'empty-block',
message: 'Empty block'
});
this.component.warn(this, compiler_warnings.empty_block);
}
}
}

@ -18,6 +18,7 @@ import is_contextual from './is_contextual';
import EachBlock from '../EachBlock';
import { clone } from '../../../utils/clone';
import { Node as PeriscopicNode } from 'periscopic';
import compiler_errors from '../../compiler_errors';
type Owner = INode;
@ -85,10 +86,7 @@ export default class Expression {
if (name[0] === '$') {
const store_name = name.slice(1);
if (template_scope.names.has(store_name) || scope.has(store_name)) {
component.error(node, {
code: 'contextual-store',
message: 'Stores must be declared at the top level of the component (this may change in a future version of Svelte)'
});
component.error(node, compiler_errors.contextual_store);
}
}

@ -19,6 +19,7 @@ import mark_each_block_bindings from '../shared/mark_each_block_bindings';
import { string_to_member_expression } from '../../../utils/string_to_member_expression';
import SlotTemplate from '../../../nodes/SlotTemplate';
import { is_head } from '../shared/is_head';
import compiler_warnings from '../../../compiler_warnings';
type SlotDefinition = { block: Block; scope: TemplateScope; get_context?: Node; get_changes?: Node };
@ -108,10 +109,7 @@ export default class InlineComponentWrapper extends Wrapper {
}
if (variable.reassigned || variable.export_name || variable.is_reactive_dependency) {
this.renderer.component.warn(this.node, {
code: 'reactive-component',
message: `<${name}/> will not be reactive if ${name} changes. Use <svelte:component this={${name}}/> if you want this reactivity.`
});
this.renderer.component.warn(this.node, compiler_warnings.reactive_component(name));
}
}

@ -21,6 +21,9 @@
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
// Hides exports flagged with @internal from the d.ts output
"stripInternal": true,
// TODO: error all the things
//"strict": true,
"noImplicitThis": true,

Loading…
Cancel
Save