diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index ee5c8395f85d..ef64a979532b 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -36,7 +36,8 @@ export default function read_style(parser, start, attributes) { content: { start: content_start, end: content_end, - styles: parser.template.slice(content_start, content_end) + styles: parser.template.slice(content_start, content_end), + comment: null } }; } diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index 16960e9721e6..42c48a89ad74 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -283,25 +283,26 @@ export default function tag(parser) { if (is_top_level_script_or_style) { parser.eat('>', true); - if (name === 'script') { - const content = read_script(parser, start, element.attributes); - /** @type {import('#compiler').Comment | null} */ - let prev_comment = null; - for (let i = current.fragment.nodes.length - 1; i >= 0; i--) { - const node = current.fragment.nodes[i]; + /** @type {import('#compiler').Comment | null} */ + let prev_comment = null; + for (let i = current.fragment.nodes.length - 1; i >= 0; i--) { + const node = current.fragment.nodes[i]; - if (i === current.fragment.nodes.length - 1 && node.end !== start) { - break; - } + if (i === current.fragment.nodes.length - 1 && node.end !== start) { + break; + } - if (node.type === 'Comment') { - prev_comment = node; - break; - } else if (node.type !== 'Text' || node.data.trim()) { - break; - } + if (node.type === 'Comment') { + prev_comment = node; + break; + } else if (node.type !== 'Text' || node.data.trim()) { + break; } + } + + if (name === 'script') { + const content = read_script(parser, start, element.attributes); if (prev_comment) { // We take advantage of the fact that the root will never have leadingComments set, // and set the previous comment to it so that the warning mechanism can later @@ -318,6 +319,7 @@ export default function tag(parser) { } } else { const content = read_style(parser, start, element.attributes); + content.content.comment = prev_comment; if (current.css) error(start, 'duplicate-style-element'); current.css = content; diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js new file mode 100644 index 000000000000..8d7fef873f7d --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js @@ -0,0 +1,34 @@ +import { walk } from 'zimmerframe'; +import { warn } from '../../../warnings.js'; +import { is_keyframes_node } from '../../css.js'; + +/** + * @param {import('#compiler').Css.StyleSheet} stylesheet + * @param {import('../../types.js').RawWarning[]} warnings + */ +export function warn_unused(stylesheet, warnings) { + walk(stylesheet, { warnings, stylesheet }, visitors); +} + +/** @type {import('zimmerframe').Visitors} */ +const visitors = { + Atrule(node, context) { + if (!is_keyframes_node(node)) { + context.next(); + } + }, + PseudoClassSelector(node, context) { + if (node.name === 'is' || node.name === 'where') { + context.next(); + } + }, + ComplexSelector(node, context) { + if (!node.metadata.used) { + const content = context.state.stylesheet.content; + const text = content.styles.substring(node.start - content.start, node.end - content.start); + warn(context.state.warnings, node, context.path, 'css-unused-selector', text); + } + + context.next(); + } +}; diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 887c96173436..d9c6dfe64adc 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -23,6 +23,7 @@ import { should_proxy_or_freeze } from '../3-transform/client/utils.js'; import { analyze_css } from './css/css-analyze.js'; import { prune } from './css/css-prune.js'; import { hash } from './utils.js'; +import { warn_unused } from './css/css-warn.js'; /** * @param {import('#compiler').Script | null} script @@ -548,6 +549,7 @@ export function analyze_component(root, source, options) { for (const element of analysis.elements) { prune(analysis.css.ast, element); } + warn_unused(analysis.css.ast, analysis.warnings); outer: for (const element of analysis.elements) { if (element.metadata.scoped) { diff --git a/packages/svelte/src/compiler/types/css.d.ts b/packages/svelte/src/compiler/types/css.d.ts index efa33e9e5a1d..4bce9044256f 100644 --- a/packages/svelte/src/compiler/types/css.d.ts +++ b/packages/svelte/src/compiler/types/css.d.ts @@ -1,3 +1,5 @@ +import type { Comment } from '#compiler'; + export namespace Css { export interface BaseNode { start: number; @@ -12,6 +14,8 @@ export namespace Css { start: number; end: number; styles: string; + /** Possible comment atop the style tag */ + comment: Comment | null; }; } diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js index 7b863ef2b1ba..8f0e81d9c021 100644 --- a/packages/svelte/src/compiler/warnings.js +++ b/packages/svelte/src/compiler/warnings.js @@ -7,7 +7,8 @@ import { /** @satisfies {Warnings} */ const css = { - 'unused-selector': () => 'Unused CSS selector' + /** @param {string} name */ + 'css-unused-selector': (name) => `Unused CSS selector "${name}"` }; /** @satisfies {Warnings} */ @@ -300,6 +301,11 @@ export function warn(array, node, path, code, ...args) { ) ); } + + // Style nodes + if (current.type === 'StyleSheet' && current.content.comment) { + ignores.push(...current.content.comment.ignores); + } } if (ignores.includes(code)) return; diff --git a/packages/svelte/tests/css/samples/attribute-selector-case-sensitive/_config.js b/packages/svelte/tests/css/samples/attribute-selector-case-sensitive/_config.js new file mode 100644 index 000000000000..4be32cf3658a --- /dev/null +++ b/packages/svelte/tests/css/samples/attribute-selector-case-sensitive/_config.js @@ -0,0 +1,20 @@ +import { test } from '../../test'; + +export default test({ + warnings: [ + { + code: 'css-unused-selector', + end: { + character: 44, + column: 14, + line: 4 + }, + message: 'Unused CSS selector "p[type=\'B\' s]"', + start: { + character: 31, + column: 1, + line: 4 + } + } + ] +}); diff --git a/packages/svelte/tests/css/samples/descendant-selector-unmatched/_config.js b/packages/svelte/tests/css/samples/descendant-selector-unmatched/_config.js new file mode 100644 index 000000000000..509e5faec95f --- /dev/null +++ b/packages/svelte/tests/css/samples/descendant-selector-unmatched/_config.js @@ -0,0 +1,20 @@ +import { test } from '../../test'; + +export default test({ + warnings: [ + { + code: 'css-unused-selector', + end: { + character: 33, + column: 6, + line: 6 + }, + message: 'Unused CSS selector "x y z"', + start: { + character: 28, + column: 1, + line: 6 + } + } + ] +}); diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-each-else/_config.js b/packages/svelte/tests/css/samples/general-siblings-combinator-each-else/_config.js index c733bcfbdede..e0059c8eb54a 100644 --- a/packages/svelte/tests/css/samples/general-siblings-combinator-each-else/_config.js +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-each-else/_config.js @@ -5,8 +5,8 @@ export default test({ { code: 'css-unused-selector', message: 'Unused CSS selector ".b ~ .c"', - start: { character: 199, column: 1, line: 13 }, - end: { character: 206, column: 8, line: 13 } + start: { character: 198, column: 1, line: 13 }, + end: { character: 205, column: 8, line: 13 } } ] }); diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/_config.js b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/_config.js index 2143e5e57555..2038788f678a 100644 --- a/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/_config.js +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/_config.js @@ -2,12 +2,19 @@ import { test } from '../../test'; export default test({ warnings: [ - // TODO - // { - // code: 'css-unused-selector', - // message: 'Unused CSS selector ".a ~ .b"', - // start: { character: 111, column: 1, line: 10 }, - // end: { character: 118, column: 8, line: 10 } - // }, + { + code: 'css-unused-selector', + end: { + character: 479, + column: 19, + line: 22 + }, + message: 'Unused CSS selector ":global(.x) + .bar"', + start: { + character: 461, + column: 1, + line: 22 + } + } ] }); diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/_config.js b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/_config.js index 2143e5e57555..b595370622f4 100644 --- a/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/_config.js +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/_config.js @@ -2,12 +2,19 @@ import { test } from '../../test'; export default test({ warnings: [ - // TODO - // { - // code: 'css-unused-selector', - // message: 'Unused CSS selector ".a ~ .b"', - // start: { character: 111, column: 1, line: 10 }, - // end: { character: 118, column: 8, line: 10 } - // }, + { + code: 'css-unused-selector', + end: { + character: 472, + column: 19, + line: 22 + }, + message: 'Unused CSS selector ":global(.x) + .bar"', + start: { + character: 454, + column: 1, + line: 22 + } + } ] }); diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js index 8d9f01485611..1de4e6d06daf 100644 --- a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js @@ -5,38 +5,38 @@ export default test({ { code: 'css-unused-selector', message: 'Unused CSS selector ".a ~ .b"', - start: { character: 111, column: 1, line: 10 }, - end: { character: 118, column: 8, line: 10 } + start: { character: 110, column: 1, line: 10 }, + end: { character: 117, column: 8, line: 10 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".b ~ .c"', - start: { character: 138, column: 1, line: 11 }, - end: { character: 145, column: 8, line: 11 } + start: { character: 137, column: 1, line: 11 }, + end: { character: 144, column: 8, line: 11 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".c ~ .f"', - start: { character: 165, column: 1, line: 12 }, - end: { character: 172, column: 8, line: 12 } + start: { character: 164, column: 1, line: 12 }, + end: { character: 171, column: 8, line: 12 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".f ~ .g"', - start: { character: 192, column: 1, line: 13 }, - end: { character: 199, column: 8, line: 13 } + start: { character: 191, column: 1, line: 13 }, + end: { character: 198, column: 8, line: 13 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".b ~ .f"', - start: { character: 219, column: 1, line: 14 }, - end: { character: 226, column: 8, line: 14 } + start: { character: 218, column: 1, line: 14 }, + end: { character: 225, column: 8, line: 14 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".b ~ .g"', - start: { character: 246, column: 1, line: 15 }, - end: { character: 253, column: 8, line: 15 } + start: { character: 245, column: 1, line: 15 }, + end: { character: 252, column: 8, line: 15 } } ] }); diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/_config.js b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/_config.js index 2143e5e57555..d9b40d64627f 100644 --- a/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/_config.js +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/_config.js @@ -2,12 +2,19 @@ import { test } from '../../test'; export default test({ warnings: [ - // TODO - // { - // code: 'css-unused-selector', - // message: 'Unused CSS selector ".a ~ .b"', - // start: { character: 111, column: 1, line: 10 }, - // end: { character: 118, column: 8, line: 10 } - // }, + { + code: 'css-unused-selector', + end: { + character: 496, + column: 10, + line: 26 + }, + message: 'Unused CSS selector ".x + .bar"', + start: { + character: 487, + column: 1, + line: 26 + } + } ] }); diff --git a/packages/svelte/tests/css/samples/host/_config.js b/packages/svelte/tests/css/samples/host/_config.js index 26f4fd65842d..e5c8c0fe0ec6 100644 --- a/packages/svelte/tests/css/samples/host/_config.js +++ b/packages/svelte/tests/css/samples/host/_config.js @@ -6,12 +6,12 @@ export default test({ code: 'css-unused-selector', message: 'Unused CSS selector ":host > span"', start: { - character: 147, + character: 145, column: 1, line: 18 }, end: { - character: 159, + character: 157, column: 13, line: 18 } diff --git a/packages/svelte/tests/css/samples/is/_config.js b/packages/svelte/tests/css/samples/is/_config.js new file mode 100644 index 000000000000..4a3961aaef94 --- /dev/null +++ b/packages/svelte/tests/css/samples/is/_config.js @@ -0,0 +1,20 @@ +import { test } from '../../test'; + +export default test({ + warnings: [ + { + code: 'css-unused-selector', + end: { + character: 38, + column: 11, + line: 6 + }, + message: 'Unused CSS selector "z"', + start: { + character: 37, + column: 10, + line: 6 + } + } + ] +}); diff --git a/packages/svelte/tests/css/samples/nested-css/_config.js b/packages/svelte/tests/css/samples/nested-css/_config.js new file mode 100644 index 000000000000..e51fbe576c11 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css/_config.js @@ -0,0 +1,104 @@ +import { test } from '../../test'; + +export default test({ + warnings: [ + { + code: 'css-unused-selector', + end: { + character: 239, + column: 13, + line: 20 + }, + message: 'Unused CSS selector ".unused"', + start: { + character: 232, + column: 6, + line: 20 + } + }, + { + code: 'css-unused-selector', + end: { + character: 302, + column: 10, + line: 27 + }, + message: 'Unused CSS selector ".unused"', + start: { + character: 295, + column: 3, + line: 27 + } + }, + { + code: 'css-unused-selector', + end: { + character: 328, + column: 6, + line: 30 + }, + message: 'Unused CSS selector ".c"', + start: { + character: 326, + column: 4, + line: 30 + } + }, + { + code: 'css-unused-selector', + end: { + character: 381, + column: 10, + line: 37 + }, + message: 'Unused CSS selector ".unused"', + start: { + character: 374, + column: 3, + line: 37 + } + }, + { + code: 'css-unused-selector', + end: { + character: 471, + column: 7, + line: 47 + }, + message: 'Unused CSS selector "& &"', + start: { + character: 468, + column: 4, + line: 47 + } + }, + { + code: 'css-unused-selector', + end: { + character: 634, + column: 5, + line: 66 + }, + message: 'Unused CSS selector "&.b"', + start: { + character: 631, + column: 2, + line: 66 + } + }, + { + code: 'css-unused-selector', + end: { + character: 666, + column: 9, + line: 70 + }, + message: 'Unused CSS selector ".unused"', + start: { + character: 659, + column: 2, + line: 70 + } + } + ] +}); diff --git a/packages/svelte/tests/css/samples/siblings-combinator-each-else-nested/_config.js b/packages/svelte/tests/css/samples/siblings-combinator-each-else-nested/_config.js index 47a1c2e25341..b7f24a88810c 100644 --- a/packages/svelte/tests/css/samples/siblings-combinator-each-else-nested/_config.js +++ b/packages/svelte/tests/css/samples/siblings-combinator-each-else-nested/_config.js @@ -5,62 +5,62 @@ export default test({ { code: 'css-unused-selector', message: 'Unused CSS selector ".a + .c"', - start: { character: 479, column: 1, line: 23 }, - end: { character: 486, column: 8, line: 23 } + start: { character: 478, column: 1, line: 23 }, + end: { character: 485, column: 8, line: 23 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".a + .g"', - start: { character: 506, column: 1, line: 24 }, - end: { character: 513, column: 8, line: 24 } + start: { character: 505, column: 1, line: 24 }, + end: { character: 512, column: 8, line: 24 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".b + .e"', - start: { character: 533, column: 1, line: 25 }, - end: { character: 540, column: 8, line: 25 } + start: { character: 532, column: 1, line: 25 }, + end: { character: 539, column: 8, line: 25 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".c + .g"', - start: { character: 560, column: 1, line: 26 }, - end: { character: 567, column: 8, line: 26 } + start: { character: 559, column: 1, line: 26 }, + end: { character: 566, column: 8, line: 26 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".c + .k"', - start: { character: 587, column: 1, line: 27 }, - end: { character: 594, column: 8, line: 27 } + start: { character: 586, column: 1, line: 27 }, + end: { character: 593, column: 8, line: 27 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".d + .d"', - start: { character: 614, column: 1, line: 28 }, - end: { character: 621, column: 8, line: 28 } + start: { character: 613, column: 1, line: 28 }, + end: { character: 620, column: 8, line: 28 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".e + .f"', - start: { character: 641, column: 1, line: 29 }, - end: { character: 648, column: 8, line: 29 } + start: { character: 640, column: 1, line: 29 }, + end: { character: 647, column: 8, line: 29 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".f + .f"', - start: { character: 668, column: 1, line: 30 }, - end: { character: 675, column: 8, line: 30 } + start: { character: 667, column: 1, line: 30 }, + end: { character: 674, column: 8, line: 30 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".g + .j"', - start: { character: 695, column: 1, line: 31 }, - end: { character: 702, column: 8, line: 31 } + start: { character: 694, column: 1, line: 31 }, + end: { character: 701, column: 8, line: 31 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".g + .h + .i + .j"', - start: { character: 722, column: 1, line: 32 }, - end: { character: 739, column: 18, line: 32 } + start: { character: 721, column: 1, line: 32 }, + end: { character: 738, column: 18, line: 32 } } ] }); diff --git a/packages/svelte/tests/css/samples/siblings-combinator-each-else/_config.js b/packages/svelte/tests/css/samples/siblings-combinator-each-else/_config.js index e0b5ab155725..0014358f74a6 100644 --- a/packages/svelte/tests/css/samples/siblings-combinator-each-else/_config.js +++ b/packages/svelte/tests/css/samples/siblings-combinator-each-else/_config.js @@ -5,14 +5,14 @@ export default test({ { code: 'css-unused-selector', message: 'Unused CSS selector ".a + .d"', - start: { character: 172, column: 1, line: 12 }, - end: { character: 179, column: 8, line: 12 } + start: { character: 171, column: 1, line: 12 }, + end: { character: 178, column: 8, line: 12 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".b + .c"', - start: { character: 199, column: 1, line: 13 }, - end: { character: 206, column: 8, line: 13 } + start: { character: 198, column: 1, line: 13 }, + end: { character: 205, column: 8, line: 13 } } ] }); diff --git a/packages/svelte/tests/css/samples/siblings-combinator-slot/_config.js b/packages/svelte/tests/css/samples/siblings-combinator-slot/_config.js index 4d4d48693432..b1c2369ab7a7 100644 --- a/packages/svelte/tests/css/samples/siblings-combinator-slot/_config.js +++ b/packages/svelte/tests/css/samples/siblings-combinator-slot/_config.js @@ -5,20 +5,20 @@ export default test({ { code: 'css-unused-selector', message: 'Unused CSS selector ".a + .b"', - start: { character: 84, column: 1, line: 9 }, - end: { character: 91, column: 8, line: 9 } + start: { character: 83, column: 1, line: 9 }, + end: { character: 90, column: 8, line: 9 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".b + .c"', - start: { character: 111, column: 1, line: 10 }, - end: { character: 118, column: 8, line: 10 } + start: { character: 110, column: 1, line: 10 }, + end: { character: 117, column: 8, line: 10 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".c + .f"', - start: { character: 138, column: 1, line: 11 }, - end: { character: 145, column: 8, line: 11 } + start: { character: 137, column: 1, line: 11 }, + end: { character: 144, column: 8, line: 11 } } ] }); diff --git a/packages/svelte/tests/css/samples/unused-selector-child-combinator/_config.js b/packages/svelte/tests/css/samples/unused-selector-child-combinator/_config.js index 5b50fe9c923e..cc9927ae0f12 100644 --- a/packages/svelte/tests/css/samples/unused-selector-child-combinator/_config.js +++ b/packages/svelte/tests/css/samples/unused-selector-child-combinator/_config.js @@ -5,20 +5,20 @@ export default test({ { code: 'css-unused-selector', message: 'Unused CSS selector "article > *"', - start: { character: 10, column: 1, line: 2 }, - end: { character: 21, column: 12, line: 2 } + start: { character: 9, column: 1, line: 2 }, + end: { character: 20, column: 12, line: 2 } }, { code: 'css-unused-selector', message: 'Unused CSS selector "article *"', - start: { character: 49, column: 1, line: 6 }, - end: { character: 58, column: 10, line: 6 } + start: { character: 47, column: 1, line: 6 }, + end: { character: 56, column: 10, line: 6 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".article > *"', - start: { character: 86, column: 1, line: 10 }, - end: { character: 98, column: 13, line: 10 } + start: { character: 83, column: 1, line: 10 }, + end: { character: 95, column: 13, line: 10 } } ] }); diff --git a/packages/svelte/tests/css/samples/unused-selector-child-combinator/expected.css b/packages/svelte/tests/css/samples/unused-selector-child-combinator/expected.css index ad93ca631a24..fbe005adc9fa 100644 --- a/packages/svelte/tests/css/samples/unused-selector-child-combinator/expected.css +++ b/packages/svelte/tests/css/samples/unused-selector-child-combinator/expected.css @@ -1,6 +1,6 @@ /* (unused) article > * { font-size: 36px; - }*/ + }*/ /* (unused) article * { font-size: 36px; diff --git a/packages/svelte/tests/css/samples/unused-selector-child-combinator/input.svelte b/packages/svelte/tests/css/samples/unused-selector-child-combinator/input.svelte index 51463bbc9b1e..b0dc86133cf6 100644 --- a/packages/svelte/tests/css/samples/unused-selector-child-combinator/input.svelte +++ b/packages/svelte/tests/css/samples/unused-selector-child-combinator/input.svelte @@ -1,7 +1,7 @@