feat: add errorMode option to compile

- allow compiler to pass error as warnings
- enforce stops after errors during compilation
- should review Element.ts:302
- add a test case for errorMode
- adding documentation
pull/6194/head
Maxime LUCE 5 years ago
parent dafbdc286e
commit d3e11bb731

@ -45,6 +45,7 @@ The following options can be passed to the compiler. None are required:
| `name` | string | `"Component"`
| `format` | `"esm"` or `"cjs"` | `"esm"`
| `generate` | `"dom"` or `"ssr"` | `"dom"`
| `errorMode` | `"throw"` or `"warn"` | `"throw"`
| `dev` | boolean | `false`
| `immutable` | boolean | `false`
| `hydratable` | boolean | `false`
@ -66,6 +67,7 @@ The following options can be passed to the compiler. None are required:
| `name` | `"Component"` | `string` that sets the name of the resulting JavaScript class (though the compiler will rename it if it would otherwise conflict with other variables in scope). It will normally be inferred from `filename`.
| `format` | `"esm"` | If `"esm"`, creates a JavaScript module (with `import` and `export`). If `"cjs"`, creates a CommonJS module (with `require` and `module.exports`), which is useful in some server-side rendering situations or for testing.
| `generate` | `"dom"` | If `"dom"`, Svelte emits a JavaScript class for mounting to the DOM. If `"ssr"`, Svelte emits an object with a `render` method suitable for server-side rendering. If `false`, no JavaScript or CSS is returned; just metadata.
| `errorMode` | `"throw"` | If `"throw"`, Svelte throws when a compilation error occured. If `"warn"`, Svelte will treat errors as warnings and add them to the warning report.
| `dev` | `false` | If `true`, causes extra code to be added to components that will perform runtime checks and provide debugging information during development.
| `immutable` | `false` | If `true`, tells the compiler that you promise not to mutate any objects. This allows it to be less conservative about checking whether values have changed.
| `hydratable` | `false` | If `true` when generating DOM code, enables the `hydrate: true` runtime option, which allows a component to upgrade existing DOM rather than creating new DOM from scratch. When generating SSR code, this adds markers to `<head>` elements so that hydration knows which to replace.

@ -412,14 +412,18 @@ export default class Component {
message: string;
}
) {
error(e.message, {
name: 'ValidationError',
code: e.code,
source: this.source,
start: pos.start,
end: pos.end,
filename: this.compile_options.filename
});
if (this.compile_options.errorMode === 'warn') {
this.warn(pos, e);
} else {
error(e.message, {
name: 'ValidationError',
code: e.code,
source: this.source,
start: pos.start,
end: pos.end,
filename: this.compile_options.filename
});
}
}
warn(
@ -460,7 +464,7 @@ export default class Component {
extract_exports(node) {
if (node.type === 'ExportDefaultDeclaration') {
this.error(node, {
return this.error(node, {
code: 'default-export',
message: 'A component cannot have a default export'
});
@ -468,7 +472,7 @@ export default class Component {
if (node.type === 'ExportNamedDeclaration') {
if (node.source) {
this.error(node, {
return this.error(node, {
code: 'not-implemented',
message: 'A component currently cannot have an export ... from'
});
@ -550,7 +554,7 @@ export default class Component {
scope.declarations.forEach((node, name) => {
if (name[0] === '$') {
this.error(node as any, {
return this.error(node as any, {
code: 'illegal-declaration',
message: 'The $ prefix is reserved, and cannot be used for variable and import names'
});
@ -568,7 +572,7 @@ export default class Component {
globals.forEach((node, name) => {
if (name[0] === '$') {
this.error(node as any, {
return this.error(node as any, {
code: 'illegal-subscription',
message: 'Cannot reference store value inside <script context="module">'
});
@ -629,7 +633,7 @@ export default class Component {
instance_scope.declarations.forEach((node, name) => {
if (name[0] === '$') {
this.error(node as any, {
return this.error(node as any, {
code: 'illegal-declaration',
message: 'The $ prefix is reserved, and cannot be used for variable and import names'
});
@ -666,7 +670,7 @@ export default class Component {
});
} else if (name[0] === '$') {
if (name === '$' || name[1] === '$') {
this.error(node as any, {
return this.error(node as any, {
code: 'illegal-global',
message: `${name} is an illegal variable name`
});
@ -869,7 +873,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, {
return 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)'
});
@ -937,7 +941,7 @@ export default class Component {
if (variable.export_name) {
// TODO is this still true post-#3539?
component.error(declarator as any, {
return component.error(declarator as any, {
code: 'destructured-prop',
message: 'Cannot declare props in destructured declaration'
});
@ -1298,7 +1302,7 @@ export default class Component {
if (cycle && cycle.length) {
const declarationList = lookup.get(cycle[0]);
const declaration = declarationList[0];
this.error(declaration.node, {
return this.error(declaration.node, {
code: 'cyclical-reactive-declaration',
message: `Cyclical dependency detected: ${cycle.join(' → ')}`
});
@ -1324,7 +1328,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, {
return this.error(node, {
code: 'illegal-global',
message: `${name} is an illegal variable name`
});
@ -1384,13 +1388,13 @@ function process_component_options(component: Component, nodes) {
if (!chunk) return true;
if (value.length > 1) {
component.error(attribute, { code, message });
return component.error(attribute, { code, message });
}
if (chunk.type === 'Text') return chunk.data;
if (chunk.expression.type !== 'Literal') {
component.error(attribute, { code, message });
return component.error(attribute, { code, message });
}
return chunk.expression.value;
@ -1408,11 +1412,11 @@ function process_component_options(component: Component, nodes) {
const tag = get_value(attribute, code, message);
if (typeof tag !== 'string' && tag !== null) {
component.error(attribute, { code, message });
return component.error(attribute, { code, message });
}
if (tag && !/^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/.test(tag)) {
component.error(attribute, {
return component.error(attribute, {
code: 'invalid-tag-property',
message: "tag name must be two or more words joined by the '-' character"
});
@ -1435,18 +1439,18 @@ function process_component_options(component: Component, nodes) {
const ns = get_value(attribute, code, message);
if (typeof ns !== 'string') {
component.error(attribute, { code, message });
return component.error(attribute, { code, message });
}
if (valid_namespaces.indexOf(ns) === -1) {
const match = fuzzymatch(ns, valid_namespaces);
if (match) {
component.error(attribute, {
return component.error(attribute, {
code: 'invalid-namespace-property',
message: `Invalid namespace '${ns}' (did you mean '${match}'?)`
});
} else {
component.error(attribute, {
return component.error(attribute, {
code: 'invalid-namespace-property',
message: `Invalid namespace '${ns}'`
});
@ -1465,7 +1469,7 @@ function process_component_options(component: Component, nodes) {
const value = get_value(attribute, code, message);
if (typeof value !== 'boolean') {
component.error(attribute, { code, message });
return component.error(attribute, { code, message });
}
component_options[name] = value;
@ -1473,13 +1477,13 @@ function process_component_options(component: Component, nodes) {
}
default:
component.error(attribute, {
return component.error(attribute, {
code: 'invalid-options-attribute',
message: '<svelte:options> unknown attribute'
});
}
} else {
component.error(attribute, {
return component.error(attribute, {
code: 'invalid-options-attribute',
message: "<svelte:options> can only have static 'tag', 'namespace', 'accessors', 'immutable' and 'preserveWhitespace' attributes"
});

@ -120,7 +120,7 @@ export default class Selector {
while (i-- > 1) {
const selector = block.selectors[i];
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
component.error(selector, {
return component.error(selector, {
code: 'css-invalid-global',
message: ':global(...) must be the first element in a compound selector'
});
@ -141,7 +141,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], {
return 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'
});

@ -14,6 +14,7 @@ const valid_options = [
'filename',
'sourcemap',
'generate',
'errorMode',
'outputFilename',
'cssOutputFilename',
'sveltePath',

@ -24,6 +24,7 @@ export default class Animation extends Node {
code: 'duplicate-animation',
message: "An element can only have one 'animate' directive"
});
return;
}
const block = parent.parent;
@ -33,6 +34,7 @@ export default class Animation extends Node {
code: 'invalid-animation',
message: 'An element that uses the animate directive must be the immediate child of a keyed each block'
});
return;
}
(block as EachBlock).has_animation = true;

@ -39,6 +39,7 @@ export default class Binding extends Node {
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]`)'
});
return;
}
this.name = info.name;
@ -55,12 +56,14 @@ export default class Binding extends Node {
code: 'invalid-binding',
message: 'Cannot bind to a variable declared with the let: directive'
});
return;
} 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'
});
return;
}
scope.dependencies_for_name.get(name).forEach(name => {
@ -77,6 +80,7 @@ export default class Binding extends Node {
code: 'binding-undeclared',
message: `${name} is not declared`
});
return;
}
variable[this.expression.node.type === 'MemberExpression' ? 'mutated' : 'reassigned'] = true;
@ -86,6 +90,7 @@ export default class Binding extends Node {
code: 'invalid-binding',
message: 'Cannot bind to a variable which is not writable'
});
return;
}
}

@ -65,6 +65,7 @@ export default class EachBlock extends AbstractBlock {
code: 'invalid-animation',
message: 'An element that uses the animate directive must be the sole child of a keyed each block'
});
return;
}
}

@ -145,6 +145,7 @@ export default class Element extends Node {
code: 'textarea-duplicate-value',
message: 'A <textarea> can have either a value attribute or (equivalently) child content, but not both'
});
return;
}
// this is an egregious hack, but it's the easiest way to get <textarea>
@ -278,7 +279,7 @@ export default class Element extends Node {
// Errors
if (/(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/.test(name)) {
component.error(attribute, {
return component.error(attribute, {
code: 'illegal-attribute',
message: `'${name}' is not a valid attribute name`
});
@ -286,23 +287,24 @@ export default class Element extends Node {
if (name === 'slot') {
if (!attribute.is_static) {
component.error(attribute, {
return component.error(attribute, {
code: 'invalid-slot-attribute',
message: 'slot attribute cannot have a dynamic value'
});
}
if (component.slot_outlets.has(name)) {
component.error(attribute, {
return component.error(attribute, {
code: 'duplicate-slot-attribute',
message: `Duplicate '${name}' slot`
});
component.slot_outlets.add(name);
// this code was unreachable. Still needed?
// component.slot_outlets.add(name);
}
if (!(parent.type === 'SlotTemplate' || within_custom_element(parent))) {
component.error(attribute, {
return 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'
});
@ -601,7 +603,7 @@ export default class Element extends Node {
validate_bindings_foreign() {
this.bindings.forEach(binding => {
if (binding.name !== 'this') {
this.component.error(binding, {
return this.component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding. Foreign elements only support bind:this`
});
@ -620,7 +622,7 @@ export default class Element extends Node {
if (!attribute) return null;
if (!attribute.is_static) {
component.error(attribute, {
return component.error(attribute, {
code: 'invalid-type',
message: '\'type\' attribute cannot be dynamic if input uses two-way binding'
});
@ -629,7 +631,7 @@ export default class Element extends Node {
const value = attribute.get_static_value();
if (value === true) {
component.error(attribute, {
return component.error(attribute, {
code: 'missing-type',
message: '\'type\' attribute must be specified'
});
@ -647,7 +649,7 @@ export default class Element extends Node {
this.name !== 'textarea' &&
this.name !== 'select'
) {
component.error(binding, {
return component.error(binding, {
code: 'invalid-binding',
message: `'value' is not a valid binding on <${this.name}> elements`
});
@ -659,7 +661,7 @@ export default class Element extends Node {
);
if (attribute && !attribute.is_static) {
component.error(attribute, {
return component.error(attribute, {
code: 'dynamic-multiple-attribute',
message: '\'multiple\' attribute cannot be dynamic if select uses two-way binding'
});
@ -669,7 +671,7 @@ export default class Element extends Node {
}
} else if (name === 'checked' || name === 'indeterminate') {
if (this.name !== 'input') {
component.error(binding, {
return component.error(binding, {
code: 'invalid-binding',
message: `'${name}' is not a valid binding on <${this.name}> elements`
});
@ -680,11 +682,11 @@ export default class Element extends Node {
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 });
return component.error(binding, { code: 'invalid-binding', message });
}
} else if (name === 'group') {
if (this.name !== 'input') {
component.error(binding, {
return component.error(binding, {
code: 'invalid-binding',
message: `'group' is not a valid binding on <${this.name}> elements`
});
@ -693,14 +695,14 @@ export default class Element extends Node {
const type = check_type_attribute();
if (type !== 'checkbox' && type !== 'radio') {
component.error(binding, {
return component.error(binding, {
code: 'invalid-binding',
message: '\'group\' binding can only be used with <input type="checkbox"> or <input type="radio">'
});
}
} else if (name === 'files') {
if (this.name !== 'input') {
component.error(binding, {
return component.error(binding, {
code: 'invalid-binding',
message: `'files' is not a valid binding on <${this.name}> elements`
});
@ -709,7 +711,7 @@ export default class Element extends Node {
const type = check_type_attribute();
if (type !== 'file') {
component.error(binding, {
return component.error(binding, {
code: 'invalid-binding',
message: '\'files\' binding can only be used with <input type="file">'
});
@ -717,7 +719,7 @@ export default class Element extends Node {
} else if (name === 'open') {
if (this.name !== 'details') {
component.error(binding, {
return component.error(binding, {
code: 'invalid-binding',
message: `'${name}' binding can only be used with <details>`
});
@ -736,7 +738,7 @@ export default class Element extends Node {
name === 'ended'
) {
if (this.name !== 'audio' && this.name !== 'video') {
component.error(binding, {
return component.error(binding, {
code: 'invalid-binding',
message: `'${name}' binding can only be used with <audio> or <video>`
});
@ -746,24 +748,24 @@ export default class Element extends Node {
name === 'videoWidth'
) {
if (this.name !== 'video') {
component.error(binding, {
return component.error(binding, {
code: 'invalid-binding',
message: `'${name}' binding can only be used with <video>`
});
}
} else if (dimensions.test(name)) {
if (this.name === 'svg' && (name === 'offsetWidth' || name === 'offsetHeight')) {
component.error(binding, {
return component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding on <svg>. Use '${name.replace('offset', 'client')}' instead`
});
} else if (svg.test(this.name)) {
component.error(binding, {
return component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding on SVG elements`
});
} else if (is_void(this.name)) {
component.error(binding, {
return 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`
});
@ -777,18 +779,18 @@ export default class Element extends Node {
);
if (!contenteditable) {
component.error(binding, {
return component.error(binding, {
code: 'missing-contenteditable-attribute',
message: '\'contenteditable\' attribute is required for textContent and innerHTML two-way bindings'
});
} else if (contenteditable && !contenteditable.is_static) {
component.error(contenteditable, {
return component.error(contenteditable, {
code: 'dynamic-contenteditable-attribute',
message: '\'contenteditable\' attribute cannot be dynamic if element uses two-way binding'
});
}
} else if (name !== 'this') {
component.error(binding, {
return component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding`
});
@ -816,14 +818,14 @@ export default class Element extends Node {
this.handlers.forEach(handler => {
if (handler.modifiers.has('passive') && handler.modifiers.has('preventDefault')) {
component.error(handler, {
return component.error(handler, {
code: 'invalid-event-modifier',
message: 'The \'passive\' and \'preventDefault\' modifiers cannot be used together'
});
}
if (handler.modifiers.has('passive') && handler.modifiers.has('nonpassive')) {
component.error(handler, {
return component.error(handler, {
code: 'invalid-event-modifier',
message: 'The \'passive\' and \'nonpassive\' modifiers cannot be used together'
});
@ -831,7 +833,7 @@ export default class Element extends Node {
handler.modifiers.forEach(modifier => {
if (!valid_modifiers.has(modifier)) {
component.error(handler, {
return component.error(handler, {
code: 'invalid-event-modifier',
message: `Valid event modifiers are ${list(Array.from(valid_modifiers))}`
});
@ -856,7 +858,7 @@ export default class Element extends Node {
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, {
return component.error(handler, {
code: 'invalid-event-modifier',
message: `The '${modifier}' modifier cannot be used in legacy mode`
});

@ -18,6 +18,7 @@ export default class Head extends Node {
code: 'invalid-attribute',
message: '<svelte:head> should not have any attributes or directives'
});
return;
}
this.children = map_children(component, parent, scope, info.children.filter(child => {

@ -40,7 +40,7 @@ export default class InlineComponent extends Node {
/* eslint-disable no-fallthrough */
switch (node.type) {
case 'Action':
component.error(node, {
return component.error(node, {
code: 'invalid-action',
message: 'Actions can only be applied to DOM elements, not components'
});
@ -56,7 +56,7 @@ export default class InlineComponent extends Node {
break;
case 'Class':
component.error(node, {
return component.error(node, {
code: 'invalid-class',
message: 'Classes can only be applied to DOM elements, not components'
});
@ -70,7 +70,7 @@ export default class InlineComponent extends Node {
break;
case 'Transition':
component.error(node, {
return component.error(node, {
code: 'invalid-transition',
message: 'Transitions can only be applied to DOM elements, not components'
});
@ -98,7 +98,7 @@ export default class InlineComponent extends Node {
this.handlers.forEach(handler => {
handler.modifiers.forEach(modifier => {
if (modifier !== 'once') {
component.error(handler, {
return component.error(handler, {
code: 'invalid-event-modifier',
message: "Event modifiers other than 'once' can only be used on DOM elements"
});

@ -26,7 +26,7 @@ export default class Let extends Node {
walk(info.expression, {
enter(node: Identifier|BasePattern) {
if (!applicable.has(node.type)) {
component.error(node as any, {
return component.error(node as any, {
code: 'invalid-let',
message: 'let directive value must be an identifier or an object/array pattern'
});

@ -17,7 +17,7 @@ export default class Slot extends Element {
info.attributes.forEach(attr => {
if (attr.type !== 'Attribute' && attr.type !== 'Spread') {
component.error(attr, {
return component.error(attr, {
code: 'invalid-slot-directive',
message: '<slot> cannot have directives'
});
@ -25,7 +25,7 @@ export default class Slot extends Element {
if (attr.name === 'name') {
if (attr.value.length !== 1 || attr.value[0].type !== 'Text') {
component.error(attr, {
return component.error(attr, {
code: 'dynamic-slot-name',
message: '<slot> name cannot be dynamic'
});
@ -33,7 +33,7 @@ export default class Slot extends Element {
this.slot_name = attr.value[0].data;
if (this.slot_name === 'default') {
component.error(attr, {
return component.error(attr, {
code: 'invalid-slot-name',
message: 'default is a reserved word — it cannot be used as a slot name'
});

@ -45,14 +45,14 @@ 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, {
return component.error(node, {
code: 'invalid-slot-attribute',
message: 'slot attribute cannot have a dynamic value'
});
}
const value = this.slot_attribute.get_static_value();
if (typeof value === 'boolean') {
component.error(node, {
return component.error(node, {
code: 'invalid-slot-attribute',
message: 'slot attribute value is missing'
});
@ -73,7 +73,7 @@ export default class SlotTemplate extends Node {
validate_slot_template_placement() {
if (this.parent.type !== 'InlineComponent') {
this.component.error(this, {
return this.component.error(this, {
code: 'invalid-slotted-content',
message: '<svelte:fragment> must be a child of a component'
});

@ -18,11 +18,12 @@ export default class Title extends Node {
code: 'illegal-attribute',
message: '<title> cannot have attributes'
});
return;
}
info.children.forEach(child => {
if (child.type !== 'Text' && child.type !== 'MustacheTag') {
component.error(child, {
return component.error(child, {
code: 'illegal-structure',
message: '<title> can only contain text and {tags}'
});

@ -34,6 +34,7 @@ export default class Transition extends Node {
code: 'duplicate-transition',
message
});
return;
}
this.expression = info.expression

@ -36,7 +36,7 @@ export default class Window extends Node {
const { parts } = flatten_reference(node.expression);
// TODO is this constraint necessary?
component.error(node.expression, {
return 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('.')}'`
});
@ -52,12 +52,12 @@ export default class Window extends Node {
const message = `'${node.name}' is not a valid binding on <svelte:window>`;
if (match) {
component.error(node, {
return component.error(node, {
code: 'invalid-binding',
message: `${message} (did you mean '${match}'?)`
});
} else {
component.error(node, {
return component.error(node, {
code: 'invalid-binding',
message: `${message} — valid bindings are ${list(valid_bindings)}`
});

@ -84,7 +84,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, {
return 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)'
});

@ -116,6 +116,7 @@ export interface CompileOptions {
name?: string;
filename?: string;
generate?: 'dom' | 'ssr' | false;
errorMode?: 'throw' | 'warn';
sourcemap?: object | string;
outputFilename?: string;

@ -0,0 +1,6 @@
<script>
const dummy = 'foo';
</script>
<input bind:value={dummy}>
<input bind:value={undeclared}>

@ -0,0 +1,47 @@
[
{
"code": "invalid-binding",
"message": "Cannot bind to a variable which is not writable",
"pos": 61,
"start": {
"line": 5,
"column": 19,
"character": 61
},
"end": {
"line": 5,
"column": 24,
"character": 66
}
},
{
"code": "missing-declaration",
"message": "'undeclared' is not defined",
"pos": 88,
"start": {
"character": 88,
"column": 19,
"line": 6
},
"end": {
"character": 98,
"column": 29,
"line": 6
}
},
{
"code": "binding-undeclared",
"message": "undeclared is not declared",
"pos": 88,
"end": {
"character": 98,
"column": 29,
"line": 6
},
"start": {
"character": 88,
"column": 19,
"line": 6
}
}
]
Loading…
Cancel
Save