fix: correctly match `:has()`'s selector during css pruning (#15277)

Fixes #14072

`:has()` was matching only against descendants or siblings, but not sibling's descendants. This makes the logic be able to go forward or backwards, simplifying a lot of cases along the way.
pull/15516/head
7nik 6 months ago committed by GitHub
parent f227cfcea8
commit 8e9a21e374
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly match `:has()` selector during css pruning

@ -5,9 +5,12 @@ import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../
import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js'; import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js';
/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */ /** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */
/** @typedef {FORWARD | BACKWARD} Direction */
const NODE_PROBABLY_EXISTS = 0; const NODE_PROBABLY_EXISTS = 0;
const NODE_DEFINITELY_EXISTS = 1; const NODE_DEFINITELY_EXISTS = 1;
const FORWARD = 0;
const BACKWARD = 1;
const whitelist_attribute_selector = new Map([ const whitelist_attribute_selector = new Map([
['details', ['open']], ['details', ['open']],
@ -43,6 +46,27 @@ const nesting_selector = {
} }
}; };
/** @type {Compiler.AST.CSS.RelativeSelector} */
const any_selector = {
type: 'RelativeSelector',
start: -1,
end: -1,
combinator: null,
selectors: [
{
type: 'TypeSelector',
name: '*',
start: -1,
end: -1
}
],
metadata: {
is_global: false,
is_global_like: false,
scoped: false
}
};
/** /**
* Snippets encountered already (avoids infinite loops) * Snippets encountered already (avoids infinite loops)
* @type {Set<Compiler.AST.SnippetBlock>} * @type {Set<Compiler.AST.SnippetBlock>}
@ -72,7 +96,8 @@ export function prune(stylesheet, element) {
apply_selector( apply_selector(
selectors, selectors,
/** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule), /** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule),
element element,
BACKWARD
) )
) { ) {
node.metadata.used = true; node.metadata.used = true;
@ -159,16 +184,17 @@ function truncate(node) {
* @param {Compiler.AST.CSS.RelativeSelector[]} relative_selectors * @param {Compiler.AST.CSS.RelativeSelector[]} relative_selectors
* @param {Compiler.AST.CSS.Rule} rule * @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @param {Direction} direction
* @returns {boolean} * @returns {boolean}
*/ */
function apply_selector(relative_selectors, rule, element) { function apply_selector(relative_selectors, rule, element, direction) {
const parent_selectors = relative_selectors.slice(); const rest_selectors = relative_selectors.slice();
const relative_selector = parent_selectors.pop(); const relative_selector = direction === FORWARD ? rest_selectors.shift() : rest_selectors.pop();
const matched = const matched =
!!relative_selector && !!relative_selector &&
relative_selector_might_apply_to_node(relative_selector, rule, element) && relative_selector_might_apply_to_node(relative_selector, rule, element, direction) &&
apply_combinator(relative_selector, parent_selectors, rule, element); apply_combinator(relative_selector, rest_selectors, rule, element, direction);
if (matched) { if (matched) {
if (!is_outer_global(relative_selector)) { if (!is_outer_global(relative_selector)) {
@ -183,76 +209,63 @@ function apply_selector(relative_selectors, rule, element) {
/** /**
* @param {Compiler.AST.CSS.RelativeSelector} relative_selector * @param {Compiler.AST.CSS.RelativeSelector} relative_selector
* @param {Compiler.AST.CSS.RelativeSelector[]} parent_selectors * @param {Compiler.AST.CSS.RelativeSelector[]} rest_selectors
* @param {Compiler.AST.CSS.Rule} rule * @param {Compiler.AST.CSS.Rule} rule
* @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 {Direction} direction
* @returns {boolean} * @returns {boolean}
*/ */
function apply_combinator(relative_selector, parent_selectors, rule, node) { function apply_combinator(relative_selector, rest_selectors, rule, node, direction) {
if (!relative_selector.combinator) return true; const combinator =
direction == FORWARD ? rest_selectors[0]?.combinator : relative_selector.combinator;
if (!combinator) return true;
const name = relative_selector.combinator.name; switch (combinator.name) {
switch (name) {
case ' ': case ' ':
case '>': { case '>': {
const is_adjacent = combinator.name === '>';
const parents =
direction === FORWARD
? get_descendant_elements(node, is_adjacent)
: get_ancestor_elements(node, is_adjacent);
let parent_matched = false; let parent_matched = false;
const path = node.metadata.path; for (const parent of parents) {
let i = path.length; if (apply_selector(rest_selectors, rule, parent, direction)) {
parent_matched = true;
while (i--) {
const parent = path[i];
if (parent.type === 'SnippetBlock') {
if (seen.has(parent)) {
parent_matched = true;
} else {
seen.add(parent);
for (const site of parent.metadata.sites) {
if (apply_combinator(relative_selector, parent_selectors, rule, site)) {
parent_matched = true;
}
}
}
break;
}
if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
if (apply_selector(parent_selectors, rule, parent)) {
parent_matched = true;
}
if (name === '>') return parent_matched;
} }
} }
return parent_matched || parent_selectors.every((selector) => is_global(selector, rule)); return (
parent_matched ||
(direction === BACKWARD &&
(!is_adjacent || parents.length === 0) &&
rest_selectors.every((selector) => is_global(selector, rule)))
);
} }
case '+': case '+':
case '~': { case '~': {
const siblings = get_possible_element_siblings(node, name === '+'); const siblings = get_possible_element_siblings(node, direction, combinator.name === '+');
let sibling_matched = false; let sibling_matched = false;
for (const possible_sibling of siblings.keys()) { for (const possible_sibling of siblings.keys()) {
if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') { if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') {
// `{@render foo()}<p>foo</p>` with `:global(.x) + p` is a match // `{@render foo()}<p>foo</p>` with `:global(.x) + p` is a match
if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) { if (rest_selectors.length === 1 && rest_selectors[0].metadata.is_global) {
sibling_matched = true; sibling_matched = true;
} }
} else if (apply_selector(parent_selectors, rule, possible_sibling)) { } else if (apply_selector(rest_selectors, rule, possible_sibling, direction)) {
sibling_matched = true; sibling_matched = true;
} }
} }
return ( return (
sibling_matched || sibling_matched ||
(get_element_parent(node) === null && (direction === BACKWARD &&
parent_selectors.every((selector) => is_global(selector, rule))) get_element_parent(node) === null &&
rest_selectors.every((selector) => is_global(selector, rule)))
); );
} }
@ -313,9 +326,10 @@ const regex_backslash_and_following_character = /\\(.)/g;
* @param {Compiler.AST.CSS.RelativeSelector} relative_selector * @param {Compiler.AST.CSS.RelativeSelector} relative_selector
* @param {Compiler.AST.CSS.Rule} rule * @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @param {Direction} direction
* @returns {boolean} * @returns {boolean}
*/ */
function relative_selector_might_apply_to_node(relative_selector, rule, element) { function relative_selector_might_apply_to_node(relative_selector, rule, element, direction) {
// Sort :has(...) selectors in one bucket and everything else into another // Sort :has(...) selectors in one bucket and everything else into another
const has_selectors = []; const has_selectors = [];
const other_selectors = []; const other_selectors = [];
@ -331,13 +345,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
// If we're called recursively from a :has(...) selector, we're on the way of checking if the other selectors match. // If we're called recursively from a :has(...) selector, we're on the way of checking if the other selectors match.
// In that case ignore this check (because we just came from this) to avoid an infinite loop. // In that case ignore this check (because we just came from this) to avoid an infinite loop.
if (has_selectors.length > 0) { if (has_selectors.length > 0) {
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
const child_elements = [];
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
const descendant_elements = [];
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
let sibling_elements; // do them lazy because it's rarely used and expensive to calculate
// If this is a :has inside a global selector, we gotta include the element itself, too, // If this is a :has inside a global selector, we gotta include the element itself, too,
// because the global selector might be for an element that's outside the component, // because the global selector might be for an element that's outside the component,
// e.g. :root:has(.scoped), :global(.foo):has(.scoped), or :root { &:has(.scoped) {} } // e.g. :root:has(.scoped), :global(.foo):has(.scoped), or :root { &:has(.scoped) {} }
@ -353,46 +360,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
) )
) )
); );
if (include_self) {
child_elements.push(element);
descendant_elements.push(element);
}
const seen = new Set();
/**
* @param {Compiler.AST.SvelteNode} node
* @param {{ is_child: boolean }} state
*/
function walk_children(node, state) {
walk(node, state, {
_(node, context) {
if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
descendant_elements.push(node);
if (context.state.is_child) {
child_elements.push(node);
context.state.is_child = false;
context.next();
context.state.is_child = true;
} else {
context.next();
}
} else if (node.type === 'RenderTag') {
for (const snippet of node.metadata.snippets) {
if (seen.has(snippet)) continue;
seen.add(snippet);
walk_children(snippet.body, context.state);
}
} else {
context.next();
}
}
});
}
walk_children(element.fragment, { is_child: true });
// :has(...) is special in that it means "look downwards in the CSS tree". Since our matching algorithm goes // :has(...) is special in that it means "look downwards in the CSS tree". Since our matching algorithm goes
// upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the // upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the
@ -403,37 +370,34 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
let matched = false; let matched = false;
for (const complex_selector of complex_selectors) { for (const complex_selector of complex_selectors) {
const selectors = truncate(complex_selector); const [first, ...rest] = truncate(complex_selector);
const left_most_combinator = selectors[0]?.combinator ?? descendant_combinator; // if it was just a :global(...)
// In .x:has(> y), we want to search for y, ignoring the left-most combinator if (!first) {
// (else it would try to walk further up and fail because there are no selectors left) complex_selector.metadata.used = true;
if (selectors.length > 0) { matched = true;
selectors[0] = { continue;
...selectors[0],
combinator: null
};
} }
const descendants = if (include_self) {
left_most_combinator.name === '+' || left_most_combinator.name === '~' const selector_including_self = [
? (sibling_elements ??= get_following_sibling_elements(element, include_self)) first.combinator ? { ...first, combinator: null } : first,
: left_most_combinator.name === '>' ...rest
? child_elements ];
: descendant_elements; if (apply_selector(selector_including_self, rule, element, FORWARD)) {
let selector_matched = false;
// Iterate over all descendant elements and check if the selector inside :has matches
for (const element of descendants) {
if (
selectors.length === 0 /* is :global(...) */ ||
(element.metadata.scoped && selector_matched) ||
apply_selector(selectors, rule, element)
) {
complex_selector.metadata.used = true; complex_selector.metadata.used = true;
selector_matched = matched = true; matched = true;
} }
} }
const selector_excluding_self = [
any_selector,
first.combinator ? first : { ...first, combinator: descendant_combinator },
...rest
];
if (apply_selector(selector_excluding_self, rule, element, FORWARD)) {
complex_selector.metadata.used = true;
matched = true;
}
} }
if (!matched) { if (!matched) {
@ -458,7 +422,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
) { ) {
const args = selector.args; const args = selector.args;
const complex_selector = args.children[0]; const complex_selector = args.children[0];
return apply_selector(complex_selector.children, rule, element); return apply_selector(complex_selector.children, rule, element, BACKWARD);
} }
// We came across a :global, everything beyond it is global and therefore a potential match // We came across a :global, everything beyond it is global and therefore a potential match
@ -507,7 +471,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
if (is_global) { if (is_global) {
complex_selector.metadata.used = true; complex_selector.metadata.used = true;
matched = true; matched = true;
} else if (apply_selector(relative, rule, element)) { } else if (apply_selector(relative, rule, element, BACKWARD)) {
complex_selector.metadata.used = true; complex_selector.metadata.used = true;
matched = true; matched = true;
} else if (complex_selector.children.length > 1 && (name == 'is' || name == 'where')) { } else if (complex_selector.children.length > 1 && (name == 'is' || name == 'where')) {
@ -591,7 +555,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
for (const complex_selector of parent.prelude.children) { for (const complex_selector of parent.prelude.children) {
if ( if (
apply_selector(get_relative_selectors(complex_selector), parent, element) || apply_selector(get_relative_selectors(complex_selector), parent, element, direction) ||
complex_selector.children.every((s) => is_global(s, parent)) complex_selector.children.every((s) => is_global(s, parent))
) { ) {
complex_selector.metadata.used = true; complex_selector.metadata.used = true;
@ -612,80 +576,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
return true; return true;
} }
/**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @param {boolean} include_self
*/
function get_following_sibling_elements(element, include_self) {
const path = element.metadata.path;
let i = path.length;
/** @type {Compiler.AST.SvelteNode} */
let start = element;
let nodes = /** @type {Compiler.AST.SvelteNode[]} */ (
/** @type {Compiler.AST.Fragment} */ (path[0]).nodes
);
// find the set of nodes to walk...
while (i--) {
const node = path[i];
if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
nodes = node.fragment.nodes;
break;
}
if (node.type !== 'Fragment') {
start = node;
}
}
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
const siblings = [];
// ...then walk them, starting from the node containing the element in question
// skipping nodes that appears before the element
const seen = new Set();
let skip = true;
/** @param {Compiler.AST.SvelteNode} node */
function get_siblings(node) {
walk(node, null, {
RegularElement(node) {
if (node === element) {
skip = false;
if (include_self) siblings.push(node);
} else if (!skip) {
siblings.push(node);
}
},
SvelteElement(node) {
if (node === element) {
skip = false;
if (include_self) siblings.push(node);
} else if (!skip) {
siblings.push(node);
}
},
RenderTag(node) {
for (const snippet of node.metadata.snippets) {
if (seen.has(snippet)) continue;
seen.add(snippet);
get_siblings(snippet.body);
}
}
});
}
for (const node of nodes.slice(nodes.indexOf(start))) {
get_siblings(node);
}
return siblings;
}
/** /**
* @param {any} operator * @param {any} operator
* @param {any} expected_value * @param {any} expected_value
@ -822,6 +712,84 @@ function unquote(str) {
return str; return str;
} }
/**
* @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 {Set<Compiler.AST.SnippetBlock>} seen
*/
function get_ancestor_elements(node, adjacent_only, seen = new Set()) {
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
const ancestors = [];
const path = node.metadata.path;
let i = path.length;
while (i--) {
const parent = path[i];
if (parent.type === 'SnippetBlock') {
if (!seen.has(parent)) {
seen.add(parent);
for (const site of parent.metadata.sites) {
ancestors.push(...get_ancestor_elements(site, adjacent_only, seen));
}
}
break;
}
if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
ancestors.push(parent);
if (adjacent_only) {
break;
}
}
}
return ancestors;
}
/**
* @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 {Set<Compiler.AST.SnippetBlock>} seen
*/
function get_descendant_elements(node, adjacent_only, seen = new Set()) {
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
const descendants = [];
/**
* @param {Compiler.AST.SvelteNode} node
*/
function walk_children(node) {
walk(node, null, {
_(node, context) {
if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
descendants.push(node);
if (!adjacent_only) {
context.next();
}
} else if (node.type === 'RenderTag') {
for (const snippet of node.metadata.snippets) {
if (seen.has(snippet)) continue;
seen.add(snippet);
walk_children(snippet.body);
}
} else {
context.next();
}
}
});
}
walk_children(node.type === 'RenderTag' ? node : node.fragment);
return descendants;
}
/** /**
* @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
* @returns {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null} * @returns {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null}
@ -843,11 +811,12 @@ 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 {Direction} direction
* @param {boolean} adjacent_only * @param {boolean} adjacent_only
* @param {Set<Compiler.AST.SnippetBlock>} seen * @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, seen = new Set()) { function get_possible_element_siblings(node, direction, 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;
@ -859,9 +828,9 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
while (i--) { while (i--) {
const fragment = /** @type {Compiler.AST.Fragment} */ (path[i--]); const fragment = /** @type {Compiler.AST.Fragment} */ (path[i--]);
let j = fragment.nodes.indexOf(current); let j = fragment.nodes.indexOf(current) + (direction === FORWARD ? 1 : -1);
while (j--) { while (j >= 0 && j < fragment.nodes.length) {
const node = fragment.nodes[j]; const node = fragment.nodes[j];
if (node.type === 'RegularElement') { if (node.type === 'RegularElement') {
@ -876,21 +845,28 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
return result; return result;
} }
} }
// Special case: slots, render tags and svelte:element tags could resolve to no siblings,
// so we want to continue until we find a definite sibling even with the adjacent-only combinator
} else if (is_block(node)) { } else if (is_block(node)) {
if (node.type === 'SlotElement') { if (node.type === 'SlotElement') {
result.set(node, NODE_PROBABLY_EXISTS); result.set(node, NODE_PROBABLY_EXISTS);
} }
const possible_last_child = get_possible_last_child(node, adjacent_only); const possible_last_child = get_possible_nested_siblings(node, direction, adjacent_only);
add_to_map(possible_last_child, result); add_to_map(possible_last_child, result);
if (adjacent_only && has_definite_elements(possible_last_child)) { if (adjacent_only && has_definite_elements(possible_last_child)) {
return result; return result;
} }
} else if (node.type === 'RenderTag' || node.type === 'SvelteElement') { } else if (node.type === 'SvelteElement') {
result.set(node, NODE_PROBABLY_EXISTS); result.set(node, NODE_PROBABLY_EXISTS);
// Special case: slots, render tags and svelte:element tags could resolve to no siblings, } else if (node.type === 'RenderTag') {
// so we want to continue until we find a definite sibling even with the adjacent-only combinator result.set(node, NODE_PROBABLY_EXISTS);
for (const snippet of node.metadata.snippets) {
add_to_map(get_possible_nested_siblings(snippet, direction, adjacent_only), result);
}
} }
j = direction === FORWARD ? j + 1 : j - 1;
} }
current = path[i]; current = path[i];
@ -910,7 +886,7 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
seen.add(current); 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, seen); const siblings = get_possible_element_siblings(site, direction, 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)) {
@ -923,7 +899,7 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
if (current.type === 'EachBlock' && fragment === current.body) { if (current.type === 'EachBlock' && fragment === current.body) {
// `{#each ...}<a /><b />{/each}` — `<b>` can be previous sibling of `<a />` // `{#each ...}<a /><b />{/each}` — `<b>` can be previous sibling of `<a />`
add_to_map(get_possible_last_child(current, adjacent_only), result); add_to_map(get_possible_nested_siblings(current, direction, adjacent_only), result);
} }
} }
@ -931,11 +907,13 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
} }
/** /**
* @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement} node * @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement | Compiler.AST.SnippetBlock} node
* @param {Direction} direction
* @param {boolean} adjacent_only * @param {boolean} adjacent_only
* @param {Set<Compiler.AST.SnippetBlock>} seen
* @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement, NodeExistsValue>} * @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement, NodeExistsValue>}
*/ */
function get_possible_last_child(node, adjacent_only) { function get_possible_nested_siblings(node, direction, adjacent_only, seen = new Set()) {
/** @type {Array<Compiler.AST.Fragment | undefined | null>} */ /** @type {Array<Compiler.AST.Fragment | undefined | null>} */
let fragments = []; let fragments = [];
@ -956,12 +934,20 @@ function get_possible_last_child(node, adjacent_only) {
case 'SlotElement': case 'SlotElement':
fragments.push(node.fragment); fragments.push(node.fragment);
break; break;
case 'SnippetBlock':
if (seen.has(node)) {
return new Map();
}
seen.add(node);
fragments.push(node.body);
break;
} }
/** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement, NodeExistsValue>} NodeMap */ /** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement, NodeExistsValue>} NodeMap */
const result = new Map(); const result = new Map();
let exhaustive = node.type !== 'SlotElement'; let exhaustive = node.type !== 'SlotElement' && node.type !== 'SnippetBlock';
for (const fragment of fragments) { for (const fragment of fragments) {
if (fragment == null) { if (fragment == null) {
@ -969,7 +955,7 @@ function get_possible_last_child(node, adjacent_only) {
continue; continue;
} }
const map = loop_child(fragment.nodes, adjacent_only); const map = loop_child(fragment.nodes, direction, adjacent_only, seen);
exhaustive &&= has_definite_elements(map); exhaustive &&= has_definite_elements(map);
add_to_map(map, result); add_to_map(map, result);
@ -1012,27 +998,28 @@ function add_to_map(from, to) {
} }
/** /**
* @param {NodeExistsValue | undefined} exist1 * @param {NodeExistsValue} exist1
* @param {NodeExistsValue | undefined} exist2 * @param {NodeExistsValue | undefined} exist2
* @returns {NodeExistsValue} * @returns {NodeExistsValue}
*/ */
function higher_existence(exist1, exist2) { function higher_existence(exist1, exist2) {
// @ts-expect-error TODO figure out if this is a bug if (exist2 === undefined) return exist1;
if (exist1 === undefined || exist2 === undefined) return exist1 || exist2;
return exist1 > exist2 ? exist1 : exist2; return exist1 > exist2 ? exist1 : exist2;
} }
/** /**
* @param {Compiler.AST.SvelteNode[]} children * @param {Compiler.AST.SvelteNode[]} children
* @param {Direction} direction
* @param {boolean} adjacent_only * @param {boolean} adjacent_only
* @param {Set<Compiler.AST.SnippetBlock>} seen
*/ */
function loop_child(children, adjacent_only) { function loop_child(children, direction, adjacent_only, seen) {
/** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement, NodeExistsValue>} */ /** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement, NodeExistsValue>} */
const result = new Map(); const result = new Map();
let i = children.length; let i = direction === FORWARD ? 0 : children.length - 1;
while (i--) { while (i >= 0 && i < children.length) {
const child = children[i]; const child = children[i];
if (child.type === 'RegularElement') { if (child.type === 'RegularElement') {
@ -1042,13 +1029,19 @@ function loop_child(children, adjacent_only) {
} }
} else if (child.type === 'SvelteElement') { } else if (child.type === 'SvelteElement') {
result.set(child, NODE_PROBABLY_EXISTS); result.set(child, NODE_PROBABLY_EXISTS);
} else if (child.type === 'RenderTag') {
for (const snippet of child.metadata.snippets) {
add_to_map(get_possible_nested_siblings(snippet, direction, adjacent_only, seen), result);
}
} else if (is_block(child)) { } else if (is_block(child)) {
const child_result = get_possible_last_child(child, adjacent_only); const child_result = get_possible_nested_siblings(child, direction, adjacent_only, seen);
add_to_map(child_result, result); add_to_map(child_result, result);
if (adjacent_only && has_definite_elements(child_result)) { if (adjacent_only && has_definite_elements(child_result)) {
break; break;
} }
} }
i = direction === FORWARD ? i + 1 : i - 1;
} }
return result; return result;

@ -6,210 +6,238 @@ export default test({
code: 'css_unused_selector', code: 'css_unused_selector',
message: 'Unused CSS selector ".unused:has(y)"', message: 'Unused CSS selector ".unused:has(y)"',
start: { start: {
line: 33, line: 41,
column: 1, column: 1,
character: 330 character: 378
}, },
end: { end: {
line: 33, line: 41,
column: 15, column: 15,
character: 344 character: 392
} }
}, },
{ {
code: 'css_unused_selector', code: 'css_unused_selector',
message: 'Unused CSS selector ".unused:has(:global(y))"', message: 'Unused CSS selector ".unused:has(:global(y))"',
start: { start: {
line: 36, line: 44,
column: 1, column: 1,
character: 365 character: 413
}, },
end: { end: {
line: 36, line: 44,
column: 24, column: 24,
character: 388 character: 436
} }
}, },
{ {
code: 'css_unused_selector', code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(.unused)"', message: 'Unused CSS selector "x:has(.unused)"',
start: { start: {
line: 39, line: 47,
column: 1, column: 1,
character: 409 character: 457
}, },
end: { end: {
line: 39, line: 47,
column: 15, column: 15,
character: 423 character: 471
} }
}, },
{ {
code: 'css_unused_selector', code: 'css_unused_selector',
message: 'Unused CSS selector ":global(.foo):has(.unused)"', message: 'Unused CSS selector ":global(.foo):has(.unused)"',
start: { start: {
line: 42, line: 50,
column: 1, column: 1,
character: 444 character: 492
}, },
end: { end: {
line: 42, line: 50,
column: 27, column: 27,
character: 470 character: 518
} }
}, },
{ {
code: 'css_unused_selector', code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(y):has(.unused)"', message: 'Unused CSS selector "x:has(y):has(.unused)"',
start: { start: {
line: 52, line: 60,
column: 1, column: 1,
character: 578 character: 626
}, },
end: { end: {
line: 52, line: 60,
column: 22, column: 22,
character: 599 character: 647
} }
}, },
{ {
code: 'css_unused_selector', code: 'css_unused_selector',
message: 'Unused CSS selector ".unused"', message: 'Unused CSS selector ".unused"',
start: { start: {
line: 71, line: 79,
column: 2, column: 2,
character: 804 character: 852
}, },
end: { end: {
line: 71, line: 79,
column: 9, column: 9,
character: 811 character: 859
} }
}, },
{ {
code: 'css_unused_selector', code: 'css_unused_selector',
message: 'Unused CSS selector ".unused x:has(y)"', message: 'Unused CSS selector ".unused x:has(y)"',
start: { start: {
line: 87, line: 95,
column: 1, column: 1,
character: 958 character: 1006
}, },
end: { end: {
line: 87, line: 95,
column: 17, column: 17,
character: 974 character: 1022
} }
}, },
{ {
code: 'css_unused_selector', code: 'css_unused_selector',
message: 'Unused CSS selector ".unused:has(.unused)"', message: 'Unused CSS selector ".unused:has(.unused)"',
start: { start: {
line: 90, line: 98,
column: 1, column: 1,
character: 995 character: 1043
}, },
end: { end: {
line: 90, line: 98,
column: 21, column: 21,
character: 1015 character: 1063
} }
}, },
{ {
code: 'css_unused_selector', code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(> z)"', message: 'Unused CSS selector "x:has(> z)"',
start: { start: {
line: 100, line: 108,
column: 1, column: 1,
character: 1115 character: 1163
}, },
end: { end: {
line: 100, line: 108,
column: 11, column: 11,
character: 1125 character: 1173
} }
}, },
{ {
code: 'css_unused_selector', code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(> d)"', message: 'Unused CSS selector "x:has(> d)"',
start: { start: {
line: 103, line: 111,
column: 1, column: 1,
character: 1146 character: 1194
}, },
end: { end: {
line: 103, line: 111,
column: 11, column: 11,
character: 1156 character: 1204
} }
}, },
{ {
code: 'css_unused_selector', code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(~ y)"', message: 'Unused CSS selector "x:has(~ y)"',
start: { start: {
line: 123, line: 131,
column: 1, column: 1,
character: 1348 character: 1396
}, },
end: { end: {
line: 123, line: 131,
column: 11, column: 11,
character: 1358 character: 1406
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "d:has(+ f)"',
start: {
line: 141,
column: 1,
character: 1494
},
end: {
line: 141,
column: 11,
character: 1504
} }
}, },
{ {
code: 'css_unused_selector', code: 'css_unused_selector',
message: 'Unused CSS selector "f:has(~ d)"', message: 'Unused CSS selector "f:has(~ d)"',
start: { start: {
line: 133, line: 144,
column: 1, column: 1,
character: 1446 character: 1525
}, },
end: { end: {
line: 133, line: 144,
column: 11, column: 11,
character: 1456 character: 1535
} }
}, },
{ {
code: 'css_unused_selector', code: 'css_unused_selector',
message: 'Unused CSS selector ":has(.unused)"', message: 'Unused CSS selector ":has(.unused)"',
start: { start: {
line: 141, line: 152,
column: 2, column: 2,
character: 1529 character: 1608
}, },
end: { end: {
line: 141, line: 152,
column: 15, column: 15,
character: 1542 character: 1621
} }
}, },
{ {
code: 'css_unused_selector', code: 'css_unused_selector',
message: 'Unused CSS selector "&:has(.unused)"', message: 'Unused CSS selector "&:has(.unused)"',
start: { start: {
line: 147, line: 158,
column: 2, column: 2,
character: 1600 character: 1679
}, },
end: { end: {
line: 147, line: 158,
column: 16, column: 16,
character: 1614 character: 1693
} }
}, },
{ {
code: 'css_unused_selector', code: 'css_unused_selector',
message: 'Unused CSS selector ":global(.foo):has(.unused)"', message: 'Unused CSS selector ":global(.foo):has(.unused)"',
start: { start: {
line: 155, line: 166,
column: 1, column: 1,
character: 1684 character: 1763
}, },
end: { end: {
line: 155, line: 166,
column: 27, column: 27,
character: 1710 character: 1789
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "h:has(> h > i)"',
start: {
line: 173,
column: 1,
character: 1848
},
end: {
line: 173,
column: 15,
character: 1862
} }
} }
] ]

@ -118,6 +118,9 @@
d.svelte-xyz:has(~ f:where(.svelte-xyz)) { d.svelte-xyz:has(~ f:where(.svelte-xyz)) {
color: green; color: green;
} }
/* (unused) d:has(+ f) {
color: red;
}*/
/* (unused) f:has(~ d) { /* (unused) f:has(~ d) {
color: red; color: red;
}*/ }*/
@ -143,3 +146,13 @@
/* (unused) :global(.foo):has(.unused) { /* (unused) :global(.foo):has(.unused) {
color: red; color: red;
}*/ }*/
g.svelte-xyz:has(> h:where(.svelte-xyz) > i:where(.svelte-xyz)) {
color: green;
}
/* (unused) h:has(> h > i) {
color: red;
}*/
g.svelte-xyz:has(+ j:where(.svelte-xyz) > k:where(.svelte-xyz)) {
color: green;
}

@ -9,6 +9,14 @@
</y> </y>
</x> </x>
<c></c> <c></c>
<g>
<h>
<i></i>
</h>
</g>
<j>
<k></k>
</j>
<style> <style>
x:has(y) { x:has(y) {
@ -130,6 +138,9 @@
d:has(~ f) { d:has(~ f) {
color: green; color: green;
} }
d:has(+ f) {
color: red;
}
f:has(~ d) { f:has(~ d) {
color: red; color: red;
} }
@ -155,4 +166,14 @@
:global(.foo):has(.unused) { :global(.foo):has(.unused) {
color: red; color: red;
} }
g:has(> h > i) {
color: green;
}
h:has(> h > i) {
color: red;
}
g:has(+ j > k) {
color: green;
}
</style> </style>

@ -1,20 +1,5 @@
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
warnings: [ 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
}
}
]
}); });

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

@ -12,14 +12,22 @@
</div> </div>
{/snippet} {/snippet}
{#snippet c()}
<span></span>
{@render c()}
{/snippet}
<style> <style>
div div { div div {
color: green; color: green;
} }
div + div { div + div {
color: red; /* this is marked as unused, but only because we've written an infinite loop - worth fixing? */ color: green;
} }
div:has(div) { div:has(div) {
color: green; color: green;
} }
span:has(~span) {
color: green;
}
</style> </style>

Loading…
Cancel
Save