diff --git a/.changeset/strong-feet-happen.md b/.changeset/strong-feet-happen.md new file mode 100644 index 000000000000..4d24ac07d4e1 --- /dev/null +++ b/.changeset/strong-feet-happen.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure `:has` selectors followed by other selectors match 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 f65d1c281dce..f5f7440085f8 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 @@ -127,8 +127,7 @@ const visitors = { selectors_to_check, /** @type {Compiler.Css.Rule} */ (node.metadata.rule), element, - context.state.stylesheet, - true + context.state.stylesheet ) ) { mark(inner, element); @@ -144,8 +143,7 @@ const visitors = { selectors, /** @type {Compiler.Css.Rule} */ (node.metadata.rule), context.state.element, - context.state.stylesheet, - true + context.state.stylesheet ) ) { mark(inner, context.state.element); @@ -191,10 +189,9 @@ function truncate(node) { * @param {Compiler.Css.Rule} rule * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element * @param {Compiler.Css.StyleSheet} stylesheet - * @param {boolean} check_has Whether or not to check the `:has(...)` selectors * @returns {boolean} */ -function apply_selector(relative_selectors, rule, element, stylesheet, check_has) { +function apply_selector(relative_selectors, rule, element, stylesheet) { const parent_selectors = relative_selectors.slice(); const relative_selector = parent_selectors.pop(); @@ -202,17 +199,11 @@ function apply_selector(relative_selectors, rule, element, stylesheet, check_has const possible_match = relative_selector_might_apply_to_node( relative_selector, - parent_selectors, rule, element, - stylesheet, - check_has + stylesheet ); - if (possible_match === 'definite_match') { - return true; - } - if (!possible_match) { return false; } @@ -224,8 +215,7 @@ function apply_selector(relative_selectors, rule, element, stylesheet, check_has parent_selectors, rule, element, - stylesheet, - check_has + stylesheet ); } @@ -246,7 +236,6 @@ function apply_selector(relative_selectors, rule, element, stylesheet, check_has * @param {Compiler.Css.Rule} rule * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element * @param {Compiler.Css.StyleSheet} stylesheet - * @param {boolean} check_has Whether or not to check the `:has(...)` selectors * @returns {boolean} */ function apply_combinator( @@ -255,8 +244,7 @@ function apply_combinator( parent_selectors, rule, element, - stylesheet, - check_has + stylesheet ) { const name = combinator.name; @@ -281,7 +269,7 @@ function apply_combinator( } if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') { - if (apply_selector(parent_selectors, rule, parent, stylesheet, check_has)) { + if (apply_selector(parent_selectors, rule, parent, stylesheet)) { // TODO the `name === ' '` causes false positives, but removing it causes false negatives... if (name === ' ' || crossed_component_boundary) { mark(parent_selectors[parent_selectors.length - 1], parent); @@ -312,9 +300,7 @@ function apply_combinator( mark(relative_selector, element); sibling_matched = true; } - } else if ( - apply_selector(parent_selectors, rule, possible_sibling, stylesheet, check_has) - ) { + } else if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) { mark(relative_selector, element); sibling_matched = true; } @@ -393,21 +379,12 @@ const regex_backslash_and_following_character = /\\(.)/g; * Ensure that `element` satisfies each simple selector in `relative_selector` * * @param {Compiler.Css.RelativeSelector} relative_selector - * @param {Compiler.Css.RelativeSelector[]} parent_selectors * @param {Compiler.Css.Rule} rule * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element * @param {Compiler.Css.StyleSheet} stylesheet - * @param {boolean} check_has Whether or not to check the `:has(...)` selectors - * @returns {boolean | 'definite_match'} + * @returns {boolean } */ -function relative_selector_might_apply_to_node( - relative_selector, - parent_selectors, - rule, - element, - stylesheet, - check_has -) { +function relative_selector_might_apply_to_node(relative_selector, rule, element, stylesheet) { // Sort :has(...) selectors in one bucket and everything else into another const has_selectors = []; const other_selectors = []; @@ -422,7 +399,26 @@ function relative_selector_might_apply_to_node( // 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 (check_has && has_selectors.length > 0) { + if (has_selectors.length > 0) { + /** @type {Array} */ + const child_elements = []; + /** @type {Array} */ + const descendant_elements = []; + + /** + * @param {Compiler.SvelteNode} node + * @param {boolean} is_recursing + */ + function collect_child_elements(node, is_recursing) { + if (node.type === 'RegularElement' || node.type === 'SvelteElement') { + descendant_elements.push(node); + if (!is_recursing) child_elements.push(node); + node.fragment.nodes.forEach((node) => collect_child_elements(node, true)); + } + } + + element.fragment.nodes.forEach((node) => collect_child_elements(node, false)); + // :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 // selector in a way that is similar to ancestor matching. In a sense, we're treating `.x:has(.y)` as `.x .y`. @@ -443,25 +439,20 @@ function relative_selector_might_apply_to_node( }; } - if ( - selectors.length === 0 /* is :global(...) */ || - apply_selector(selectors, rule, element, stylesheet, check_has) - ) { - // Treat e.g. `.x:has(.y)` as `.x .y` with the .y part already being matched, - // and now looking upwards for the .x part. + const descendants = + 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 ( - apply_combinator( - left_most_combinator, - selectors[0] ?? [], - [...parent_selectors, relative_selector], - rule, - element, - stylesheet, - false - ) + selectors.length === 0 /* is :global(...) */ || + (element.metadata.scoped && selector_matched) || + apply_selector(selectors, rule, element, stylesheet) ) { complex_selector.metadata.used = true; - matched = true; + selector_matched = matched = true; } } } @@ -484,9 +475,6 @@ function relative_selector_might_apply_to_node( return false; } } - - // We return this to signal the parent "don't bother checking the rest of the selectors, I already did that" - return 'definite_match'; } for (const selector of other_selectors) { @@ -507,7 +495,7 @@ function relative_selector_might_apply_to_node( ) { const args = selector.args; const complex_selector = args.children[0]; - return apply_selector(complex_selector.children, rule, element, stylesheet, check_has); + return apply_selector(complex_selector.children, rule, element, stylesheet); } // We came across a :global, everything beyond it is global and therefore a potential match @@ -520,7 +508,7 @@ function relative_selector_might_apply_to_node( const relative = truncate(complex_selector); if ( relative.length === 0 /* is :global(...) */ || - apply_selector(relative, rule, element, stylesheet, check_has) + apply_selector(relative, rule, element, stylesheet) ) { complex_selector.metadata.used = true; matched = true; @@ -621,7 +609,7 @@ function relative_selector_might_apply_to_node( const parent = /** @type {Compiler.Css.Rule} */ (rule.metadata.parent_rule); for (const complex_selector of parent.prelude.children) { - if (apply_selector(truncate(complex_selector), parent, element, stylesheet, check_has)) { + if (apply_selector(truncate(complex_selector), parent, element, stylesheet)) { complex_selector.metadata.used = true; matched = true; } diff --git a/packages/svelte/tests/css/samples/has/_config.js b/packages/svelte/tests/css/samples/has/_config.js index 68a1a35fe75c..815e48e0962d 100644 --- a/packages/svelte/tests/css/samples/has/_config.js +++ b/packages/svelte/tests/css/samples/has/_config.js @@ -6,112 +6,112 @@ export default test({ code: 'css_unused_selector', message: 'Unused CSS selector ".unused:has(y)"', start: { - character: 269, + line: 28, column: 1, - line: 27 + character: 277 }, end: { - character: 283, + line: 28, column: 15, - line: 27 + character: 291 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ".unused:has(:global(y))"', start: { - character: 304, + line: 31, column: 1, - line: 30 + character: 312 }, end: { - character: 327, + line: 31, column: 24, - line: 30 + character: 335 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(.unused)"', start: { - character: 348, + line: 34, column: 1, - line: 33 + character: 356 }, end: { - character: 362, + line: 34, column: 15, - line: 33 + character: 370 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(y):has(.unused)"', start: { - character: 517, + line: 47, column: 1, - line: 46 + character: 525 }, end: { - character: 538, + line: 47, column: 22, - line: 46 + character: 546 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ".unused"', start: { - character: 743, + line: 66, column: 2, - line: 65 + character: 751 }, end: { - character: 750, + line: 66, column: 9, - line: 65 + character: 758 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ".unused x:has(y)"', start: { - character: 897, + line: 82, column: 1, - line: 81 + character: 905 }, end: { - character: 913, + line: 82, column: 17, - line: 81 + character: 921 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ".unused:has(.unused)"', start: { - line: 84, + line: 85, column: 1, - character: 934 + character: 942 }, end: { - line: 84, + line: 85, column: 21, - character: 954 + character: 962 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(> z)"', start: { - line: 91, + line: 92, column: 1, - character: 1021 + character: 1029 }, end: { - line: 91, + line: 92, column: 11, - character: 1031 + character: 1039 } } ] diff --git a/packages/svelte/tests/css/samples/has/expected.css b/packages/svelte/tests/css/samples/has/expected.css index 8b01b2a74957..215f7d667e03 100644 --- a/packages/svelte/tests/css/samples/has/expected.css +++ b/packages/svelte/tests/css/samples/has/expected.css @@ -88,3 +88,10 @@ x.svelte-xyz > y:where(.svelte-xyz):has(z:where(.svelte-xyz)) { color: green; } + + x.svelte-xyz:has(y:where(.svelte-xyz)) z:where(.svelte-xyz) { + color: green; + } + x.svelte-xyz:has(y:where(.svelte-xyz)) + c:where(.svelte-xyz) { + color: green; + } diff --git a/packages/svelte/tests/css/samples/has/input.svelte b/packages/svelte/tests/css/samples/has/input.svelte index 2cbd06b95a08..7cbebe27ec19 100644 --- a/packages/svelte/tests/css/samples/has/input.svelte +++ b/packages/svelte/tests/css/samples/has/input.svelte @@ -3,6 +3,7 @@ +