diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index 67b2cd4380..aea7dc3f5e 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -27,7 +27,7 @@ import Slot from './nodes/Slot'; import { Node, ImportDeclaration, Identifier, Program, ExpressionStatement, AssignmentExpression, Literal } from 'estree'; import add_to_set from './utils/add_to_set'; import check_graph_for_cycles from './utils/check_graph_for_cycles'; -import { print, x } from 'code-red'; +import { print, x, b } from 'code-red'; interface ComponentOptions { namespace?: string; @@ -722,6 +722,8 @@ export default class Component { toRemove.unshift([parent, prop, index]); }; + const toInsert = new Map(); + walk(content, { enter(node, parent, prop, index) { if (map.has(node)) { @@ -747,12 +749,37 @@ export default class Component { } component.warn_on_undefined_store_value_references(node, parent, scope); + + if (component.compile_options.dev) { + const to_insert_for_loop_protect = component.loop_protect(node, prop, index); + if (to_insert_for_loop_protect) { + if (!Array.isArray(parent[prop])) { + parent[prop] = { + type: 'BlockStatement', + body: [to_insert_for_loop_protect.node, node], + }; + } else { + // can't insert directly, will screw up the index in the for-loop of estree-walker + if (!toInsert.has(parent)) { + toInsert.set(parent, []); + } + toInsert.get(parent).push(to_insert_for_loop_protect); + } + } + } }, leave(node) { if (map.has(node)) { scope = scope.parent; } + if (toInsert.has(node)) { + const nodes_to_insert = toInsert.get(node); + for (const { index, prop, node: node_to_insert } of nodes_to_insert.reverse()) { + node[prop].splice(index, 0, node_to_insert); + } + toInsert.delete(node); + } }, }); @@ -836,6 +863,35 @@ export default class Component { } } + loop_protect(node, prop, index) { + if (node.type === 'WhileStatement' || + node.type === 'ForStatement' || + node.type === 'DoWhileStatement') { + const id = this.get_unique_name('LP'); + this.add_var({ + name: id.name, + internal: true, + }); + + const before = b`const ${id} = Date.now();`; + const inside = b` + if (Date.now() - ${id} > 100) { + throw new Error('Infinite loop detected'); + } + `; + // wrap expression statement with BlockStatement + if (node.body.type !== 'BlockStatement') { + node.body = { + type: 'BlockStatement', + body: [node.body], + }; + } + node.body.body.push(inside[0]); + return { index, prop, node: before[0] }; + } + return null; + } + invalidate(name, value?) { const variable = this.var_lookup.get(name); diff --git a/test/js/samples/loop_protect/_config.js b/test/js/samples/loop_protect/_config.js new file mode 100644 index 0000000000..b1f2518e8a --- /dev/null +++ b/test/js/samples/loop_protect/_config.js @@ -0,0 +1,5 @@ +export default { + options: { + dev: true + } +}; \ No newline at end of file diff --git a/test/js/samples/loop_protect/expected.js b/test/js/samples/loop_protect/expected.js new file mode 100644 index 0000000000..3119b4081c --- /dev/null +++ b/test/js/samples/loop_protect/expected.js @@ -0,0 +1,126 @@ +/* generated by Svelte vX.Y.Z */ +import { + SvelteComponentDev, + dispatch_dev, + init, + noop, + safe_not_equal +} from "svelte/internal"; + +const file = undefined; + +function create_fragment(ctx) { + const block = { + c: noop, + l: function claim(nodes) { + throw new Error("options.hydrate only works if the component was compiled with the `hydratable: true` option"); + }, + m: noop, + p: noop, + i: noop, + o: noop, + d: noop + }; + + dispatch_dev("SvelteRegisterBlock", { + block, + id: create_fragment.name, + type: "component", + source: "", + ctx + }); + + return block; +} + +function instance($$self) { + const LP = Date.now(); + + while (true) { + foo(); + + if (Date.now() - LP > 100) { + throw new Error("Infinite loop detected"); + } + } + + const LP_1 = Date.now(); + + for (; ; ) { + foo(); + + if (Date.now() - LP_1 > 100) { + throw new Error("Infinite loop detected"); + } + } + + const LP_2 = Date.now(); + + while (true) { + foo(); + + if (Date.now() - LP_2 > 100) { + throw new Error("Infinite loop detected"); + } + } + + const LP_4 = Date.now(); + + do { + foo(); + + if (Date.now() - LP_4 > 100) { + throw new Error("Infinite loop detected"); + } + } while (true); + + $$self.$capture_state = () => { + return {}; + }; + + $$self.$inject_state = $$props => { + + }; + + $: { + const LP_3 = Date.now(); + + while (true) { + foo(); + + if (Date.now() - LP_3 > 100) { + throw new Error("Infinite loop detected"); + } + } + } + + $: { + const LP_5 = Date.now(); + + do { + foo(); + + if (Date.now() - LP_5 > 100) { + throw new Error("Infinite loop detected"); + } + } while (true); + } + + return {}; +} + +class Component extends SvelteComponentDev { + constructor(options) { + super(options); + init(this, options, instance, create_fragment, safe_not_equal, {}); + + dispatch_dev("SvelteRegisterComponent", { + component: this, + tagName: "Component", + options, + id: create_fragment.name + }); + } +} + +export default Component; \ No newline at end of file diff --git a/test/js/samples/loop_protect/input.svelte b/test/js/samples/loop_protect/input.svelte new file mode 100644 index 0000000000..c39ea75f72 --- /dev/null +++ b/test/js/samples/loop_protect/input.svelte @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/test/runtime/samples/loop-protect/_config.js b/test/runtime/samples/loop-protect/_config.js new file mode 100644 index 0000000000..2a1f5cad4f --- /dev/null +++ b/test/runtime/samples/loop-protect/_config.js @@ -0,0 +1,6 @@ +export default { + error: 'Infinite loop detected', + compileOptions: { + dev: true, + } +}; diff --git a/test/runtime/samples/loop-protect/main.svelte b/test/runtime/samples/loop-protect/main.svelte new file mode 100644 index 0000000000..aff13c82f1 --- /dev/null +++ b/test/runtime/samples/loop-protect/main.svelte @@ -0,0 +1,5 @@ + \ No newline at end of file