From 8e31c4e7b6871967cdb8e59094a13ccec8eda8cd Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 8 Apr 2024 15:51:19 +0200 Subject: [PATCH 1/6] fix: better sibling selector handling Keep sibling selectors when dealing with slots/render tags/`svelte:element` tags fixes #9274 --- .changeset/giant-plants-grin.md | 5 ++ .../phases/2-analyze/css/css-prune.js | 70 ++++++++++++------- .../_config.js | 13 ++++ .../expected.css | 13 ++++ .../input.svelte | 23 ++++++ .../_config.js | 13 ++++ .../expected.css | 13 ++++ .../input.svelte | 23 ++++++ .../_config.js | 13 ++++ .../expected.css | 13 ++++ .../input.svelte | 27 +++++++ 11 files changed, 199 insertions(+), 27 deletions(-) create mode 100644 .changeset/giant-plants-grin.md create mode 100644 packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/_config.js create mode 100644 packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/expected.css create mode 100644 packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/input.svelte create mode 100644 packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/_config.js create mode 100644 packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/expected.css create mode 100644 packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/input.svelte create mode 100644 packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/_config.js create mode 100644 packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/expected.css create mode 100644 packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/input.svelte diff --git a/.changeset/giant-plants-grin.md b/.changeset/giant-plants-grin.md new file mode 100644 index 000000000000..4f5db1712ac8 --- /dev/null +++ b/.changeset/giant-plants-grin.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: keep sibling selectors when dealing with slots/render tags/`svelte:element` tags 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 c299612fd140..a5010c543a90 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 @@ -175,7 +175,13 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { let sibling_matched = false; for (const possible_sibling of siblings.keys()) { - if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) { + if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') { + // `{@render foo()}

foo

` with `:global(.x) + p` is a match + if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) { + mark(relative_selector, element); + sibling_matched = true; + } + } else if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) { mark(relative_selector, element); sibling_matched = true; } @@ -564,38 +570,39 @@ function get_element_parent(node) { function find_previous_sibling(node) { /** @type {import('#compiler').SvelteNode} */ let current_node = node; - do { - if (current_node.type === 'SlotElement') { - const slot_children = current_node.fragment.nodes; - if (slot_children.length > 0) { - current_node = slot_children.slice(-1)[0]; // go to its last child first - continue; - } - } - while ( - // @ts-expect-error TODO - !current_node.prev && - // @ts-expect-error TODO - current_node.parent && - // @ts-expect-error TODO - current_node.parent.type === 'SlotElement' - ) { - // @ts-expect-error TODO - current_node = current_node.parent; + + while ( + // @ts-expect-error TODO + !current_node.prev && + // @ts-expect-error TODO + current_node.parent?.type === 'SlotElement' + ) { + // @ts-expect-error TODO + current_node = current_node.parent; + } + + // @ts-expect-error + current_node = current_node.prev; + + while (current_node?.type === 'SlotElement') { + const slot_children = current_node.fragment.nodes; + if (slot_children.length > 0) { + current_node = slot_children.slice(-1)[0]; + } else { + break; } - // @ts-expect-error - current_node = current_node.prev; - } while (current_node && current_node.type === 'SlotElement'); + } + return current_node; } /** * @param {import('#compiler').SvelteNode} node * @param {boolean} adjacent_only - * @returns {Map} + * @returns {Map} */ function get_possible_element_siblings(node, adjacent_only) { - /** @type {Map} */ + /** @type {Map} */ const result = new Map(); /** @type {import('#compiler').SvelteNode} */ @@ -618,6 +625,14 @@ function get_possible_element_siblings(node, adjacent_only) { if (adjacent_only && has_definite_elements(possible_last_child)) { return result; } + } else if ( + prev.type === 'SlotElement' || + prev.type === 'RenderTag' || + prev.type === 'SvelteElement' + ) { + result.set(prev, NODE_PROBABLY_EXISTS); + // Special case: slots, render tags and svelte:element tags could resolve to no siblings, + // so we want to continue until we find a definite sibling even with the adjacent-only combinator } } @@ -720,7 +735,7 @@ function get_possible_last_child(relative_selector, adjacent_only) { } /** - * @param {Map} result + * @param {Map} result * @returns {boolean} */ function has_definite_elements(result) { @@ -734,8 +749,9 @@ function has_definite_elements(result) { } /** - * @param {Map} from - * @param {Map} to + * @template T + * @param {Map} from + * @param {Map} to * @returns {void} */ function add_to_map(from, to) { 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 new file mode 100644 index 000000000000..2143e5e57555 --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/_config.js @@ -0,0 +1,13 @@ +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 } + // }, + ] +}); diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/expected.css b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/expected.css new file mode 100644 index 000000000000..5495a803ef7c --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/expected.css @@ -0,0 +1,13 @@ + + .before.svelte-xyz + .foo:where(.svelte-xyz) { color: green; } + .before.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; } + .before.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; } + + .x + .foo.svelte-xyz { color: green; } + .x + .foo.svelte-xyz span:where(.svelte-xyz) { color: green; } + .x ~ .foo.svelte-xyz { color: green; } + .x ~ .foo.svelte-xyz span:where(.svelte-xyz) { color: green; } + .x ~ .bar.svelte-xyz { color: green; } + + /* no match */ + /* (unused) :global(.x) + .bar { color: green; }*/ diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/input.svelte b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/input.svelte new file mode 100644 index 000000000000..1e2c6fdc2dd6 --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/input.svelte @@ -0,0 +1,23 @@ +
+

before

+ {@render children()} +

+ foo +

+

bar

+
+ + 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 new file mode 100644 index 000000000000..2143e5e57555 --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/_config.js @@ -0,0 +1,13 @@ +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 } + // }, + ] +}); diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/expected.css b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/expected.css new file mode 100644 index 000000000000..5495a803ef7c --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/expected.css @@ -0,0 +1,13 @@ + + .before.svelte-xyz + .foo:where(.svelte-xyz) { color: green; } + .before.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; } + .before.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; } + + .x + .foo.svelte-xyz { color: green; } + .x + .foo.svelte-xyz span:where(.svelte-xyz) { color: green; } + .x ~ .foo.svelte-xyz { color: green; } + .x ~ .foo.svelte-xyz span:where(.svelte-xyz) { color: green; } + .x ~ .bar.svelte-xyz { color: green; } + + /* no match */ + /* (unused) :global(.x) + .bar { color: green; }*/ diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/input.svelte b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/input.svelte new file mode 100644 index 000000000000..d556ea6b8b11 --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/input.svelte @@ -0,0 +1,23 @@ +
+

before

+ +

+ foo +

+

bar

+
+ + 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 new file mode 100644 index 000000000000..2143e5e57555 --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/_config.js @@ -0,0 +1,13 @@ +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 } + // }, + ] +}); diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/expected.css b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/expected.css new file mode 100644 index 000000000000..830d3667024b --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/expected.css @@ -0,0 +1,13 @@ + + .before.svelte-xyz + .foo:where(.svelte-xyz) { color: green; } + .before.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; } + .before.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; } + + .x.svelte-xyz + .foo:where(.svelte-xyz) { color: green; } + .x.svelte-xyz + .foo:where(.svelte-xyz) span:where(.svelte-xyz) { color: green; } + .x.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; } + .x.svelte-xyz ~ .foo:where(.svelte-xyz) span:where(.svelte-xyz) { color: green; } + .x.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; } + + /* no match */ + /* (unused) .x + .bar { color: green; }*/ diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/input.svelte b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/input.svelte new file mode 100644 index 000000000000..1c51a2c516a1 --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/input.svelte @@ -0,0 +1,27 @@ + + +
+

before

+ +

+ foo +

+

bar

+
+ + From 116cdafdb90e7a984049798a8d78cc982627ac14 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 8 Apr 2024 17:50:48 +0200 Subject: [PATCH 2/6] chore: css unused selector warnings --- .../compiler/phases/2-analyze/css/css-warn.js | 30 +++++++++++++++++++ .../src/compiler/phases/2-analyze/index.js | 2 ++ packages/svelte/src/compiler/warnings.js | 3 +- .../_config.js | 20 +++++++++++++ .../descendant-selector-unmatched/_config.js | 20 +++++++++++++ .../_config.js | 21 ++++++++----- .../_config.js | 21 ++++++++----- packages/svelte/tests/css/test.ts | 30 ++++++++++--------- packages/svelte/tests/helpers.js | 6 +++- 9 files changed, 123 insertions(+), 30 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js create mode 100644 packages/svelte/tests/css/samples/attribute-selector-case-sensitive/_config.js create mode 100644 packages/svelte/tests/css/samples/descendant-selector-unmatched/_config.js 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..32dfab32abd3 --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js @@ -0,0 +1,30 @@ +import { walk } from 'zimmerframe'; +import { warn } from '../../../warnings.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 = { + ComplexSelector(node, context) { + if (!node.metadata.used) { + for (let i = context.path.length - 1; i >= 0; i--) { + const parent = context.path[i]; + if (parent.type === 'RelativeSelector' && parent.metadata.is_global) { + return; // no need to recurse; everything below is global, too + } + } + + 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/warnings.js b/packages/svelte/src/compiler/warnings.js index 7b863ef2b1ba..98dc07d7a019 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} */ 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-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..2038788f678a 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: 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/test.ts b/packages/svelte/tests/css/test.ts index 71b8e9644cc7..d17f7e9c6bb6 100644 --- a/packages/svelte/tests/css/test.ts +++ b/packages/svelte/tests/css/test.ts @@ -8,12 +8,17 @@ import { mount, unmount } from 'svelte'; import { suite, type BaseTest } from '../suite.js'; import type { CompileOptions, Warning } from '#compiler'; -// function normalize_warning(warning) { -// warning.frame = warning.frame.replace(/^\n/, '').replace(/^\t+/gm, '').replace(/\s+$/gm, ''); -// delete warning.filename; -// delete warning.toString; -// return warning; -// } +function normalize_warning(warning: Warning) { + delete warning.filename; + return warning; +} + +function load_warnings(path: string) { + if (!fs.existsSync(path)) { + return []; + } + return JSON.parse(fs.readFileSync(path, 'utf-8')).map(normalize_warning); +} interface CssTest extends BaseTest { compileOptions?: Partial; @@ -22,9 +27,6 @@ interface CssTest extends BaseTest { } const { test, run } = suite(async (config, cwd) => { - // TODO - // const expected_warnings = (config.warnings || []).map(normalize_warning); - await compile_directory(cwd, 'client', { cssHash: () => 'svelte-xyz', ...config.compileOptions }); await compile_directory(cwd, 'server', { cssHash: () => 'svelte-xyz', ...config.compileOptions }); @@ -33,11 +35,11 @@ const { test, run } = suite(async (config, cwd) => { assert.equal(dom_css, ssr_css); - // TODO reenable - // const dom_warnings = dom.warnings.map(normalize_warning); - // const ssr_warnings = ssr.warnings.map(normalize_warning); - // assert.deepEqual(dom_warnings, ssr_warnings); - // assert.deepEqual(dom_warnings.map(normalize_warning), expected_warnings); + const dom_warnings = load_warnings(`${cwd}/_output/client/input.svelte.warnings.json`); + const ssr_warnings = load_warnings(`${cwd}/_output/server/input.svelte.warnings.json`); + const expected_warnings = (config.warnings || []).map(normalize_warning); + assert.deepEqual(dom_warnings, ssr_warnings); + assert.deepEqual(dom_warnings.map(normalize_warning), expected_warnings); const expected = { html: try_read_file(`${cwd}/expected.html`), diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js index 16c923391d6b..f640e519f9cb 100644 --- a/packages/svelte/tests/helpers.js +++ b/packages/svelte/tests/helpers.js @@ -71,7 +71,7 @@ export async function compile_directory( for (const file of glob('**', { cwd, filesOnly: true })) { if (file.startsWith('_')) continue; - let text = fs.readFileSync(`${cwd}/${file}`, 'utf-8'); + let text = fs.readFileSync(`${cwd}/${file}`, 'utf-8').replace(/\r\n/g, '\n'); let opts = { filename: path.join(cwd, file), ...compileOptions, @@ -138,6 +138,10 @@ export async function compile_directory( write(`${output_dir}/${file}.css.map`, JSON.stringify(compiled.css.map, null, '\t')); } } + + if (compiled.warnings.length > 0) { + write(`${output_dir}/${file}.warnings.json`, JSON.stringify(compiled.warnings, null, '\t')); + } } } } From 5c25d41794cec04fc5c9c29094072c3b39ddc781 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 8 Apr 2024 22:31:40 +0200 Subject: [PATCH 3/6] finalize --- .../compiler/phases/2-analyze/css/css-warn.js | 18 +-- .../_config.js | 4 +- .../_config.js | 4 +- .../_config.js | 24 ++-- .../_config.js | 21 ++-- .../svelte/tests/css/samples/host/_config.js | 4 +- .../svelte/tests/css/samples/is/_config.js | 20 ++++ .../tests/css/samples/nested-css/_config.js | 104 ++++++++++++++++++ .../_config.js | 40 +++---- .../siblings-combinator-each-else/_config.js | 8 +- .../siblings-combinator-slot/_config.js | 12 +- .../_config.js | 12 +- .../expected.css | 2 +- .../input.svelte | 2 +- .../unused-selector-trailing/_config.js | 20 ++++ 15 files changed, 225 insertions(+), 70 deletions(-) create mode 100644 packages/svelte/tests/css/samples/is/_config.js create mode 100644 packages/svelte/tests/css/samples/nested-css/_config.js create mode 100644 packages/svelte/tests/css/samples/unused-selector-trailing/_config.js 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 index 32dfab32abd3..8d7fef873f7d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js @@ -1,5 +1,6 @@ import { walk } from 'zimmerframe'; import { warn } from '../../../warnings.js'; +import { is_keyframes_node } from '../../css.js'; /** * @param {import('#compiler').Css.StyleSheet} stylesheet @@ -11,15 +12,18 @@ export function warn_unused(stylesheet, warnings) { /** @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) { - for (let i = context.path.length - 1; i >= 0; i--) { - const parent = context.path[i]; - if (parent.type === 'RelativeSelector' && parent.metadata.is_global) { - return; // no need to recurse; everything below is global, too - } - } - 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); 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-slot-global/_config.js b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/_config.js index 2038788f678a..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 @@ -5,13 +5,13 @@ export default test({ { code: 'css-unused-selector', end: { - character: 479, + character: 472, column: 19, line: 22 }, message: 'Unused CSS selector ":global(.x) + .bar"', start: { - character: 461, + 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 @@