Skip to content

Commit 88fcb9c

Browse files
committed
more robust expression parsing
1 parent e610f1a commit 88fcb9c

File tree

6 files changed

+228
-29
lines changed

6 files changed

+228
-29
lines changed

packages/svelte/src/compiler/phases/1-parse/read/expression.js

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { parse_expression_at } from '../acorn.js';
44
import { regex_whitespace } from '../../patterns.js';
55
import * as e from '../../../errors.js';
6+
import { find_matching_bracket } from '../utils/bracket.js';
67

78
/**
89
* @param {Parser} parser
@@ -41,33 +42,17 @@ export default function read_expression(parser) {
4142
} catch (err) {
4243
if (parser.loose) {
4344
// Find the next } and treat it as the end of the expression
44-
let index = parser.index;
45-
let num_braces = 0;
46-
while (index < parser.template.length) {
47-
const char = parser.template[index];
48-
if (char === '{') num_braces += 1;
49-
if (char === '}') {
50-
if (num_braces === 0) {
51-
// We assume that there's some kind of whitespace or the start of the closing tag after the closing brace,
52-
// else this hints at a wrong counting of braces (e.g. in the case of foo={'hi}'})
53-
if (!/[\s>/]/.test(parser.template[index + 1])) {
54-
num_braces += 1;
55-
continue;
56-
}
57-
58-
const start = parser.index;
59-
parser.index = index;
60-
// We don't know what the expression is and signal this by returning an empty identifier
61-
return {
62-
type: 'Identifier',
63-
start,
64-
end: index,
65-
name: ''
66-
};
67-
}
68-
num_braces -= 1;
69-
}
70-
index += 1;
45+
const end = find_matching_bracket(parser.template, parser.index, '{');
46+
if (end) {
47+
const start = parser.index;
48+
parser.index = end;
49+
// We don't know what the expression is and signal this by returning an empty identifier
50+
return {
51+
type: 'Identifier',
52+
start,
53+
end,
54+
name: ''
55+
};
7156
}
7257
}
7358

packages/svelte/src/compiler/phases/1-parse/utils/bracket.js

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import full_char_code_at from './full_char_code_at.js';
2+
13
const SQUARE_BRACKET_OPEN = '['.charCodeAt(0);
24
const SQUARE_BRACKET_CLOSE = ']'.charCodeAt(0);
35
const CURLY_BRACKET_OPEN = '{'.charCodeAt(0);
@@ -33,3 +35,137 @@ export function get_bracket_close(open) {
3335
return CURLY_BRACKET_CLOSE;
3436
}
3537
}
38+
39+
/**
40+
* @param {number} num
41+
* @returns {number} Infinity if {@link num} is negative, else {@link num}.
42+
*/
43+
function infinity_if_negative(num) {
44+
if (num < 0) {
45+
return Infinity;
46+
}
47+
return num;
48+
}
49+
50+
/**
51+
* @param {string} string The string to search.
52+
* @param {number} search_start_index The index to start searching at.
53+
* @param {"'" | '"' | '`'} string_start_char The character that started this string.
54+
* @returns {number} The index of the end of this string expression, or `Infinity` if not found.
55+
*/
56+
function find_string_end(string, search_start_index, string_start_char) {
57+
let string_to_search;
58+
if (string_start_char === '`') {
59+
string_to_search = string;
60+
} else {
61+
// we could slice at the search start index, but this way the index remains valid
62+
string_to_search = string.slice(
63+
0,
64+
infinity_if_negative(string.indexOf('\n', search_start_index))
65+
);
66+
}
67+
68+
return find_unescaped_char(string_to_search, search_start_index, string_start_char);
69+
}
70+
71+
/**
72+
* @param {string} string The string to search.
73+
* @param {number} search_start_index The index to start searching at.
74+
* @returns {number} The index of the end of this regex expression, or `Infinity` if not found.
75+
*/
76+
function find_regex_end(string, search_start_index) {
77+
return find_unescaped_char(string, search_start_index, '/');
78+
}
79+
80+
/**
81+
*
82+
* @param {string} string The string to search.
83+
* @param {number} search_start_index The index to begin the search at.
84+
* @param {string} char The character to search for.
85+
* @returns {number} The index of the first unescaped instance of {@link char}, or `Infinity` if not found.
86+
*/
87+
function find_unescaped_char(string, search_start_index, char) {
88+
let i = search_start_index;
89+
while (true) {
90+
const found_index = string.indexOf(char, i);
91+
if (found_index === -1) {
92+
return Infinity;
93+
}
94+
if (count_leading_backslashes(string, found_index - 1) % 2 === 0) {
95+
return found_index;
96+
}
97+
i = found_index + 1;
98+
}
99+
}
100+
101+
/**
102+
* Count consecutive leading backslashes before {@link search_start_index}.
103+
*
104+
* @example
105+
* ```js
106+
* count_leading_backslashes('\\\\\\foo', 2); // 3 (the backslashes have to be escaped in the string literal, there are three in reality)
107+
* ```
108+
*
109+
* @param {string} string The string to search.
110+
* @param {number} search_start_index The index to begin the search at.
111+
*/
112+
function count_leading_backslashes(string, search_start_index) {
113+
let i = search_start_index;
114+
let count = 0;
115+
while (string[i] === '\\') {
116+
count++;
117+
i--;
118+
}
119+
return count;
120+
}
121+
122+
/**
123+
* Finds the corresponding closing bracket, ignoring brackets found inside comments, strings, or regex expressions.
124+
* @param {string} template The string to search.
125+
* @param {number} index The index to begin the search at.
126+
* @param {string} open The opening bracket (ex: `'{'` will search for `'}'`).
127+
* @returns {number | undefined} The index of the closing bracket, or undefined if not found.
128+
*/
129+
export function find_matching_bracket(template, index, open) {
130+
const open_code = full_char_code_at(open, 0);
131+
const close_code = get_bracket_close(open_code);
132+
let brackets = 1;
133+
let i = index;
134+
while (brackets > 0 && i < template.length) {
135+
const char = template[i];
136+
switch (char) {
137+
case "'":
138+
case '"':
139+
case '`':
140+
i = find_string_end(template, i + 1, char) + 1;
141+
continue;
142+
case '/': {
143+
const next_char = template[i + 1];
144+
if (!next_char) continue;
145+
if (next_char === '/') {
146+
i = infinity_if_negative(template.indexOf('\n', i + 1)) + '\n'.length;
147+
continue;
148+
}
149+
if (next_char === '*') {
150+
i = infinity_if_negative(template.indexOf('*/', i + 1)) + '*/'.length;
151+
continue;
152+
}
153+
i = find_regex_end(template, i + 1) + '/'.length;
154+
continue;
155+
}
156+
default: {
157+
const code = full_char_code_at(template, i);
158+
if (code === open_code) {
159+
brackets++;
160+
} else if (code === close_code) {
161+
brackets--;
162+
}
163+
if (brackets === 0) {
164+
return i;
165+
}
166+
i++;
167+
}
168+
}
169+
}
170+
return undefined;
171+
}

packages/svelte/tests/parser-legacy/samples/loose-invalid-expression/input.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@
66
<Component onclick={() => x.} />
77

88
<input bind:value={a.} />
9+
10+
asd{a.}asd
11+
{foo[bar.]}

packages/svelte/tests/parser-legacy/samples/loose-invalid-expression/output.json

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"html": {
33
"type": "Fragment",
44
"start": 0,
5-
"end": 140,
5+
"end": 164,
66
"children": [
77
{
88
"type": "Element",
@@ -200,6 +200,42 @@
200200
}
201201
],
202202
"children": []
203+
},
204+
{
205+
"type": "Text",
206+
"start": 140,
207+
"end": 145,
208+
"raw": "\n\nasd",
209+
"data": "\n\nasd"
210+
},
211+
{
212+
"type": "MustacheTag",
213+
"start": 145,
214+
"end": 149,
215+
"expression": {
216+
"type": "Identifier",
217+
"start": 146,
218+
"end": 148,
219+
"name": ""
220+
}
221+
},
222+
{
223+
"type": "Text",
224+
"start": 149,
225+
"end": 153,
226+
"raw": "asd\n",
227+
"data": "asd\n"
228+
},
229+
{
230+
"type": "MustacheTag",
231+
"start": 153,
232+
"end": 164,
233+
"expression": {
234+
"type": "Identifier",
235+
"start": 154,
236+
"end": 163,
237+
"name": ""
238+
}
203239
}
204240
]
205241
}

packages/svelte/tests/parser-modern/samples/loose-invalid-expression/input.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@
66
<Component onclick={() => x.} />
77

88
<input bind:value={a.} />
9+
10+
asd{a.}asd
11+
{foo[bar.]}

packages/svelte/tests/parser-modern/samples/loose-invalid-expression/output.json

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"css": null,
33
"js": [],
44
"start": 0,
5-
"end": 140,
5+
"end": 164,
66
"type": "Root",
77
"fragment": {
88
"type": "Fragment",
@@ -211,6 +211,42 @@
211211
"type": "Fragment",
212212
"nodes": []
213213
}
214+
},
215+
{
216+
"type": "Text",
217+
"start": 140,
218+
"end": 145,
219+
"raw": "\n\nasd",
220+
"data": "\n\nasd"
221+
},
222+
{
223+
"type": "ExpressionTag",
224+
"start": 145,
225+
"end": 149,
226+
"expression": {
227+
"type": "Identifier",
228+
"start": 146,
229+
"end": 148,
230+
"name": ""
231+
}
232+
},
233+
{
234+
"type": "Text",
235+
"start": 149,
236+
"end": 153,
237+
"raw": "asd\n",
238+
"data": "asd\n"
239+
},
240+
{
241+
"type": "ExpressionTag",
242+
"start": 153,
243+
"end": 164,
244+
"expression": {
245+
"type": "Identifier",
246+
"start": 154,
247+
"end": 163,
248+
"name": ""
249+
}
214250
}
215251
]
216252
},

0 commit comments

Comments
 (0)