Skip to content

Commit d2b6159

Browse files
authored
chore: css unused selector warnings (#11098)
The character adjustments in the existing warnings are because we remove some tabs from empty lines when initializing the Svelte 5 repo; the warnings were just not checked at that time yet.
1 parent ed9bab9 commit d2b6159

File tree

31 files changed

+374
-107
lines changed

31 files changed

+374
-107
lines changed

packages/svelte/src/compiler/phases/1-parse/read/style.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ export default function read_style(parser, start, attributes) {
3636
content: {
3737
start: content_start,
3838
end: content_end,
39-
styles: parser.template.slice(content_start, content_end)
39+
styles: parser.template.slice(content_start, content_end),
40+
comment: null
4041
}
4142
};
4243
}

packages/svelte/src/compiler/phases/1-parse/state/element.js

+17-15
Original file line numberDiff line numberDiff line change
@@ -283,25 +283,26 @@ export default function tag(parser) {
283283

284284
if (is_top_level_script_or_style) {
285285
parser.eat('>', true);
286-
if (name === 'script') {
287-
const content = read_script(parser, start, element.attributes);
288286

289-
/** @type {import('#compiler').Comment | null} */
290-
let prev_comment = null;
291-
for (let i = current.fragment.nodes.length - 1; i >= 0; i--) {
292-
const node = current.fragment.nodes[i];
287+
/** @type {import('#compiler').Comment | null} */
288+
let prev_comment = null;
289+
for (let i = current.fragment.nodes.length - 1; i >= 0; i--) {
290+
const node = current.fragment.nodes[i];
293291

294-
if (i === current.fragment.nodes.length - 1 && node.end !== start) {
295-
break;
296-
}
292+
if (i === current.fragment.nodes.length - 1 && node.end !== start) {
293+
break;
294+
}
297295

298-
if (node.type === 'Comment') {
299-
prev_comment = node;
300-
break;
301-
} else if (node.type !== 'Text' || node.data.trim()) {
302-
break;
303-
}
296+
if (node.type === 'Comment') {
297+
prev_comment = node;
298+
break;
299+
} else if (node.type !== 'Text' || node.data.trim()) {
300+
break;
304301
}
302+
}
303+
304+
if (name === 'script') {
305+
const content = read_script(parser, start, element.attributes);
305306
if (prev_comment) {
306307
// We take advantage of the fact that the root will never have leadingComments set,
307308
// and set the previous comment to it so that the warning mechanism can later
@@ -318,6 +319,7 @@ export default function tag(parser) {
318319
}
319320
} else {
320321
const content = read_style(parser, start, element.attributes);
322+
content.content.comment = prev_comment;
321323

322324
if (current.css) error(start, 'duplicate-style-element');
323325
current.css = content;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { walk } from 'zimmerframe';
2+
import { warn } from '../../../warnings.js';
3+
import { is_keyframes_node } from '../../css.js';
4+
5+
/**
6+
* @param {import('#compiler').Css.StyleSheet} stylesheet
7+
* @param {import('../../types.js').RawWarning[]} warnings
8+
*/
9+
export function warn_unused(stylesheet, warnings) {
10+
walk(stylesheet, { warnings, stylesheet }, visitors);
11+
}
12+
13+
/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, { warnings: import('../../types.js').RawWarning[], stylesheet: import('#compiler').Css.StyleSheet }>} */
14+
const visitors = {
15+
Atrule(node, context) {
16+
if (!is_keyframes_node(node)) {
17+
context.next();
18+
}
19+
},
20+
PseudoClassSelector(node, context) {
21+
if (node.name === 'is' || node.name === 'where') {
22+
context.next();
23+
}
24+
},
25+
ComplexSelector(node, context) {
26+
if (!node.metadata.used) {
27+
const content = context.state.stylesheet.content;
28+
const text = content.styles.substring(node.start - content.start, node.end - content.start);
29+
warn(context.state.warnings, node, context.path, 'css-unused-selector', text);
30+
}
31+
32+
context.next();
33+
}
34+
};

packages/svelte/src/compiler/phases/2-analyze/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { should_proxy_or_freeze } from '../3-transform/client/utils.js';
2323
import { analyze_css } from './css/css-analyze.js';
2424
import { prune } from './css/css-prune.js';
2525
import { hash } from './utils.js';
26+
import { warn_unused } from './css/css-warn.js';
2627

2728
/**
2829
* @param {import('#compiler').Script | null} script
@@ -548,6 +549,7 @@ export function analyze_component(root, source, options) {
548549
for (const element of analysis.elements) {
549550
prune(analysis.css.ast, element);
550551
}
552+
warn_unused(analysis.css.ast, analysis.warnings);
551553

552554
outer: for (const element of analysis.elements) {
553555
if (element.metadata.scoped) {

packages/svelte/src/compiler/types/css.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { Comment } from '#compiler';
2+
13
export namespace Css {
24
export interface BaseNode {
35
start: number;
@@ -12,6 +14,8 @@ export namespace Css {
1214
start: number;
1315
end: number;
1416
styles: string;
17+
/** Possible comment atop the style tag */
18+
comment: Comment | null;
1519
};
1620
}
1721

packages/svelte/src/compiler/warnings.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77

88
/** @satisfies {Warnings} */
99
const css = {
10-
'unused-selector': () => 'Unused CSS selector'
10+
/** @param {string} name */
11+
'css-unused-selector': (name) => `Unused CSS selector "${name}"`
1112
};
1213

1314
/** @satisfies {Warnings} */
@@ -300,6 +301,11 @@ export function warn(array, node, path, code, ...args) {
300301
)
301302
);
302303
}
304+
305+
// Style nodes
306+
if (current.type === 'StyleSheet' && current.content.comment) {
307+
ignores.push(...current.content.comment.ignores);
308+
}
303309
}
304310

305311
if (ignores.includes(code)) return;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
warnings: [
5+
{
6+
code: 'css-unused-selector',
7+
end: {
8+
character: 44,
9+
column: 14,
10+
line: 4
11+
},
12+
message: 'Unused CSS selector "p[type=\'B\' s]"',
13+
start: {
14+
character: 31,
15+
column: 1,
16+
line: 4
17+
}
18+
}
19+
]
20+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
warnings: [
5+
{
6+
code: 'css-unused-selector',
7+
end: {
8+
character: 33,
9+
column: 6,
10+
line: 6
11+
},
12+
message: 'Unused CSS selector "x y z"',
13+
start: {
14+
character: 28,
15+
column: 1,
16+
line: 6
17+
}
18+
}
19+
]
20+
});

packages/svelte/tests/css/samples/general-siblings-combinator-each-else/_config.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ export default test({
55
{
66
code: 'css-unused-selector',
77
message: 'Unused CSS selector ".b ~ .c"',
8-
start: { character: 199, column: 1, line: 13 },
9-
end: { character: 206, column: 8, line: 13 }
8+
start: { character: 198, column: 1, line: 13 },
9+
end: { character: 205, column: 8, line: 13 }
1010
}
1111
]
1212
});

packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/_config.js

+14-7
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@ import { test } from '../../test';
22

33
export default test({
44
warnings: [
5-
// TODO
6-
// {
7-
// code: 'css-unused-selector',
8-
// message: 'Unused CSS selector ".a ~ .b"',
9-
// start: { character: 111, column: 1, line: 10 },
10-
// end: { character: 118, column: 8, line: 10 }
11-
// },
5+
{
6+
code: 'css-unused-selector',
7+
end: {
8+
character: 479,
9+
column: 19,
10+
line: 22
11+
},
12+
message: 'Unused CSS selector ":global(.x) + .bar"',
13+
start: {
14+
character: 461,
15+
column: 1,
16+
line: 22
17+
}
18+
}
1219
]
1320
});

packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/_config.js

+14-7
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@ import { test } from '../../test';
22

33
export default test({
44
warnings: [
5-
// TODO
6-
// {
7-
// code: 'css-unused-selector',
8-
// message: 'Unused CSS selector ".a ~ .b"',
9-
// start: { character: 111, column: 1, line: 10 },
10-
// end: { character: 118, column: 8, line: 10 }
11-
// },
5+
{
6+
code: 'css-unused-selector',
7+
end: {
8+
character: 472,
9+
column: 19,
10+
line: 22
11+
},
12+
message: 'Unused CSS selector ":global(.x) + .bar"',
13+
start: {
14+
character: 454,
15+
column: 1,
16+
line: 22
17+
}
18+
}
1219
]
1320
});

packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js

+12-12
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,38 @@ export default test({
55
{
66
code: 'css-unused-selector',
77
message: 'Unused CSS selector ".a ~ .b"',
8-
start: { character: 111, column: 1, line: 10 },
9-
end: { character: 118, column: 8, line: 10 }
8+
start: { character: 110, column: 1, line: 10 },
9+
end: { character: 117, column: 8, line: 10 }
1010
},
1111
{
1212
code: 'css-unused-selector',
1313
message: 'Unused CSS selector ".b ~ .c"',
14-
start: { character: 138, column: 1, line: 11 },
15-
end: { character: 145, column: 8, line: 11 }
14+
start: { character: 137, column: 1, line: 11 },
15+
end: { character: 144, column: 8, line: 11 }
1616
},
1717
{
1818
code: 'css-unused-selector',
1919
message: 'Unused CSS selector ".c ~ .f"',
20-
start: { character: 165, column: 1, line: 12 },
21-
end: { character: 172, column: 8, line: 12 }
20+
start: { character: 164, column: 1, line: 12 },
21+
end: { character: 171, column: 8, line: 12 }
2222
},
2323
{
2424
code: 'css-unused-selector',
2525
message: 'Unused CSS selector ".f ~ .g"',
26-
start: { character: 192, column: 1, line: 13 },
27-
end: { character: 199, column: 8, line: 13 }
26+
start: { character: 191, column: 1, line: 13 },
27+
end: { character: 198, column: 8, line: 13 }
2828
},
2929
{
3030
code: 'css-unused-selector',
3131
message: 'Unused CSS selector ".b ~ .f"',
32-
start: { character: 219, column: 1, line: 14 },
33-
end: { character: 226, column: 8, line: 14 }
32+
start: { character: 218, column: 1, line: 14 },
33+
end: { character: 225, column: 8, line: 14 }
3434
},
3535
{
3636
code: 'css-unused-selector',
3737
message: 'Unused CSS selector ".b ~ .g"',
38-
start: { character: 246, column: 1, line: 15 },
39-
end: { character: 253, column: 8, line: 15 }
38+
start: { character: 245, column: 1, line: 15 },
39+
end: { character: 252, column: 8, line: 15 }
4040
}
4141
]
4242
});

packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/_config.js

+14-7
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@ import { test } from '../../test';
22

33
export default test({
44
warnings: [
5-
// TODO
6-
// {
7-
// code: 'css-unused-selector',
8-
// message: 'Unused CSS selector ".a ~ .b"',
9-
// start: { character: 111, column: 1, line: 10 },
10-
// end: { character: 118, column: 8, line: 10 }
11-
// },
5+
{
6+
code: 'css-unused-selector',
7+
end: {
8+
character: 496,
9+
column: 10,
10+
line: 26
11+
},
12+
message: 'Unused CSS selector ".x + .bar"',
13+
start: {
14+
character: 487,
15+
column: 1,
16+
line: 26
17+
}
18+
}
1219
]
1320
});

packages/svelte/tests/css/samples/host/_config.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ export default test({
66
code: 'css-unused-selector',
77
message: 'Unused CSS selector ":host > span"',
88
start: {
9-
character: 147,
9+
character: 145,
1010
column: 1,
1111
line: 18
1212
},
1313
end: {
14-
character: 159,
14+
character: 157,
1515
column: 13,
1616
line: 18
1717
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
warnings: [
5+
{
6+
code: 'css-unused-selector',
7+
end: {
8+
character: 38,
9+
column: 11,
10+
line: 6
11+
},
12+
message: 'Unused CSS selector "z"',
13+
start: {
14+
character: 37,
15+
column: 10,
16+
line: 6
17+
}
18+
}
19+
]
20+
});

0 commit comments

Comments
 (0)