Skip to content

Commit a1f3cbd

Browse files
committed
invert: when a not selector always matches by never matching, it's used, when it never matches by always matching, it's unused
1 parent 4af1858 commit a1f3cbd

File tree

11 files changed

+115
-67
lines changed

11 files changed

+115
-67
lines changed

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

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js';
1010
* stylesheet: Compiler.Css.StyleSheet;
1111
* element: Compiler.AST.RegularElement | Compiler.AST.SvelteElement;
1212
* from_render_tag: boolean;
13+
* inside_not: boolean;
1314
* }} State
1415
*/
1516
/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */
@@ -61,9 +62,13 @@ export function prune(stylesheet, element) {
6162
const parent = get_element_parent(element);
6263
if (!parent) return;
6364

64-
walk(stylesheet, { stylesheet, element: parent, from_render_tag: true }, visitors);
65+
walk(
66+
stylesheet,
67+
{ stylesheet, element: parent, from_render_tag: true, inside_not: false },
68+
visitors
69+
);
6570
} else {
66-
walk(stylesheet, { stylesheet, element, from_render_tag: false }, visitors);
71+
walk(stylesheet, { stylesheet, element, from_render_tag: false, inside_not: false }, visitors);
6772
}
6873
}
6974

@@ -127,7 +132,7 @@ const visitors = {
127132
selectors_to_check,
128133
/** @type {Compiler.Css.Rule} */ (node.metadata.rule),
129134
element,
130-
context.state.stylesheet
135+
context.state
131136
)
132137
) {
133138
mark(inner, element);
@@ -143,7 +148,7 @@ const visitors = {
143148
selectors,
144149
/** @type {Compiler.Css.Rule} */ (node.metadata.rule),
145150
context.state.element,
146-
context.state.stylesheet
151+
context.state
147152
)
148153
) {
149154
mark(inner, context.state.element);
@@ -188,10 +193,10 @@ function truncate(node) {
188193
* @param {Compiler.Css.RelativeSelector[]} relative_selectors
189194
* @param {Compiler.Css.Rule} rule
190195
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
191-
* @param {Compiler.Css.StyleSheet} stylesheet
196+
* @param {State} state
192197
* @returns {boolean}
193198
*/
194-
function apply_selector(relative_selectors, rule, element, stylesheet) {
199+
function apply_selector(relative_selectors, rule, element, state) {
195200
const parent_selectors = relative_selectors.slice();
196201
const relative_selector = parent_selectors.pop();
197202

@@ -201,7 +206,7 @@ function apply_selector(relative_selectors, rule, element, stylesheet) {
201206
relative_selector,
202207
rule,
203208
element,
204-
stylesheet
209+
state
205210
);
206211

207212
if (!possible_match) {
@@ -215,14 +220,14 @@ function apply_selector(relative_selectors, rule, element, stylesheet) {
215220
parent_selectors,
216221
rule,
217222
element,
218-
stylesheet
223+
state
219224
);
220225
}
221226

222227
// if this is the left-most non-global selector, mark it — we want
223228
// `x y z {...}` to become `x.blah y z.blah {...}`
224229
const parent = parent_selectors[parent_selectors.length - 1];
225-
if (!parent || is_global(parent, rule)) {
230+
if (!state.inside_not && (!parent || is_global(parent, rule))) {
226231
mark(relative_selector, element);
227232
}
228233

@@ -235,17 +240,10 @@ function apply_selector(relative_selectors, rule, element, stylesheet) {
235240
* @param {Compiler.Css.RelativeSelector[]} parent_selectors
236241
* @param {Compiler.Css.Rule} rule
237242
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
238-
* @param {Compiler.Css.StyleSheet} stylesheet
243+
* @param {State} state
239244
* @returns {boolean}
240245
*/
241-
function apply_combinator(
242-
combinator,
243-
relative_selector,
244-
parent_selectors,
245-
rule,
246-
element,
247-
stylesheet
248-
) {
246+
function apply_combinator(combinator, relative_selector, parent_selectors, rule, element, state) {
249247
const name = combinator.name;
250248

251249
switch (name) {
@@ -269,9 +267,9 @@ function apply_combinator(
269267
}
270268

271269
if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
272-
if (apply_selector(parent_selectors, rule, parent, stylesheet)) {
270+
if (apply_selector(parent_selectors, rule, parent, state)) {
273271
// TODO the `name === ' '` causes false positives, but removing it causes false negatives...
274-
if (name === ' ' || crossed_component_boundary) {
272+
if (!state.inside_not && (name === ' ' || crossed_component_boundary)) {
275273
mark(parent_selectors[parent_selectors.length - 1], parent);
276274
}
277275

@@ -297,11 +295,15 @@ function apply_combinator(
297295
if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') {
298296
// `{@render foo()}<p>foo</p>` with `:global(.x) + p` is a match
299297
if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) {
300-
mark(relative_selector, element);
298+
if (!state.inside_not) {
299+
mark(relative_selector, element);
300+
}
301301
sibling_matched = true;
302302
}
303-
} else if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) {
304-
mark(relative_selector, element);
303+
} else if (apply_selector(parent_selectors, rule, possible_sibling, state)) {
304+
if (!state.inside_not) {
305+
mark(relative_selector, element);
306+
}
305307
sibling_matched = true;
306308
}
307309
}
@@ -381,10 +383,10 @@ const regex_backslash_and_following_character = /\\(.)/g;
381383
* @param {Compiler.Css.RelativeSelector} relative_selector
382384
* @param {Compiler.Css.Rule} rule
383385
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
384-
* @param {Compiler.Css.StyleSheet} stylesheet
386+
* @param {State} state
385387
* @returns {boolean }
386388
*/
387-
function relative_selector_might_apply_to_node(relative_selector, rule, element, stylesheet) {
389+
function relative_selector_might_apply_to_node(relative_selector, rule, element, state) {
388390
// Sort :has(...) selectors in one bucket and everything else into another
389391
const has_selectors = [];
390392
const other_selectors = [];
@@ -458,7 +460,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
458460
if (
459461
selectors.length === 0 /* is :global(...) */ ||
460462
(element.metadata.scoped && selector_matched) ||
461-
apply_selector(selectors, rule, element, stylesheet)
463+
apply_selector(selectors, rule, element, state)
462464
) {
463465
complex_selector.metadata.used = true;
464466
selector_matched = matched = true;
@@ -504,7 +506,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
504506
) {
505507
const args = selector.args;
506508
const complex_selector = args.children[0];
507-
return apply_selector(complex_selector.children, rule, element, stylesheet);
509+
return apply_selector(complex_selector.children, rule, element, state);
508510
}
509511

510512
// We came across a :global, everything beyond it is global and therefore a potential match
@@ -517,15 +519,25 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
517519
const relative = truncate(complex_selector);
518520
const is_global = relative.length === 0;
519521

520-
if (is_global || apply_selector(relative, rule, element, stylesheet)) {
522+
if (is_global) {
523+
complex_selector.metadata.used = true;
524+
matched = true;
525+
} else if (name !== 'not' && apply_selector(relative, rule, element, state)) {
521526
complex_selector.metadata.used = true;
522527
matched = true;
523-
} else if (name === 'not') {
528+
} else if (
529+
name === 'not' &&
530+
!apply_selector(relative, rule, element, { ...state, inside_not: true })
531+
) {
524532
// For `:not(...)` we gotta do the inverse: If it did not match, mark the element and possibly
525533
// everything above (if the selector is written is a such) as scoped (because they matched by not matching).
526-
// The above if condition will ensure the selector itself will be marked as used if it doesn't match at least once,
527-
// and therefore having actual usefulness in the CSS output.
528534
element.metadata.scoped = true;
535+
complex_selector.metadata.used = true;
536+
matched = true;
537+
538+
for (const r of relative) {
539+
r.metadata.scoped = true;
540+
}
529541

530542
// bar:not(foo bar) means that foo is an ancestor of bar
531543
if (complex_selector.children.length > 1) {
@@ -633,7 +645,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
633645
const parent = /** @type {Compiler.Css.Rule} */ (rule.metadata.parent_rule);
634646

635647
for (const complex_selector of parent.prelude.children) {
636-
if (apply_selector(truncate(complex_selector), parent, element, stylesheet)) {
648+
if (apply_selector(truncate(complex_selector), parent, element, state)) {
637649
complex_selector.metadata.used = true;
638650
matched = true;
639651
}

packages/svelte/tests/css/samples/not-selector-global/_config.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ export default test({
44
warnings: [
55
{
66
code: 'css_unused_selector',
7-
message: 'Unused CSS selector ":global(.x) :not(.unused)"',
7+
message: 'Unused CSS selector ":global(.x) :not(p)"',
88
start: {
9-
line: 17,
9+
line: 14,
1010
column: 1,
11-
character: 289
11+
character: 197
1212
},
1313
end: {
14-
line: 17,
15-
column: 26,
16-
character: 314
14+
line: 14,
15+
column: 20,
16+
character: 216
1717
}
1818
}
1919
]

packages/svelte/tests/css/samples/not-selector-global/expected.css

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88
.x:not(.foo.svelte-xyz) {
99
color: green;
1010
}
11-
.x:not(.unused.svelte-xyz) {
12-
color: red; /* TODO would be nice to prune this one day */
13-
}
14-
/* (unused) :global(.x) :not(.unused) {
11+
/* (unused) :global(.x) :not(p) {
1512
color: red;
1613
}*/
14+
.x:not(p.svelte-xyz) {
15+
color: red; /* TODO would be nice to prune this one day */
16+
}
17+
.x .svelte-xyz:not(.unused:where(.svelte-xyz)) {
18+
color: green;
19+
}

packages/svelte/tests/css/samples/not-selector-global/input.svelte

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111
:global(.x):not(.foo) {
1212
color: green;
1313
}
14-
:global(.x):not(.unused) {
14+
:global(.x) :not(p) {
15+
color: red;
16+
}
17+
:global(.x):not(p) {
1518
color: red; /* TODO would be nice to prune this one day */
1619
}
1720
:global(.x) :not(.unused) {
18-
color: red;
21+
color: green;
1922
}
2023
</style>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
warnings: []
5+
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
.svelte-xyz:not(.bar:where(.svelte-xyz)) {
3+
color: green;
4+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<p class="foo svelte-xyz">foo</p> <p class="bar">bar</p>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<p class="foo">foo</p>
2+
<p class="bar">bar</p>
3+
4+
<style>
5+
:not(.bar) {
6+
color: green;
7+
}
8+
</style>

packages/svelte/tests/css/samples/not-selector/_config.js

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,44 @@ export default test({
44
warnings: [
55
{
66
code: 'css_unused_selector',
7-
message: 'Unused CSS selector ":not(.unused)"',
7+
message: 'Unused CSS selector ":not(p)"',
88
start: {
9-
line: 8,
9+
line: 11,
1010
column: 1,
11-
character: 89
11+
character: 125
1212
},
1313
end: {
14-
line: 8,
15-
column: 14,
16-
character: 102
14+
line: 11,
15+
column: 8,
16+
character: 132
1717
}
1818
},
1919
{
2020
code: 'css_unused_selector',
21-
message: 'Unused CSS selector ":not(.foo):not(.unused)"',
21+
message: 'Unused CSS selector "p :not(.foo)"',
2222
start: {
23-
line: 12,
23+
line: 22,
2424
column: 1,
25-
character: 124
25+
character: 235
2626
},
2727
end: {
28-
line: 12,
29-
column: 24,
30-
character: 147
28+
line: 22,
29+
column: 13,
30+
character: 247
3131
}
3232
},
3333
{
3434
code: 'css_unused_selector',
35-
message: 'Unused CSS selector "p :not(.foo)"',
35+
message: 'Unused CSS selector "p :not(.unused)"',
3636
start: {
37-
line: 19,
37+
line: 25,
3838
column: 1,
39-
character: 203
39+
character: 268
4040
},
4141
end: {
42-
line: 19,
43-
column: 13,
44-
character: 215
42+
line: 25,
43+
column: 16,
44+
character: 283
4545
}
4646
}
4747
]

packages/svelte/tests/css/samples/not-selector/expected.css

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@
22
.svelte-xyz:not(.foo:where(.svelte-xyz)) {
33
color: green;
44
}
5-
/* (unused) :not(.unused) {
5+
.svelte-xyz:not(.unused:where(.svelte-xyz)) {
6+
color: green;
7+
}
8+
/* (unused) :not(p) {
69
color: red;
710
}*/
811

9-
/* (unused) :not(.foo):not(.unused) {
10-
color: red;
11-
}*/
12+
.svelte-xyz:not(.foo:where(.svelte-xyz)):not(.unused:where(.svelte-xyz)) {
13+
color: green;
14+
}
1215

1316
p.svelte-xyz:not(.foo:where(.svelte-xyz)) {
1417
color: green;
1518
}
1619
/* (unused) p :not(.foo) {
1720
color: red;
1821
}*/
22+
/* (unused) p :not(.unused) {
23+
color: red;
24+
}*/

packages/svelte/tests/css/samples/not-selector/input.svelte

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@
66
color: green;
77
}
88
:not(.unused) {
9+
color: green;
10+
}
11+
:not(p) {
912
color: red;
1013
}
1114
1215
:not(.foo):not(.unused) {
13-
color: red;
16+
color: green;
1417
}
1518
1619
p:not(.foo) {
@@ -19,4 +22,7 @@
1922
p :not(.foo) {
2023
color: red;
2124
}
25+
p :not(.unused) {
26+
color: red;
27+
}
2228
</style>

0 commit comments

Comments
 (0)