diff --git a/CHANGELOG.md b/CHANGELOG.md index 899e0d5b83..6337653fb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased * Fix binding to module-level variables ([#4086](https://github.com/sveltejs/svelte/issues/4086)) +* Improve parsing error messages when there is a pending unclosed tag ([#4131](https://github.com/sveltejs/svelte/issues/4131)) * Disallow attribute/prop names from matching two-way-bound names or `{shorthand}` attribute/prop names ([#4325](https://github.com/sveltejs/svelte/issues/4325)) * Improve performance of `flush()` by not using `.shift()` ([#4356](https://github.com/sveltejs/svelte/pull/4356)) * Fix code generation error with precedence of arrow functions ([#4384](https://github.com/sveltejs/svelte/issues/4384)) diff --git a/src/compiler/parse/state/mustache.ts b/src/compiler/parse/state/mustache.ts index 140e722b10..e5e365dddf 100644 --- a/src/compiler/parse/state/mustache.ts +++ b/src/compiler/parse/state/mustache.ts @@ -3,6 +3,7 @@ import read_expression from '../read/expression'; import { closing_tag_omitted } from '../utils/html'; import { whitespace } from '../../utils/patterns'; import { trim_start, trim_end } from '../../utils/trim'; +import { to_string } from '../utils/node'; import { Parser } from '../index'; import { TemplateNode } from '../../interfaces'; @@ -106,11 +107,14 @@ export default function mustache(parser: Parser) { // :else if if (parser.eat('if')) { const block = parser.current(); - if (block.type !== 'IfBlock') + if (block.type !== 'IfBlock') { parser.error({ code: `invalid-elseif-placement`, - message: 'Cannot have an {:else if ...} block outside an {#if ...} block' + message: parser.stack.some(block => block.type === 'IfBlock') + ? `Expected to close ${to_string(block)} before seeing {:else if ...} block` + : `Cannot have an {:else if ...} block outside an {#if ...} block` }); + } parser.require_whitespace(); @@ -144,7 +148,9 @@ export default function mustache(parser: Parser) { if (block.type !== 'IfBlock' && block.type !== 'EachBlock') { parser.error({ code: `invalid-else-placement`, - message: 'Cannot have an {:else} block outside an {#if ...} or {#each ...} block' + message: parser.stack.some(block => block.type === 'IfBlock' || block.type === 'EachBlock') + ? `Expected to close ${to_string(block)} before seeing {:else} block` + : `Cannot have an {:else} block outside an {#if ...} or {#each ...} block` }); } @@ -168,14 +174,18 @@ export default function mustache(parser: Parser) { if (block.type !== 'PendingBlock') { parser.error({ code: `invalid-then-placement`, - message: 'Cannot have an {:then} block outside an {#await ...} block' + message: parser.stack.some(block => block.type === 'PendingBlock') + ? `Expected to close ${to_string(block)} before seeing {:then} block` + : `Cannot have an {:then} block outside an {#await ...} block` }); } } else { if (block.type !== 'ThenBlock' && block.type !== 'PendingBlock') { parser.error({ code: `invalid-catch-placement`, - message: 'Cannot have an {:catch} block outside an {#await ...} block' + message: parser.stack.some(block => block.type === 'ThenBlock' || block.type === 'PendingBlock') + ? `Expected to close ${to_string(block)} before seeing {:catch} block` + : `Cannot have an {:catch} block outside an {#await ...} block` }); } } diff --git a/src/compiler/parse/utils/node.ts b/src/compiler/parse/utils/node.ts new file mode 100644 index 0000000000..0d39529b59 --- /dev/null +++ b/src/compiler/parse/utils/node.ts @@ -0,0 +1,30 @@ +import { TemplateNode } from '../../interfaces'; + +export function to_string(node: TemplateNode) { + switch (node.type) { + case 'IfBlock': + return '{#if} block'; + case 'ThenBlock': + return '{:then} block'; + case 'ElseBlock': + return '{:else} block'; + case 'PendingBlock': + case 'AwaitBlock': + return '{#await} block'; + case 'CatchBlock': + return '{:catch} block'; + case 'EachBlock': + return '{#each} block'; + case 'RawMustacheTag': + return '{@html} block'; + case 'DebugTag': + return '{@debug} block'; + case 'Element': + case 'InlineComponent': + case 'Slot': + case 'Title': + return `<${node.name}> tag`; + default: + return node.type; + } +} diff --git a/test/parser/samples/error-catch-before-closing/error.json b/test/parser/samples/error-catch-before-closing/error.json new file mode 100644 index 0000000000..30fbf5fbd5 --- /dev/null +++ b/test/parser/samples/error-catch-before-closing/error.json @@ -0,0 +1,6 @@ +{ + "code": "invalid-catch-placement", + "message": "Expected to close {#each} block before seeing {:catch} block", + "start": { "line": 3, "column": 7, "character": 41 }, + "pos": 41 +} diff --git a/test/parser/samples/error-catch-before-closing/input.svelte b/test/parser/samples/error-catch-before-closing/input.svelte new file mode 100644 index 0000000000..bd771701e2 --- /dev/null +++ b/test/parser/samples/error-catch-before-closing/input.svelte @@ -0,0 +1,4 @@ +{#await true} + {#each foo as bar} +{:catch f} +{/await} \ No newline at end of file diff --git a/test/parser/samples/error-else-before-closing-2/error.json b/test/parser/samples/error-else-before-closing-2/error.json new file mode 100644 index 0000000000..d35be1abf5 --- /dev/null +++ b/test/parser/samples/error-else-before-closing-2/error.json @@ -0,0 +1,6 @@ +{ + "code": "invalid-else-placement", + "message": "Expected to close {#await} block before seeing {:else} block", + "start": { "line": 3, "column": 6, "character": 29 }, + "pos": 29 +} diff --git a/test/parser/samples/error-else-before-closing-2/input.svelte b/test/parser/samples/error-else-before-closing-2/input.svelte new file mode 100644 index 0000000000..f865a9a533 --- /dev/null +++ b/test/parser/samples/error-else-before-closing-2/input.svelte @@ -0,0 +1,4 @@ +{#if true} + {#await p} +{:else} +{/if} \ No newline at end of file diff --git a/test/parser/samples/error-else-before-closing-3/error.json b/test/parser/samples/error-else-before-closing-3/error.json new file mode 100644 index 0000000000..e5ec210c75 --- /dev/null +++ b/test/parser/samples/error-else-before-closing-3/error.json @@ -0,0 +1,6 @@ +{ + "code": "invalid-else-placement", + "message": "Cannot have an {:else} block outside an {#if ...} or {#each ...} block", + "start": { "line": 2, "column": 6, "character": 11 }, + "pos": 11 +} diff --git a/test/parser/samples/error-else-before-closing-3/input.svelte b/test/parser/samples/error-else-before-closing-3/input.svelte new file mode 100644 index 0000000000..fb434f26a3 --- /dev/null +++ b/test/parser/samples/error-else-before-closing-3/input.svelte @@ -0,0 +1,2 @@ +
tag before seeing {:else if ...} block", + "start": { "line": 3, "column": 9, "character": 25 }, + "pos": 25 +} diff --git a/test/parser/samples/error-else-if-before-closing-2/input.svelte b/test/parser/samples/error-else-if-before-closing-2/input.svelte new file mode 100644 index 0000000000..5ae36df4eb --- /dev/null +++ b/test/parser/samples/error-else-if-before-closing-2/input.svelte @@ -0,0 +1,4 @@ +{#if true} +
+{:else if false} +{/if} \ No newline at end of file diff --git a/test/parser/samples/error-else-if-before-closing/error.json b/test/parser/samples/error-else-if-before-closing/error.json new file mode 100644 index 0000000000..21d16c72a4 --- /dev/null +++ b/test/parser/samples/error-else-if-before-closing/error.json @@ -0,0 +1,6 @@ +{ + "code": "invalid-elseif-placement", + "message": "Expected to close {#await} block before seeing {:else if ...} block", + "start": { "line": 3, "column": 9, "character": 34 }, + "pos": 34 +} diff --git a/test/parser/samples/error-else-if-before-closing/input.svelte b/test/parser/samples/error-else-if-before-closing/input.svelte new file mode 100644 index 0000000000..486a921a9a --- /dev/null +++ b/test/parser/samples/error-else-if-before-closing/input.svelte @@ -0,0 +1,4 @@ +{#if true} + {#await foo} +{:else if false} +{/if} \ No newline at end of file diff --git a/test/parser/samples/error-else-if-without-if/error.json b/test/parser/samples/error-else-if-without-if/error.json new file mode 100644 index 0000000000..dd69df6772 --- /dev/null +++ b/test/parser/samples/error-else-if-without-if/error.json @@ -0,0 +1,6 @@ +{ + "code": "invalid-elseif-placement", + "message": "Cannot have an {:else if ...} block outside an {#if ...} block", + "start": { "line": 3, "column": 10, "character": 35 }, + "pos": 35 +} diff --git a/test/parser/samples/error-else-if-without-if/input.svelte b/test/parser/samples/error-else-if-without-if/input.svelte new file mode 100644 index 0000000000..06baf33511 --- /dev/null +++ b/test/parser/samples/error-else-if-without-if/input.svelte @@ -0,0 +1,4 @@ +{#await foo} +{:then bar} + {:else if} +{/await} \ No newline at end of file diff --git a/test/parser/samples/error-then-before-closing/error.json b/test/parser/samples/error-then-before-closing/error.json new file mode 100644 index 0000000000..07310c93fa --- /dev/null +++ b/test/parser/samples/error-then-before-closing/error.json @@ -0,0 +1,6 @@ +{ + "code": "invalid-then-placement", + "message": "Expected to close