fix: prevent infinite loops when pruning CSS (#14474)

fixes #14472

---------

Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
pull/14480/head
Rich Harris 4 weeks ago committed by GitHub
parent ca3690f7df
commit a60e837e44
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent infinite loops when pruning CSS

@ -43,6 +43,12 @@ const nesting_selector = {
} }
}; };
/**
* Snippets encountered already (avoids infinite loops)
* @type {Set<Compiler.AST.SnippetBlock>}
*/
const seen = new Set();
/** /**
* *
* @param {Compiler.Css.StyleSheet} stylesheet * @param {Compiler.Css.StyleSheet} stylesheet
@ -60,6 +66,8 @@ export function prune(stylesheet, element) {
ComplexSelector(node) { ComplexSelector(node) {
const selectors = get_relative_selectors(node); const selectors = get_relative_selectors(node);
seen.clear();
if ( if (
apply_selector(selectors, /** @type {Compiler.Css.Rule} */ (node.metadata.rule), element) apply_selector(selectors, /** @type {Compiler.Css.Rule} */ (node.metadata.rule), element)
) { ) {
@ -193,6 +201,9 @@ function apply_combinator(relative_selector, parent_selectors, rule, node) {
const parent = path[i]; const parent = path[i];
if (parent.type === 'SnippetBlock') { if (parent.type === 'SnippetBlock') {
if (seen.has(parent)) return true;
seen.add(parent);
for (const site of parent.metadata.sites) { for (const site of parent.metadata.sites) {
if (apply_combinator(relative_selector, parent_selectors, rule, site)) { if (apply_combinator(relative_selector, parent_selectors, rule, site)) {
return true; return true;
@ -335,6 +346,8 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
descendant_elements.push(element); descendant_elements.push(element);
} }
const seen = new Set();
/** /**
* @param {Compiler.SvelteNode} node * @param {Compiler.SvelteNode} node
* @param {{ is_child: boolean }} state * @param {{ is_child: boolean }} state
@ -355,6 +368,9 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
} }
} else if (node.type === 'RenderTag') { } else if (node.type === 'RenderTag') {
for (const snippet of node.metadata.snippets) { for (const snippet of node.metadata.snippets) {
if (seen.has(snippet)) continue;
seen.add(snippet);
walk_children(snippet.body, context.state); walk_children(snippet.body, context.state);
} }
} else { } else {
@ -618,6 +634,8 @@ function get_following_sibling_elements(element, include_self) {
// ...then walk them, starting from the node after the one // ...then walk them, starting from the node after the one
// containing the element in question // containing the element in question
const seen = new Set();
/** @param {Compiler.SvelteNode} node */ /** @param {Compiler.SvelteNode} node */
function get_siblings(node) { function get_siblings(node) {
walk(node, null, { walk(node, null, {
@ -629,6 +647,9 @@ function get_following_sibling_elements(element, include_self) {
}, },
RenderTag(node) { RenderTag(node) {
for (const snippet of node.metadata.snippets) { for (const snippet of node.metadata.snippets) {
if (seen.has(snippet)) continue;
seen.add(snippet);
get_siblings(snippet.body); get_siblings(snippet.body);
} }
} }
@ -804,9 +825,10 @@ function get_element_parent(node) {
/** /**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @param {boolean} adjacent_only * @param {boolean} adjacent_only
* @param {Set<Compiler.AST.SnippetBlock>} seen
* @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>} * @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>}
*/ */
function get_possible_element_siblings(node, adjacent_only) { function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
/** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>} */ /** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>} */
const result = new Map(); const result = new Map();
const path = node.metadata.path; const path = node.metadata.path;
@ -865,8 +887,11 @@ function get_possible_element_siblings(node, adjacent_only) {
} }
if (current.type === 'SnippetBlock') { if (current.type === 'SnippetBlock') {
if (seen.has(current)) break;
seen.add(current);
for (const site of current.metadata.sites) { for (const site of current.metadata.sites) {
const siblings = get_possible_element_siblings(site, adjacent_only); const siblings = get_possible_element_siblings(site, adjacent_only, seen);
add_to_map(siblings, result); add_to_map(siblings, result);
if (adjacent_only && current.metadata.sites.size === 1 && has_definite_elements(siblings)) { if (adjacent_only && current.metadata.sites.size === 1 && has_definite_elements(siblings)) {

@ -51,7 +51,7 @@ export function visit_component(node, context) {
if (binding?.initial?.type === 'SnippetBlock') { if (binding?.initial?.type === 'SnippetBlock') {
node.metadata.snippets.add(binding.initial); node.metadata.snippets.add(binding.initial);
} }
} else { } else if (expression.type !== 'Literal') {
resolved = false; resolved = false;
} }
} }

@ -0,0 +1,20 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css_unused_selector',
message: 'Unused CSS selector "div + div"',
start: {
line: 19,
column: 1,
character: 185
},
end: {
line: 19,
column: 10,
character: 194
}
}
]
});

@ -0,0 +1,10 @@
div div.svelte-xyz {
color: green;
}
/* (unused) div + div {
color: red; /* this is marked as unused, but only because we've written an infinite loop - worth fixing? *\/
}*/
div.svelte-xyz:has(div:where(.svelte-xyz)) {
color: green;
}

@ -0,0 +1,25 @@
{#snippet a()}
{@render b()}
<div>
{@render b()}
</div>
{/snippet}
{#snippet b()}
{@render a()}
<div>
{@render a()}
</div>
{/snippet}
<style>
div div {
color: green;
}
div + div {
color: red; /* this is marked as unused, but only because we've written an infinite loop - worth fixing? */
}
div:has(div) {
color: green;
}
</style>
Loading…
Cancel
Save