From 0cf5511ae0c5189b4dad3ac087fadebe0224a1ad Mon Sep 17 00:00:00 2001 From: Maxime LUCE Date: Sat, 17 Jul 2021 13:37:29 +0200 Subject: [PATCH] [feat] Add errorMode option to compile to allow continuing on errors (and mark them as warnings) (#6194) This PR adds a new option errorMode to CompileOptions to allow continuing the compilation process when errors occured. When set to warn, this new option will indicate to Svelte that it should log errors as warnings and continue compilation. This allows (notably) preprocessors to compile the markup to detect vars in markup before preprocessing (in this case: script and style tags are stripped so it can produce a lot of errors). This PR is part of a work on the svelte-preprocess side to improve the preprocessing of TypeScript files: https://github.com/sveltejs/svelte-preprocess/issues/318 - allow compiler to pass error as warnings - enforce stops after errors during compilation (for type-checking, TS doesn't know the error method throws) - should review Element.ts:302 - added a test case for errorMode - added documentation --- site/content/docs/04-compile-time.md | 2 + src/compiler/compile/Component.ts | 58 +++++++++--------- src/compiler/compile/css/Selector.ts | 2 +- src/compiler/compile/index.ts | 1 + src/compiler/compile/nodes/Animation.ts | 2 + src/compiler/compile/nodes/Binding.ts | 5 ++ src/compiler/compile/nodes/EachBlock.ts | 1 + src/compiler/compile/nodes/Element.ts | 60 ++++++++++--------- src/compiler/compile/nodes/Head.ts | 1 + src/compiler/compile/nodes/InlineComponent.ts | 8 +-- src/compiler/compile/nodes/Let.ts | 2 +- src/compiler/compile/nodes/Slot.ts | 6 +- src/compiler/compile/nodes/SlotTemplate.ts | 6 +- src/compiler/compile/nodes/Title.ts | 3 +- src/compiler/compile/nodes/Transition.ts | 1 + src/compiler/compile/nodes/Window.ts | 6 +- .../compile/nodes/shared/Expression.ts | 2 +- src/compiler/interfaces.ts | 1 + .../samples/error-mode-warn/input.svelte | 6 ++ .../samples/error-mode-warn/options.json | 3 + .../samples/error-mode-warn/warnings.json | 47 +++++++++++++++ 21 files changed, 150 insertions(+), 73 deletions(-) create mode 100644 test/validator/samples/error-mode-warn/input.svelte create mode 100644 test/validator/samples/error-mode-warn/options.json create mode 100644 test/validator/samples/error-mode-warn/warnings.json diff --git a/site/content/docs/04-compile-time.md b/site/content/docs/04-compile-time.md index 1053d3ed54..43812426a5 100644 --- a/site/content/docs/04-compile-time.md +++ b/site/content/docs/04-compile-time.md @@ -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"` or `false` | `"dom"` +| `errorMode` | `"throw"` or `"warn"` | `"throw"` | `varsReport` | `"strict"` or `"full"` or `false` | `"strict"` | `dev` | boolean | `false` | `immutable` | boolean | `false` @@ -67,6 +68,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. | `varsReport` | `"strict"` | If `"strict"`, Svelte returns a variables report with only variables that are not globals nor internals. If `"full"`, Svelte returns a variables report with all detected variables. If `false`, no variables report is returned. | `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. diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index 4ca111f7ea..8067e29828 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -435,14 +435,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( @@ -491,12 +495,12 @@ export default class Component { private _extract_exports(node) { if (node.type === 'ExportDefaultDeclaration') { - this.error(node, compiler_errors.default_export); + return this.error(node, compiler_errors.default_export); } if (node.type === 'ExportNamedDeclaration') { if (node.source) { - this.error(node, compiler_errors.not_implemented); + return this.error(node, compiler_errors.not_implemented); } if (node.declaration) { if (node.declaration.type === 'VariableDeclaration') { @@ -566,7 +570,7 @@ export default class Component { scope.declarations.forEach((node, name) => { if (name[0] === '$') { - this.error(node as any, compiler_errors.illegal_declaration); + return this.error(node as any, compiler_errors.illegal_declaration); } const writable = node.type === 'VariableDeclaration' && (node.kind === 'var' || node.kind === 'let'); @@ -581,7 +585,7 @@ export default class Component { globals.forEach((node, name) => { if (name[0] === '$') { - this.error(node as any, compiler_errors.illegal_subscription); + return this.error(node as any, compiler_errors.illegal_subscription); } else { this.add_var({ name, @@ -639,7 +643,7 @@ export default class Component { instance_scope.declarations.forEach((node, name) => { if (name[0] === '$') { - this.error(node as any, compiler_errors.illegal_declaration); + return this.error(node as any, compiler_errors.illegal_declaration); } const writable = node.type === 'VariableDeclaration' && (node.kind === 'var' || node.kind === 'let'); @@ -673,7 +677,7 @@ export default class Component { }); } else if (name[0] === '$') { if (name === '$' || name[1] === '$') { - this.error(node as any, compiler_errors.illegal_global(name)); + return this.error(node as any, compiler_errors.illegal_global(name)); } this.add_var({ @@ -870,7 +874,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, compiler_errors.contextual_store); + return this.error(node as any, compiler_errors.contextual_store); } } } @@ -935,7 +939,7 @@ export default class Component { if (variable.export_name) { // TODO is this still true post-#3539? - component.error(declarator as any, compiler_errors.destructured_prop); + return component.error(declarator as any, compiler_errors.destructured_prop); } if (variable.subscribable) { @@ -1300,7 +1304,7 @@ export default class Component { if (cycle && cycle.length) { const declarationList = lookup.get(cycle[0]); const declaration = declarationList[0]; - this.error(declaration.node, compiler_errors.cyclical_reactive_declaration(cycle)); + return this.error(declaration.node, compiler_errors.cyclical_reactive_declaration(cycle)); } const add_declaration = declaration => { @@ -1323,7 +1327,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, compiler_errors.illegal_global(name)); + return this.error(node, compiler_errors.illegal_global(name)); } this.has_reactive_assignments = true; // TODO does this belong here? @@ -1372,13 +1376,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; @@ -1394,11 +1398,11 @@ function process_component_options(component: Component, nodes) { const tag = get_value(attribute, compiler_errors.invalid_tag_attribute); if (typeof tag !== 'string' && tag !== null) { - component.error(attribute, compiler_errors.invalid_tag_attribute); + return 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, compiler_errors.invalid_tag_property); + return component.error(attribute, compiler_errors.invalid_tag_property); } if (tag && !component.compile_options.customElement) { @@ -1413,12 +1417,12 @@ function process_component_options(component: Component, nodes) { const ns = get_value(attribute, compiler_errors.invalid_namespace_attribute); if (typeof ns !== 'string') { - component.error(attribute, compiler_errors.invalid_namespace_attribute); + return component.error(attribute, compiler_errors.invalid_namespace_attribute); } if (valid_namespaces.indexOf(ns) === -1) { const match = fuzzymatch(ns, valid_namespaces); - component.error(attribute, compiler_errors.invalid_namespace_property(ns, match)); + return component.error(attribute, compiler_errors.invalid_namespace_property(ns, match)); } component_options.namespace = ns; @@ -1431,7 +1435,7 @@ function process_component_options(component: Component, nodes) { const value = get_value(attribute, compiler_errors.invalid_attribute_value(name)); if (typeof value !== 'boolean') { - component.error(attribute, compiler_errors.invalid_attribute_value(name)); + return component.error(attribute, compiler_errors.invalid_attribute_value(name)); } component_options[name] = value; @@ -1439,10 +1443,10 @@ function process_component_options(component: Component, nodes) { } default: - component.error(attribute, compiler_errors.invalid_options_attribute_unknown); + return component.error(attribute, compiler_errors.invalid_options_attribute_unknown); } } else { - component.error(attribute, compiler_errors.invalid_options_attribute); + return component.error(attribute, compiler_errors.invalid_options_attribute); } }); } diff --git a/src/compiler/compile/css/Selector.ts b/src/compiler/compile/css/Selector.ts index 8dfb135c3a..071a18b637 100644 --- a/src/compiler/compile/css/Selector.ts +++ b/src/compiler/compile/css/Selector.ts @@ -138,7 +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], compiler_errors.css_invalid_global); + return component.error(this.blocks[i].selectors[0], compiler_errors.css_invalid_global); } } diff --git a/src/compiler/compile/index.ts b/src/compiler/compile/index.ts index e40ff37e92..96b24bceee 100644 --- a/src/compiler/compile/index.ts +++ b/src/compiler/compile/index.ts @@ -14,6 +14,7 @@ const valid_options = [ 'filename', 'sourcemap', 'generate', + 'errorMode', 'varsReport', 'outputFilename', 'cssOutputFilename', diff --git a/src/compiler/compile/nodes/Animation.ts b/src/compiler/compile/nodes/Animation.ts index 3d8be92fa8..ac9dddd275 100644 --- a/src/compiler/compile/nodes/Animation.ts +++ b/src/compiler/compile/nodes/Animation.ts @@ -22,12 +22,14 @@ export default class Animation extends Node { if (parent.animation) { component.error(this, compiler_errors.duplicate_animation); + return; } const block = parent.parent; if (!block || block.type !== 'EachBlock' || !block.key) { // TODO can we relax the 'immediate child' rule? component.error(this, compiler_errors.invalid_animation_immediate); + return; } (block as EachBlock).has_animation = true; diff --git a/src/compiler/compile/nodes/Binding.ts b/src/compiler/compile/nodes/Binding.ts index 3584707ed5..1efc1a3038 100644 --- a/src/compiler/compile/nodes/Binding.ts +++ b/src/compiler/compile/nodes/Binding.ts @@ -37,6 +37,7 @@ export default class Binding extends Node { if (info.expression.type !== 'Identifier' && info.expression.type !== 'MemberExpression') { component.error(info, compiler_errors.invalid_directive_value); + return; } this.name = info.name; @@ -50,9 +51,11 @@ export default class Binding extends Node { // make sure we track this as a mutable ref if (scope.is_let(name)) { component.error(this, compiler_errors.invalid_binding_let); + return; } else if (scope.names.has(name)) { if (scope.is_await(name)) { component.error(this, compiler_errors.invalid_binding_await); + return; } scope.dependencies_for_name.get(name).forEach(name => { @@ -66,12 +69,14 @@ export default class Binding extends Node { if (!variable || variable.global) { component.error(this.expression.node as any, compiler_errors.binding_undeclared(name)); + return; } variable[this.expression.node.type === 'MemberExpression' ? 'mutated' : 'reassigned'] = true; if (info.expression.type === 'Identifier' && !variable.writable) { component.error(this.expression.node as any, compiler_errors.invalid_binding_writibale); + return; } } diff --git a/src/compiler/compile/nodes/EachBlock.ts b/src/compiler/compile/nodes/EachBlock.ts index 4e6b809239..c03128c388 100644 --- a/src/compiler/compile/nodes/EachBlock.ts +++ b/src/compiler/compile/nodes/EachBlock.ts @@ -63,6 +63,7 @@ export default class EachBlock extends AbstractBlock { if (this.children.length !== 1) { const child = this.children.find(child => !!(child as Element).animation); component.error((child as Element).animation, compiler_errors.invalid_animation_sole); + return; } } diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index 66e963d579..dbde8f1b2d 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -142,6 +142,7 @@ export default class Element extends Node { const value_attribute = info.attributes.find(node => node.name === 'value'); if (value_attribute) { component.error(value_attribute, compiler_errors.textarea_duplicate_value); + return; } // this is an egregious hack, but it's the easiest way to get