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