From 8e9a21e374c3bafb79e046e64ef4d625f466749f Mon Sep 17 00:00:00 2001 From: 7nik Date: Fri, 14 Mar 2025 23:44:28 +0200 Subject: [PATCH] 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. --- .changeset/cuddly-chefs-refuse.md | 5 + .../phases/2-analyze/css/css-prune.js | 427 +++++++++--------- .../svelte/tests/css/samples/has/_config.js | 148 +++--- .../svelte/tests/css/samples/has/expected.css | 13 + .../svelte/tests/css/samples/has/input.svelte | 21 + .../css/samples/render-tag-loop/_config.js | 17 +- .../css/samples/render-tag-loop/expected.css | 9 +- .../css/samples/render-tag-loop/input.svelte | 10 +- 8 files changed, 353 insertions(+), 297 deletions(-) create mode 100644 .changeset/cuddly-chefs-refuse.md diff --git a/.changeset/cuddly-chefs-refuse.md b/.changeset/cuddly-chefs-refuse.md new file mode 100644 index 0000000000..6672ac4ab3 --- /dev/null +++ b/.changeset/cuddly-chefs-refuse.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly match `:has()` selector during css pruning diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index fc8108e46e..070ec7cd34 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -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'; /** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */ +/** @typedef {FORWARD | BACKWARD} Direction */ const NODE_PROBABLY_EXISTS = 0; const NODE_DEFINITELY_EXISTS = 1; +const FORWARD = 0; +const BACKWARD = 1; const whitelist_attribute_selector = new Map([ ['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) * @type {Set} @@ -72,7 +96,8 @@ export function prune(stylesheet, element) { apply_selector( selectors, /** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule), - element + element, + BACKWARD ) ) { node.metadata.used = true; @@ -159,16 +184,17 @@ function truncate(node) { * @param {Compiler.AST.CSS.RelativeSelector[]} relative_selectors * @param {Compiler.AST.CSS.Rule} rule * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element + * @param {Direction} direction * @returns {boolean} */ -function apply_selector(relative_selectors, rule, element) { - const parent_selectors = relative_selectors.slice(); - const relative_selector = parent_selectors.pop(); +function apply_selector(relative_selectors, rule, element, direction) { + const rest_selectors = relative_selectors.slice(); + const relative_selector = direction === FORWARD ? rest_selectors.shift() : rest_selectors.pop(); const matched = !!relative_selector && - relative_selector_might_apply_to_node(relative_selector, rule, element) && - apply_combinator(relative_selector, parent_selectors, rule, element); + relative_selector_might_apply_to_node(relative_selector, rule, element, direction) && + apply_combinator(relative_selector, rest_selectors, rule, element, direction); if (matched) { 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[]} parent_selectors + * @param {Compiler.AST.CSS.RelativeSelector[]} rest_selectors * @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 {Direction} direction * @returns {boolean} */ -function apply_combinator(relative_selector, parent_selectors, rule, node) { - if (!relative_selector.combinator) return true; +function apply_combinator(relative_selector, rest_selectors, rule, node, direction) { + const combinator = + direction == FORWARD ? rest_selectors[0]?.combinator : relative_selector.combinator; + if (!combinator) return true; - const name = relative_selector.combinator.name; - - switch (name) { + switch (combinator.name) { 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; - const path = node.metadata.path; - let i = path.length; - - 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; + for (const parent of parents) { + if (apply_selector(rest_selectors, rule, parent, direction)) { + parent_matched = true; } } - 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 '~': { - const siblings = get_possible_element_siblings(node, name === '+'); + const siblings = get_possible_element_siblings(node, direction, combinator.name === '+'); let sibling_matched = false; for (const possible_sibling of siblings.keys()) { if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') { // `{@render foo()}

foo

` 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; } - } else if (apply_selector(parent_selectors, rule, possible_sibling)) { + } else if (apply_selector(rest_selectors, rule, possible_sibling, direction)) { sibling_matched = true; } } return ( sibling_matched || - (get_element_parent(node) === null && - parent_selectors.every((selector) => is_global(selector, rule))) + (direction === BACKWARD && + 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.Rule} rule * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element + * @param {Direction} direction * @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 const has_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. // In that case ignore this check (because we just came from this) to avoid an infinite loop. if (has_selectors.length > 0) { - /** @type {Array} */ - const child_elements = []; - /** @type {Array} */ - const descendant_elements = []; - /** @type {Array} */ - 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, // 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) {} } @@ -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 // 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; for (const complex_selector of complex_selectors) { - const selectors = truncate(complex_selector); - const left_most_combinator = selectors[0]?.combinator ?? descendant_combinator; - // In .x:has(> y), we want to search for y, ignoring the left-most combinator - // (else it would try to walk further up and fail because there are no selectors left) - if (selectors.length > 0) { - selectors[0] = { - ...selectors[0], - combinator: null - }; + const [first, ...rest] = truncate(complex_selector); + // if it was just a :global(...) + if (!first) { + complex_selector.metadata.used = true; + matched = true; + continue; } - const descendants = - left_most_combinator.name === '+' || left_most_combinator.name === '~' - ? (sibling_elements ??= get_following_sibling_elements(element, include_self)) - : left_most_combinator.name === '>' - ? child_elements - : descendant_elements; - - 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) - ) { + if (include_self) { + const selector_including_self = [ + first.combinator ? { ...first, combinator: null } : first, + ...rest + ]; + if (apply_selector(selector_including_self, rule, element, FORWARD)) { 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) { @@ -458,7 +422,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element) ) { const args = selector.args; 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 @@ -507,7 +471,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element) if (is_global) { complex_selector.metadata.used = true; matched = true; - } else if (apply_selector(relative, rule, element)) { + } else if (apply_selector(relative, rule, element, BACKWARD)) { complex_selector.metadata.used = true; matched = true; } 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) { 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.metadata.used = true; @@ -612,80 +576,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element) 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} */ - 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} expected_value @@ -822,6 +712,84 @@ function unquote(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} seen + */ +function get_ancestor_elements(node, adjacent_only, seen = new Set()) { + /** @type {Array} */ + 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} seen + */ +function get_descendant_elements(node, adjacent_only, seen = new Set()) { + /** @type {Array} */ + 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 * @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 {Direction} direction * @param {boolean} adjacent_only * @param {Set} seen * @returns {Map} */ -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} */ const result = new Map(); const path = node.metadata.path; @@ -859,9 +828,9 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) { while (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]; if (node.type === 'RegularElement') { @@ -876,21 +845,28 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) { 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)) { if (node.type === 'SlotElement') { 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); if (adjacent_only && has_definite_elements(possible_last_child)) { return result; } - } else if (node.type === 'RenderTag' || node.type === 'SvelteElement') { + } else if (node.type === 'SvelteElement') { result.set(node, NODE_PROBABLY_EXISTS); - // 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 (node.type === 'RenderTag') { + 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]; @@ -910,7 +886,7 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) { seen.add(current); 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); 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) { // `{#each ...}{/each}` — `` can be previous sibling of `` - 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 {Set} seen * @returns {Map} */ -function get_possible_last_child(node, adjacent_only) { +function get_possible_nested_siblings(node, direction, adjacent_only, seen = new Set()) { /** @type {Array} */ let fragments = []; @@ -956,12 +934,20 @@ function get_possible_last_child(node, adjacent_only) { case 'SlotElement': fragments.push(node.fragment); break; + + case 'SnippetBlock': + if (seen.has(node)) { + return new Map(); + } + seen.add(node); + fragments.push(node.body); + break; } /** @type {Map} NodeMap */ const result = new Map(); - let exhaustive = node.type !== 'SlotElement'; + let exhaustive = node.type !== 'SlotElement' && node.type !== 'SnippetBlock'; for (const fragment of fragments) { if (fragment == null) { @@ -969,7 +955,7 @@ function get_possible_last_child(node, adjacent_only) { continue; } - const map = loop_child(fragment.nodes, adjacent_only); + const map = loop_child(fragment.nodes, direction, adjacent_only, seen); exhaustive &&= has_definite_elements(map); 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 * @returns {NodeExistsValue} */ function higher_existence(exist1, exist2) { - // @ts-expect-error TODO figure out if this is a bug - if (exist1 === undefined || exist2 === undefined) return exist1 || exist2; + if (exist2 === undefined) return exist1; return exist1 > exist2 ? exist1 : exist2; } /** * @param {Compiler.AST.SvelteNode[]} children + * @param {Direction} direction * @param {boolean} adjacent_only + * @param {Set} seen */ -function loop_child(children, adjacent_only) { +function loop_child(children, direction, adjacent_only, seen) { /** @type {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]; if (child.type === 'RegularElement') { @@ -1042,13 +1029,19 @@ function loop_child(children, adjacent_only) { } } else if (child.type === 'SvelteElement') { 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)) { - 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); if (adjacent_only && has_definite_elements(child_result)) { break; } } + + i = direction === FORWARD ? i + 1 : i - 1; } return result; diff --git a/packages/svelte/tests/css/samples/has/_config.js b/packages/svelte/tests/css/samples/has/_config.js index 8d89d98cbd..5700a09b96 100644 --- a/packages/svelte/tests/css/samples/has/_config.js +++ b/packages/svelte/tests/css/samples/has/_config.js @@ -6,210 +6,238 @@ export default test({ code: 'css_unused_selector', message: 'Unused CSS selector ".unused:has(y)"', start: { - line: 33, + line: 41, column: 1, - character: 330 + character: 378 }, end: { - line: 33, + line: 41, column: 15, - character: 344 + character: 392 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ".unused:has(:global(y))"', start: { - line: 36, + line: 44, column: 1, - character: 365 + character: 413 }, end: { - line: 36, + line: 44, column: 24, - character: 388 + character: 436 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(.unused)"', start: { - line: 39, + line: 47, column: 1, - character: 409 + character: 457 }, end: { - line: 39, + line: 47, column: 15, - character: 423 + character: 471 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ":global(.foo):has(.unused)"', start: { - line: 42, + line: 50, column: 1, - character: 444 + character: 492 }, end: { - line: 42, + line: 50, column: 27, - character: 470 + character: 518 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(y):has(.unused)"', start: { - line: 52, + line: 60, column: 1, - character: 578 + character: 626 }, end: { - line: 52, + line: 60, column: 22, - character: 599 + character: 647 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ".unused"', start: { - line: 71, + line: 79, column: 2, - character: 804 + character: 852 }, end: { - line: 71, + line: 79, column: 9, - character: 811 + character: 859 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ".unused x:has(y)"', start: { - line: 87, + line: 95, column: 1, - character: 958 + character: 1006 }, end: { - line: 87, + line: 95, column: 17, - character: 974 + character: 1022 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ".unused:has(.unused)"', start: { - line: 90, + line: 98, column: 1, - character: 995 + character: 1043 }, end: { - line: 90, + line: 98, column: 21, - character: 1015 + character: 1063 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(> z)"', start: { - line: 100, + line: 108, column: 1, - character: 1115 + character: 1163 }, end: { - line: 100, + line: 108, column: 11, - character: 1125 + character: 1173 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(> d)"', start: { - line: 103, + line: 111, column: 1, - character: 1146 + character: 1194 }, end: { - line: 103, + line: 111, column: 11, - character: 1156 + character: 1204 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(~ y)"', start: { - line: 123, + line: 131, column: 1, - character: 1348 + character: 1396 }, end: { - line: 123, + line: 131, 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', message: 'Unused CSS selector "f:has(~ d)"', start: { - line: 133, + line: 144, column: 1, - character: 1446 + character: 1525 }, end: { - line: 133, + line: 144, column: 11, - character: 1456 + character: 1535 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ":has(.unused)"', start: { - line: 141, + line: 152, column: 2, - character: 1529 + character: 1608 }, end: { - line: 141, + line: 152, column: 15, - character: 1542 + character: 1621 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "&:has(.unused)"', start: { - line: 147, + line: 158, column: 2, - character: 1600 + character: 1679 }, end: { - line: 147, + line: 158, column: 16, - character: 1614 + character: 1693 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ":global(.foo):has(.unused)"', start: { - line: 155, + line: 166, column: 1, - character: 1684 + character: 1763 }, end: { - line: 155, + line: 166, 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 } } ] diff --git a/packages/svelte/tests/css/samples/has/expected.css b/packages/svelte/tests/css/samples/has/expected.css index b257370d61..2ce4d2bec5 100644 --- a/packages/svelte/tests/css/samples/has/expected.css +++ b/packages/svelte/tests/css/samples/has/expected.css @@ -118,6 +118,9 @@ d.svelte-xyz:has(~ f:where(.svelte-xyz)) { color: green; } + /* (unused) d:has(+ f) { + color: red; + }*/ /* (unused) f:has(~ d) { color: red; }*/ @@ -143,3 +146,13 @@ /* (unused) :global(.foo):has(.unused) { 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; + } \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/has/input.svelte b/packages/svelte/tests/css/samples/has/input.svelte index 9b254996bf..033471bc16 100644 --- a/packages/svelte/tests/css/samples/has/input.svelte +++ b/packages/svelte/tests/css/samples/has/input.svelte @@ -9,6 +9,14 @@ + + + + + + + + diff --git a/packages/svelte/tests/css/samples/render-tag-loop/_config.js b/packages/svelte/tests/css/samples/render-tag-loop/_config.js index f623b92cc3..292c6c49ac 100644 --- a/packages/svelte/tests/css/samples/render-tag-loop/_config.js +++ b/packages/svelte/tests/css/samples/render-tag-loop/_config.js @@ -1,20 +1,5 @@ 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 - } - } - ] + warnings: [] }); diff --git a/packages/svelte/tests/css/samples/render-tag-loop/expected.css b/packages/svelte/tests/css/samples/render-tag-loop/expected.css index 9ced15e964..3e449286c9 100644 --- a/packages/svelte/tests/css/samples/render-tag-loop/expected.css +++ b/packages/svelte/tests/css/samples/render-tag-loop/expected.css @@ -2,9 +2,12 @@ div.svelte-xyz div:where(.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 + div:where(.svelte-xyz) { + color: green; + } div.svelte-xyz:has(div:where(.svelte-xyz)) { color: green; } + span.svelte-xyz:has(~span:where(.svelte-xyz)) { + color: green; + } diff --git a/packages/svelte/tests/css/samples/render-tag-loop/input.svelte b/packages/svelte/tests/css/samples/render-tag-loop/input.svelte index ade8df5744..3c55261f18 100644 --- a/packages/svelte/tests/css/samples/render-tag-loop/input.svelte +++ b/packages/svelte/tests/css/samples/render-tag-loop/input.svelte @@ -12,14 +12,22 @@ {/snippet} +{#snippet c()} + + {@render c()} +{/snippet} +