diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index 77ab48545e88..ee8c44123fd3 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -24,6 +24,7 @@ import unwrap_parens from './utils/unwrap_parens'; import Slot from './nodes/Slot'; import { Node as ESTreeNode } from 'estree'; import add_to_set from './utils/add_to_set'; +import checkGraphForCycles from './utils/checkGraphForCycles'; interface ComponentOptions { namespace?: string; @@ -1205,14 +1206,27 @@ export default class Component { }); }); - const add_declaration = declaration => { - if (seen.has(declaration)) { - this.error(declaration.node, { - code: 'cyclical-reactive-declaration', - message: 'Cyclical dependency detected' + const cycle = checkGraphForCycles(unsorted_reactive_declarations.reduce((acc, declaration) => { + declaration.assignees.forEach(v => { + declaration.dependencies.forEach(w => { + if (!declaration.assignees.has(w)) { + acc.push([v, w]); + } }); - } + }); + return acc; + }, [])); + + 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(' → ')}` + }); + } + const add_declaration = declaration => { if (this.reactive_declarations.indexOf(declaration) !== -1) { return; } diff --git a/src/compiler/compile/utils/checkGraphForCycles.ts b/src/compiler/compile/utils/checkGraphForCycles.ts new file mode 100644 index 000000000000..718d2e26659f --- /dev/null +++ b/src/compiler/compile/utils/checkGraphForCycles.ts @@ -0,0 +1,36 @@ +export default function checkGraphForCycles(edges: Array<[any, any]>): any[] { + const graph: Map = edges.reduce((g, edge) => { + const [u, v] = edge; + if (!g.has(u)) g.set(u, []); + if (!g.has(v)) g.set(v, []); + g.get(u).push(v); + return g; + }, new Map()); + + const visited = new Set(); + const onStack = new Set(); + const cycles = []; + + function visit (v) { + visited.add(v); + onStack.add(v); + + graph.get(v).forEach(w => { + if (!visited.has(w)) { + visit(w); + } else if (onStack.has(w)) { + cycles.push([...onStack, w]); + } + }); + + onStack.delete(v); + } + + graph.forEach((_, v) => { + if (!visited.has(v)) { + visit(v); + } + }); + + return cycles[0]; +} diff --git a/test/runtime/samples/reactive-values-non-cyclical-declaration-order-independent/_config.js b/test/runtime/samples/reactive-values-non-cyclical-declaration-order-independent/_config.js new file mode 100644 index 000000000000..989e6d458c12 --- /dev/null +++ b/test/runtime/samples/reactive-values-non-cyclical-declaration-order-independent/_config.js @@ -0,0 +1,11 @@ +export default { + html: ` +

2+2=4

+ `, + + test({ assert, target }) { + assert.htmlEqual(target.innerHTML, ` +

2+2=4

+ `); + } +}; diff --git a/test/runtime/samples/reactive-values-non-cyclical-declaration-order-independent/main.svelte b/test/runtime/samples/reactive-values-non-cyclical-declaration-order-independent/main.svelte new file mode 100644 index 000000000000..766ca7aa86db --- /dev/null +++ b/test/runtime/samples/reactive-values-non-cyclical-declaration-order-independent/main.svelte @@ -0,0 +1,7 @@ + + +

{a}+{b}={c}

diff --git a/test/validator/samples/reactive-declaration-cyclical/errors.json b/test/validator/samples/reactive-declaration-cyclical/errors.json index 1e4169a0eec0..9a80d9fb293b 100644 --- a/test/validator/samples/reactive-declaration-cyclical/errors.json +++ b/test/validator/samples/reactive-declaration-cyclical/errors.json @@ -1,7 +1,7 @@ [{ - "message": "Cyclical dependency detected", + "message": "Cyclical dependency detected: a → b → a", "code": "cyclical-reactive-declaration", "start": { "line": 5, "column": 1, "character": 35 }, "end": { "line": 5, "column": 14, "character": 48 }, "pos": 35 -}] \ No newline at end of file +}]