Skip to content

Commit 1eed645

Browse files
authored
fix: ensure explicit nesting selector is always applied (#14193)
Previously, we were applying an explicit nesting selector to the start of a relative selector chain only when starting the traversal. Prepending the selector is important because it ensures we traverse upwards to the parent rule when the current selectors all matched and there's still more to do. But we forgot to do the prepend for parent rules, which meant that if we were nested two levels deep, we would stop too early. This fix ensures we prepend in that case, too. Fixes #14178
1 parent d7caf08 commit 1eed645

File tree

10 files changed

+179
-54
lines changed

10 files changed

+179
-54
lines changed

.changeset/nice-numbers-remember.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: ensure explicit nesting selector is always applied

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

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -77,38 +77,9 @@ const visitors = {
7777
}
7878
},
7979
ComplexSelector(node, context) {
80-
const selectors = truncate(node);
80+
const selectors = get_relative_selectors(node);
8181
const inner = selectors[selectors.length - 1];
8282

83-
if (node.metadata.rule?.metadata.parent_rule && selectors.length > 0) {
84-
let has_explicit_nesting_selector = false;
85-
86-
// nesting could be inside pseudo classes like :is, :has or :where
87-
for (let selector of selectors) {
88-
walk(
89-
selector,
90-
{},
91-
{
92-
// @ts-ignore
93-
NestingSelector() {
94-
has_explicit_nesting_selector = true;
95-
}
96-
}
97-
);
98-
// if we found one we can break from the others
99-
if (has_explicit_nesting_selector) break;
100-
}
101-
102-
if (!has_explicit_nesting_selector) {
103-
selectors[0] = {
104-
...selectors[0],
105-
combinator: descendant_combinator
106-
};
107-
108-
selectors.unshift(nesting_selector);
109-
}
110-
}
111-
11283
if (context.state.from_render_tag) {
11384
// We're searching for a match that crosses a render tag boundary. That means we have to both traverse up
11485
// the element tree (to see if we find an entry point) but also remove selectors from the end (assuming
@@ -156,6 +127,50 @@ const visitors = {
156127
}
157128
};
158129

130+
/**
131+
* Retrieves the relative selectors (minus the trailing globals) from a complex selector.
132+
* Also searches them for any existing `&` selectors and adds one if none are found.
133+
* This ensures we traverse up to the parent rule when the inner selectors match and we're
134+
* trying to see if the parent rule also matches.
135+
* @param {Compiler.Css.ComplexSelector} node
136+
*/
137+
function get_relative_selectors(node) {
138+
const selectors = truncate(node);
139+
140+
if (node.metadata.rule?.metadata.parent_rule && selectors.length > 0) {
141+
let has_explicit_nesting_selector = false;
142+
143+
// nesting could be inside pseudo classes like :is, :has or :where
144+
for (let selector of selectors) {
145+
walk(
146+
selector,
147+
{},
148+
{
149+
// @ts-ignore
150+
NestingSelector() {
151+
has_explicit_nesting_selector = true;
152+
}
153+
}
154+
);
155+
// if we found one we can break from the others
156+
if (has_explicit_nesting_selector) break;
157+
}
158+
159+
if (!has_explicit_nesting_selector) {
160+
if (selectors[0].combinator === null) {
161+
selectors[0] = {
162+
...selectors[0],
163+
combinator: descendant_combinator
164+
};
165+
}
166+
167+
selectors.unshift(nesting_selector);
168+
}
169+
}
170+
171+
return selectors;
172+
}
173+
159174
/**
160175
* Discard trailing `:global(...)` selectors without a `:has/is/where/not(...)` modifier, these are unused for scoping purposes
161176
* @param {Compiler.Css.ComplexSelector} node
@@ -641,7 +656,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
641656
const parent = /** @type {Compiler.Css.Rule} */ (rule.metadata.parent_rule);
642657

643658
for (const complex_selector of parent.prelude.children) {
644-
if (apply_selector(truncate(complex_selector), parent, element, state)) {
659+
if (apply_selector(get_relative_selectors(complex_selector), parent, element, state)) {
645660
complex_selector.metadata.used = true;
646661
matched = true;
647662
}

packages/svelte/tests/css/samples/nested-in-pseudo/_config.js

Lines changed: 0 additions & 5 deletions
This file was deleted.

packages/svelte/tests/css/samples/nested-in-pseudo/expected.css

Lines changed: 0 additions & 6 deletions
This file was deleted.

packages/svelte/tests/css/samples/nested-in-pseudo/expected.html

Lines changed: 0 additions & 1 deletion
This file was deleted.

packages/svelte/tests/css/samples/nested-in-pseudo/input.svelte

Lines changed: 0 additions & 11 deletions
This file was deleted.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
warnings: [
5+
{
6+
code: 'css_unused_selector',
7+
message: 'Unused CSS selector ".unused:has(&)"',
8+
start: {
9+
line: 10,
10+
column: 2,
11+
character: 105
12+
},
13+
end: {
14+
line: 10,
15+
column: 16,
16+
character: 119
17+
}
18+
},
19+
{
20+
code: 'css_unused_selector',
21+
message: 'Unused CSS selector "&.unused"',
22+
start: {
23+
line: 23,
24+
column: 3,
25+
character: 223
26+
},
27+
end: {
28+
line: 23,
29+
column: 11,
30+
character: 231
31+
}
32+
},
33+
{
34+
code: 'css_unused_selector',
35+
message: 'Unused CSS selector "&.unused"',
36+
start: {
37+
line: 37,
38+
column: 3,
39+
character: 344
40+
},
41+
end: {
42+
line: 37,
43+
column: 11,
44+
character: 352
45+
}
46+
}
47+
]
48+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
2+
nav.svelte-xyz {
3+
header:where(.svelte-xyz):has(&){
4+
color: green;
5+
}
6+
/* (unused) .unused:has(&){
7+
color: red;
8+
}*/
9+
}
10+
11+
header.svelte-xyz {
12+
> nav:where(.svelte-xyz) {
13+
color: green;
14+
15+
&.active {
16+
color: green;
17+
}
18+
19+
/* (unused) &.unused {
20+
color: red;
21+
}*/
22+
}
23+
}
24+
25+
header.svelte-xyz {
26+
& > nav:where(.svelte-xyz) {
27+
color: green;
28+
29+
&.active {
30+
color: green;
31+
}
32+
33+
/* (unused) &.unused {
34+
color: red;
35+
}*/
36+
}
37+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<header class="svelte-xyz"><nav class="active svelte-xyz"></nav></header>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<header>
2+
<nav class="active"></nav>
3+
</header>
4+
5+
<style>
6+
nav {
7+
header:has(&){
8+
color: green;
9+
}
10+
.unused:has(&){
11+
color: red;
12+
}
13+
}
14+
15+
header {
16+
> nav {
17+
color: green;
18+
19+
&.active {
20+
color: green;
21+
}
22+
23+
&.unused {
24+
color: red;
25+
}
26+
}
27+
}
28+
29+
header {
30+
& > nav {
31+
color: green;
32+
33+
&.active {
34+
color: green;
35+
}
36+
37+
&.unused {
38+
color: red;
39+
}
40+
}
41+
}
42+
</style>

0 commit comments

Comments
 (0)