Skip to content

Commit 14cbb65

Browse files
authored
chore: stricter control flow syntax validation in runes mode (#12342)
disallow characters between `{` and `#` / `:` / `@` in runes mode closes #11975
1 parent 76ddfb3 commit 14cbb65

File tree

9 files changed

+117
-2
lines changed

9 files changed

+117
-2
lines changed

.changeset/mighty-paws-smash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
chore: stricter control flow syntax validation in runes mode

packages/svelte/messages/compile-errors/template.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@
8888

8989
> Block was left open
9090
91+
## block_unexpected_character
92+
93+
> Expected a `%character%` character immediately following the opening bracket
94+
9195
## block_unexpected_close
9296

9397
> Unexpected block closing tag

packages/svelte/src/compiler/errors.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,16 @@ export function block_unclosed(node) {
730730
e(node, "block_unclosed", "Block was left open");
731731
}
732732

733+
/**
734+
* Expected a `%character%` character immediately following the opening bracket
735+
* @param {null | number | NodeLike} node
736+
* @param {string} character
737+
* @returns {never}
738+
*/
739+
export function block_unexpected_character(node, character) {
740+
e(node, "block_unexpected_character", `Expected a \`${character}\` character immediately following the opening bracket`);
741+
}
742+
733743
/**
734744
* Unexpected block closing tag
735745
* @param {null | number | NodeLike} node

packages/svelte/src/compiler/phases/1-parse/state/tag.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ export default function tag(parser) {
3939

4040
/** @param {import('../index.js').Parser} parser */
4141
function open(parser) {
42-
const start = parser.index - 2;
42+
let start = parser.index - 2;
43+
while (parser.template[start] !== '{') start -= 1;
4344

4445
if (parser.eat('if')) {
4546
parser.require_whitespace();
@@ -343,9 +344,12 @@ function next(parser) {
343344
parser.allow_whitespace();
344345
parser.eat('}', true);
345346

347+
let elseif_start = start - 1;
348+
while (parser.template[elseif_start] !== '{') elseif_start -= 1;
349+
346350
/** @type {ReturnType<typeof parser.append<import('#compiler').IfBlock>>} */
347351
const child = parser.append({
348-
start: start - 1,
352+
start: elseif_start,
349353
end: -1,
350354
type: 'IfBlock',
351355
elseif: true,

packages/svelte/src/compiler/phases/2-analyze/validation.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,20 @@ function validate_no_const_assignment(node, argument, scope, is_binding) {
10901090
}
10911091
}
10921092

1093+
/**
1094+
* Validates that the opening of a control flow block is `{` immediately followed by the expected character.
1095+
* In legacy mode whitespace is allowed inbetween. TODO remove once legacy mode is gone and move this into parser instead.
1096+
* @param {{start: number; end: number}} node
1097+
* @param {import('./types.js').AnalysisState} state
1098+
* @param {string} expected
1099+
*/
1100+
function validate_opening_tag(node, state, expected) {
1101+
if (state.analysis.source[node.start + 1] !== expected) {
1102+
// avoid a sea of red and only mark the first few characters
1103+
e.block_unexpected_character({ start: node.start, end: node.start + 5 }, expected);
1104+
}
1105+
}
1106+
10931107
/**
10941108
* @param {import('estree').AssignmentExpression | import('estree').UpdateExpression} node
10951109
* @param {import('estree').Pattern | import('estree').Expression} argument
@@ -1217,6 +1231,8 @@ export const validation_runes = merge(validation, a11y_validators, {
12171231
validate_call_expression(node, state.scope, path);
12181232
},
12191233
EachBlock(node, { next, state }) {
1234+
validate_opening_tag(node, state, '#');
1235+
12201236
const context = node.context;
12211237
if (
12221238
context.type === 'Identifier' &&
@@ -1226,6 +1242,51 @@ export const validation_runes = merge(validation, a11y_validators, {
12261242
}
12271243
next({ ...state });
12281244
},
1245+
IfBlock(node, { state, path }) {
1246+
const parent = path.at(-1);
1247+
const expected =
1248+
path.at(-2)?.type === 'IfBlock' && parent?.type === 'Fragment' && parent.nodes.length === 1
1249+
? ':'
1250+
: '#';
1251+
validate_opening_tag(node, state, expected);
1252+
},
1253+
AwaitBlock(node, { state }) {
1254+
validate_opening_tag(node, state, '#');
1255+
1256+
if (node.value) {
1257+
const start = /** @type {number} */ (node.value.start);
1258+
const match = state.analysis.source.substring(start - 10, start).match(/{(\s*):then\s+$/);
1259+
if (match && match[1] !== '') {
1260+
e.block_unexpected_character({ start: start - 10, end: start }, ':');
1261+
}
1262+
}
1263+
1264+
if (node.error) {
1265+
const start = /** @type {number} */ (node.error.start);
1266+
const match = state.analysis.source.substring(start - 10, start).match(/{(\s*):catch\s+$/);
1267+
if (match && match[1] !== '') {
1268+
e.block_unexpected_character({ start: start - 10, end: start }, ':');
1269+
}
1270+
}
1271+
},
1272+
KeyBlock(node, { state }) {
1273+
validate_opening_tag(node, state, '#');
1274+
},
1275+
SnippetBlock(node, { state }) {
1276+
validate_opening_tag(node, state, '#');
1277+
},
1278+
ConstTag(node, { state }) {
1279+
validate_opening_tag(node, state, '@');
1280+
},
1281+
HtmlTag(node, { state }) {
1282+
validate_opening_tag(node, state, '@');
1283+
},
1284+
DebugTag(node, { state }) {
1285+
validate_opening_tag(node, state, '@');
1286+
},
1287+
RenderTag(node, { state }) {
1288+
validate_opening_tag(node, state, '@');
1289+
},
12291290
VariableDeclarator(node, { state }) {
12301291
ensure_no_module_import_conflict(node, state);
12311292

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<svelte:options runes={false} />
2+
3+
<!-- prettier-ignore -->
4+
<div>
5+
{ #if true}
6+
<p>hi</p>
7+
{/if}
8+
</div>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[
2+
{
3+
"code": "block_unexpected_character",
4+
"message": "Expected a `#` character immediately following the opening bracket",
5+
"start": {
6+
"line": 5,
7+
"column": 1
8+
},
9+
"end": {
10+
"line": 5,
11+
"column": 6
12+
}
13+
}
14+
]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<svelte:options runes={true} />
2+
3+
<!-- prettier-ignore -->
4+
<div>
5+
{ #if true}
6+
<p>hi</p>
7+
{/if}
8+
</div>

0 commit comments

Comments
 (0)