Skip to content

Commit a60e837

Browse files
fix: prevent infinite loops when pruning CSS (#14474)
fixes #14472 --------- Co-authored-by: Simon Holthausen <[email protected]>
1 parent ca3690f commit a60e837

File tree

6 files changed

+88
-3
lines changed

6 files changed

+88
-3
lines changed

.changeset/itchy-timers-relax.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+
fix: prevent infinite loops when pruning CSS

packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ const nesting_selector = {
4343
}
4444
};
4545

46+
/**
47+
* Snippets encountered already (avoids infinite loops)
48+
* @type {Set<Compiler.AST.SnippetBlock>}
49+
*/
50+
const seen = new Set();
51+
4652
/**
4753
*
4854
* @param {Compiler.Css.StyleSheet} stylesheet
@@ -60,6 +66,8 @@ export function prune(stylesheet, element) {
6066
ComplexSelector(node) {
6167
const selectors = get_relative_selectors(node);
6268

69+
seen.clear();
70+
6371
if (
6472
apply_selector(selectors, /** @type {Compiler.Css.Rule} */ (node.metadata.rule), element)
6573
) {
@@ -193,6 +201,9 @@ function apply_combinator(relative_selector, parent_selectors, rule, node) {
193201
const parent = path[i];
194202

195203
if (parent.type === 'SnippetBlock') {
204+
if (seen.has(parent)) return true;
205+
seen.add(parent);
206+
196207
for (const site of parent.metadata.sites) {
197208
if (apply_combinator(relative_selector, parent_selectors, rule, site)) {
198209
return true;
@@ -335,6 +346,8 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
335346
descendant_elements.push(element);
336347
}
337348

349+
const seen = new Set();
350+
338351
/**
339352
* @param {Compiler.SvelteNode} node
340353
* @param {{ is_child: boolean }} state
@@ -355,6 +368,9 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
355368
}
356369
} else if (node.type === 'RenderTag') {
357370
for (const snippet of node.metadata.snippets) {
371+
if (seen.has(snippet)) continue;
372+
373+
seen.add(snippet);
358374
walk_children(snippet.body, context.state);
359375
}
360376
} else {
@@ -618,6 +634,8 @@ function get_following_sibling_elements(element, include_self) {
618634
// ...then walk them, starting from the node after the one
619635
// containing the element in question
620636

637+
const seen = new Set();
638+
621639
/** @param {Compiler.SvelteNode} node */
622640
function get_siblings(node) {
623641
walk(node, null, {
@@ -629,6 +647,9 @@ function get_following_sibling_elements(element, include_self) {
629647
},
630648
RenderTag(node) {
631649
for (const snippet of node.metadata.snippets) {
650+
if (seen.has(snippet)) continue;
651+
652+
seen.add(snippet);
632653
get_siblings(snippet.body);
633654
}
634655
}
@@ -804,9 +825,10 @@ function get_element_parent(node) {
804825
/**
805826
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
806827
* @param {boolean} adjacent_only
828+
* @param {Set<Compiler.AST.SnippetBlock>} seen
807829
* @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>}
808830
*/
809-
function get_possible_element_siblings(node, adjacent_only) {
831+
function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
810832
/** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>} */
811833
const result = new Map();
812834
const path = node.metadata.path;
@@ -865,8 +887,11 @@ function get_possible_element_siblings(node, adjacent_only) {
865887
}
866888

867889
if (current.type === 'SnippetBlock') {
890+
if (seen.has(current)) break;
891+
seen.add(current);
892+
868893
for (const site of current.metadata.sites) {
869-
const siblings = get_possible_element_siblings(site, adjacent_only);
894+
const siblings = get_possible_element_siblings(site, adjacent_only, seen);
870895
add_to_map(siblings, result);
871896

872897
if (adjacent_only && current.metadata.sites.size === 1 && has_definite_elements(siblings)) {

packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function visit_component(node, context) {
5151
if (binding?.initial?.type === 'SnippetBlock') {
5252
node.metadata.snippets.add(binding.initial);
5353
}
54-
} else {
54+
} else if (expression.type !== 'Literal') {
5555
resolved = false;
5656
}
5757
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
warnings: [
5+
{
6+
code: 'css_unused_selector',
7+
message: 'Unused CSS selector "div + div"',
8+
start: {
9+
line: 19,
10+
column: 1,
11+
character: 185
12+
},
13+
end: {
14+
line: 19,
15+
column: 10,
16+
character: 194
17+
}
18+
}
19+
]
20+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
div div.svelte-xyz {
3+
color: green;
4+
}
5+
/* (unused) div + div {
6+
color: red; /* this is marked as unused, but only because we've written an infinite loop - worth fixing? *\/
7+
}*/
8+
div.svelte-xyz:has(div:where(.svelte-xyz)) {
9+
color: green;
10+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{#snippet a()}
2+
{@render b()}
3+
<div>
4+
{@render b()}
5+
</div>
6+
{/snippet}
7+
8+
{#snippet b()}
9+
{@render a()}
10+
<div>
11+
{@render a()}
12+
</div>
13+
{/snippet}
14+
15+
<style>
16+
div div {
17+
color: green;
18+
}
19+
div + div {
20+
color: red; /* this is marked as unused, but only because we've written an infinite loop - worth fixing? */
21+
}
22+
div:has(div) {
23+
color: green;
24+
}
25+
</style>

0 commit comments

Comments
 (0)