From 45fa678242bec9809b9458a7e0102ceeacc369af Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 14 Nov 2024 11:24:02 +0100 Subject: [PATCH] fix: account for `:has(...)` as part of `:root` (#14229) We previously marked all `:root` selectors as global-like, which excempted them from further analysis. This causes problems: - things like `:not(...)` are never visited and therefore never marked as used -> we gotta do that directly when coming across this - `:has(...)` was never visited, too. Just marking it as used is not enough though, because we might need to scope its contents Therefore the logic is enhanced to account for these special cases. Fixes #14118 While fixing this I cleaned up some inconsistencies in what we mark as global. This simplified code and fixed some adjacent bugs, which conindicentally also fixes #14189 --- .changeset/beige-files-pull.md | 5 + .changeset/great-bulldogs-wonder.md | 5 + .../phases/2-analyze/css/css-analyze.js | 47 ++++---- .../phases/2-analyze/css/css-prune.js | 101 +++++++++--------- .../compiler/phases/2-analyze/css/css-warn.js | 7 +- .../compiler/phases/2-analyze/css/utils.js | 84 ++++++++++++++- .../compiler/phases/3-transform/css/index.js | 14 +-- packages/svelte/src/compiler/types/css.d.ts | 4 +- .../svelte/tests/css/samples/has/_config.js | 42 ++++++++ .../svelte/tests/css/samples/has/expected.css | 19 +++- .../svelte/tests/css/samples/has/input.svelte | 15 +++ .../svelte/tests/css/samples/is/_config.js | 40 +++---- .../svelte/tests/css/samples/is/expected.css | 15 +++ .../svelte/tests/css/samples/is/input.svelte | 15 +++ .../samples/not-selector-global/expected.css | 9 ++ .../samples/not-selector-global/input.svelte | 9 ++ .../svelte/tests/css/samples/root/_config.js | 75 ++++++++++++- .../tests/css/samples/root/expected.css | 47 +++++++- .../tests/css/samples/root/expected.html | 2 +- .../tests/css/samples/root/input.svelte | 46 +++++++- 20 files changed, 484 insertions(+), 117 deletions(-) create mode 100644 .changeset/beige-files-pull.md create mode 100644 .changeset/great-bulldogs-wonder.md diff --git a/.changeset/beige-files-pull.md b/.changeset/beige-files-pull.md new file mode 100644 index 0000000000..2cd98a2819 --- /dev/null +++ b/.changeset/beige-files-pull.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: account for `:has(...)` as part of `:root` diff --git a/.changeset/great-bulldogs-wonder.md b/.changeset/great-bulldogs-wonder.md new file mode 100644 index 0000000000..b6cafa4585 --- /dev/null +++ b/.changeset/great-bulldogs-wonder.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent nested pseudo class from being marked as unused diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js index ec36e1ce64..38551f328f 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js @@ -4,6 +4,7 @@ import { walk } from 'zimmerframe'; import * as e from '../../../errors.js'; import { is_keyframes_node } from '../../css.js'; +import { is_global, is_unscoped_pseudo_class } from './utils.js'; /** * @typedef {Visitors< @@ -15,27 +16,6 @@ import { is_keyframes_node } from '../../css.js'; * >} CssVisitors */ -/** - * True if is `:global(...)` or `:global` - * @param {Css.RelativeSelector} relative_selector - * @returns {relative_selector is Css.RelativeSelector & { selectors: [Css.PseudoClassSelector, ...Array] }} - */ -function is_global(relative_selector) { - const first = relative_selector.selectors[0]; - - return ( - first.type === 'PseudoClassSelector' && - first.name === 'global' && - (first.args === null || - // Only these two selector types keep the whole selector global, because e.g. - // :global(button).x means that the selector is still scoped because of the .x - relative_selector.selectors.every( - (selector) => - selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector' - )) - ); -} - /** * True if is `:global` * @param {Css.SimpleSelector} simple_selector @@ -119,11 +99,14 @@ const css_visitors = { node.metadata.rule?.metadata.parent_rule && node.children[0]?.selectors[0]?.type === 'NestingSelector' ) { + const first = node.children[0]?.selectors[1]; + const no_nesting_scope = + first?.type !== 'PseudoClassSelector' || is_unscoped_pseudo_class(first); const parent_is_global = node.metadata.rule.metadata.parent_rule.prelude.children.some( (child) => child.children.length === 1 && child.children[0].metadata.is_global ); // mark `&:hover` in `:global(.foo) { &:hover { color: green }}` as used - if (parent_is_global) { + if (no_nesting_scope && parent_is_global) { node.metadata.used = true; } } @@ -156,9 +139,23 @@ const css_visitors = { ].includes(first.name)); } - node.metadata.is_global_like ||= !!node.selectors.find( - (child) => child.type === 'PseudoClassSelector' && child.name === 'root' - ); + node.metadata.is_global_like ||= + node.selectors.some( + (child) => child.type === 'PseudoClassSelector' && child.name === 'root' + ) && + // :root.y:has(.x) is not a global selector because while .y is unscoped, .x inside `:has(...)` should be scoped + !node.selectors.some((child) => child.type === 'PseudoClassSelector' && child.name === 'has'); + + if (node.metadata.is_global_like || node.metadata.is_global) { + // So that nested selectors like `:root:not(.x)` are not marked as unused + for (const child of node.selectors) { + walk(/** @type {Css.Node} */ (child), null, { + ComplexSelector(node) { + node.metadata.used = true; + } + }); + } + } context.next(); }, 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 a9ea9fe913..bf0fd27566 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 @@ -1,7 +1,7 @@ /** @import { Visitors } from 'zimmerframe' */ /** @import * as Compiler from '#compiler' */ import { walk } from 'zimmerframe'; -import { get_possible_values } from './utils.js'; +import { get_parent_rules, get_possible_values, is_outer_global } from './utils.js'; import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js'; import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js'; @@ -172,7 +172,7 @@ function get_relative_selectors(node) { } /** - * Discard trailing `:global(...)` selectors without a `:has/is/where/not(...)` modifier, these are unused for scoping purposes + * Discard trailing `:global(...)` selectors, these are unused for scoping purposes * @param {Compiler.Css.ComplexSelector} node */ function truncate(node) { @@ -182,21 +182,22 @@ function truncate(node) { // not after a :global selector !metadata.is_global_like && !(first.type === 'PseudoClassSelector' && first.name === 'global' && first.args === null) && - // not a :global(...) without a :has/is/where/not(...) modifier - (!metadata.is_global || - selectors.some( - (selector) => - selector.type === 'PseudoClassSelector' && - selector.args !== null && - (selector.name === 'has' || - selector.name === 'is' || - selector.name === 'where' || - selector.name === 'not') - )) + // not a :global(...) without a :has/is/where(...) modifier that is scoped + !metadata.is_global ); }); - return node.children.slice(0, i + 1); + return node.children.slice(0, i + 1).map((child) => { + // In case of `:root.y:has(...)`, `y` is unscoped, but everything in `:has(...)` should be scoped (if not global). + // To properly accomplish that, we gotta filter out all selector types except `:has`. + const root = child.selectors.find((s) => s.type === 'PseudoClassSelector' && s.name === 'root'); + if (!root || child.metadata.is_global_like) return child; + + return { + ...child, + selectors: child.selectors.filter((s) => s.type === 'PseudoClassSelector' && s.name === 'has') + }; + }); } /** @@ -334,7 +335,9 @@ function apply_combinator(combinator, relative_selector, parent_selectors, rule, * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element */ function mark(relative_selector, element) { - relative_selector.metadata.scoped = true; + if (!is_outer_global(relative_selector)) { + relative_selector.metadata.scoped = true; + } element.metadata.scoped = true; } @@ -415,6 +418,21 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, /** @type {Array} */ let sibling_elements; // do them lazy because it's rarely used and expensive to calculate + // If this is a :has inside a global selector, we gotta include the element itself, too, + // because the global selector might be for an element that's outside the component (e.g. :root). + const rules = [rule, ...get_parent_rules(rule)]; + const include_self = + rules.some((r) => r.prelude.children.some((c) => c.children.some((s) => is_global(s, r)))) || + rules[rules.length - 1].prelude.children.some((c) => + c.children.some((r) => + r.selectors.some((s) => s.type === 'PseudoClassSelector' && s.name === 'root') + ) + ); + if (include_self) { + child_elements.push(element); + descendant_elements.push(element); + } + walk( /** @type {Compiler.SvelteNode} */ (element.fragment), { is_child: true }, @@ -460,7 +478,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, const descendants = left_most_combinator.name === '+' || left_most_combinator.name === '~' - ? (sibling_elements ??= get_following_sibling_elements(element)) + ? (sibling_elements ??= get_following_sibling_elements(element, include_self)) : left_most_combinator.name === '>' ? child_elements : descendant_elements; @@ -481,20 +499,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, } if (!matched) { - if (relative_selector.metadata.is_global && !relative_selector.metadata.is_global_like) { - // Edge case: `:global(.x):has(.y)` where `.x` is global but `.y` doesn't match. - // Since `used` is set to `true` for `:global(.x)` in css-analyze beforehand, and - // we have no way of knowing if it's safe to set it back to `false`, we'll mark - // the inner selector as used and scoped to prevent it from being pruned, which could - // result in a invalid CSS output (e.g. `.x:has(/* unused .y */)`). The result - // can't match a real element, so the only drawback is the missing prune. - // TODO clean this up some day - complex_selectors[0].metadata.used = true; - complex_selectors[0].children.forEach((selector) => { - selector.metadata.scoped = true; - }); - } - return false; } } @@ -507,9 +511,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, switch (selector.type) { case 'PseudoClassSelector': { - if (name === 'host' || name === 'root') { - return false; - } + if (name === 'host' || name === 'root') return false; if ( name === 'global' && @@ -578,23 +580,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, } if (!matched) { - if ( - relative_selector.metadata.is_global && - !relative_selector.metadata.is_global_like - ) { - // Edge case: `:global(.x):is(.y)` where `.x` is global but `.y` doesn't match. - // Since `used` is set to `true` for `:global(.x)` in css-analyze beforehand, and - // we have no way of knowing if it's safe to set it back to `false`, we'll mark - // the inner selector as used and scoped to prevent it from being pruned, which could - // result in a invalid CSS output (e.g. `.x:is(/* unused .y */)`). The result - // can't match a real element, so the only drawback is the missing prune. - // TODO clean this up some day - selector.args.children[0].metadata.used = true; - selector.args.children[0].children.forEach((selector) => { - selector.metadata.scoped = true; - }); - } - return false; } } @@ -662,7 +647,10 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, const parent = /** @type {Compiler.Css.Rule} */ (rule.metadata.parent_rule); for (const complex_selector of parent.prelude.children) { - if (apply_selector(get_relative_selectors(complex_selector), parent, element, state)) { + if ( + apply_selector(get_relative_selectors(complex_selector), parent, element, state) || + complex_selector.children.every((s) => is_global(s, parent)) + ) { complex_selector.metadata.used = true; matched = true; } @@ -681,8 +669,11 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, return true; } -/** @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element */ -function get_following_sibling_elements(element) { +/** + * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element + * @param {boolean} include_self + */ +function get_following_sibling_elements(element, include_self) { /** @type {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.Root | null} */ let parent = get_element_parent(element); @@ -723,6 +714,10 @@ function get_following_sibling_elements(element) { } } + if (include_self) { + sibling_elements.push(element); + } + return sibling_elements; } 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 482278b795..eab67327e2 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 @@ -24,7 +24,12 @@ const visitors = { } }, ComplexSelector(node, context) { - if (!node.metadata.used) { + if ( + !node.metadata.used && + // prevent double-marking of `.unused:is(.unused)` + (context.path.at(-2)?.type !== 'PseudoClassSelector' || + /** @type {Css.ComplexSelector} */ (context.path.at(-4))?.metadata.used) + ) { const content = context.state.stylesheet.content; const text = content.styles.substring(node.start - content.start, node.end - content.start); w.css_unused_selector(node, text); diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/utils.js b/packages/svelte/src/compiler/phases/2-analyze/css/utils.js index 99603fad29..0226a150c9 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/utils.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/utils.js @@ -1,4 +1,4 @@ -/** @import { AST } from '#compiler' */ +/** @import { AST, Css } from '#compiler' */ /** @import { Node } from 'estree' */ const UNKNOWN = {}; @@ -33,3 +33,85 @@ export function get_possible_values(chunk) { if (values.has(UNKNOWN)) return null; return values; } + +/** + * Returns all parent rules; root is last + * @param {Css.Rule | null} rule + */ +export function get_parent_rules(rule) { + const parents = []; + + let parent = rule?.metadata.parent_rule; + while (parent) { + parents.push(parent); + parent = parent.metadata.parent_rule; + } + + return parents; +} + +/** + * True if is `:global(...)` or `:global` and no pseudo class that is scoped. + * @param {Css.RelativeSelector} relative_selector + * @returns {relative_selector is Css.RelativeSelector & { selectors: [Css.PseudoClassSelector, ...Array] }} + */ +export function is_global(relative_selector) { + const first = relative_selector.selectors[0]; + + return ( + first.type === 'PseudoClassSelector' && + first.name === 'global' && + (first.args === null || + // Only these two selector types keep the whole selector global, because e.g. + // :global(button).x means that the selector is still scoped because of the .x + relative_selector.selectors.every( + (selector) => + is_unscoped_pseudo_class(selector) || selector.type === 'PseudoElementSelector' + )) + ); +} + +/** + * `true` if is a pseudo class that cannot be or is not scoped + * @param {Css.SimpleSelector} selector + */ +export function is_unscoped_pseudo_class(selector) { + return ( + selector.type === 'PseudoClassSelector' && + // These make the selector scoped + ((selector.name !== 'has' && + selector.name !== 'is' && + selector.name !== 'where' && + // Not is special because we want to scope as specific as possible, but because :not + // inverses the result, we want to leave the unscoped, too. The exception is more than + // one selector in the :not (.e.g :not(.x .y)), then .x and .y should be scoped + (selector.name !== 'not' || + selector.args === null || + selector.args.children.every((c) => c.children.length === 1))) || + // selectors with has/is/where/not can also be global if all their children are global + selector.args === null || + selector.args.children.every((c) => c.children.every((r) => is_global(r)))) + ); +} + +/** + * True if is `:global(...)` or `:global`, irrespective of whether or not there are any pseudo classes that are scoped. + * Difference to `is_global`: `:global(x):has(y)` is `true` for `is_outer_global` but `false` for `is_global`. + * @param {Css.RelativeSelector} relative_selector + * @returns {relative_selector is Css.RelativeSelector & { selectors: [Css.PseudoClassSelector, ...Array] }} + */ +export function is_outer_global(relative_selector) { + const first = relative_selector.selectors[0]; + + return ( + first.type === 'PseudoClassSelector' && + first.name === 'global' && + (first.args === null || + // Only these two selector types can keep the whole selector global, because e.g. + // :global(button).x means that the selector is still scoped because of the .x + relative_selector.selectors.every( + (selector) => + selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector' + )) + ); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/css/index.js b/packages/svelte/src/compiler/phases/3-transform/css/index.js index 350eca35d8..d1f3d3baa6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/css/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/css/index.js @@ -292,6 +292,13 @@ const visitors = { context.state.code.prependRight(global.start, '&'); } continue; + } else { + // for any :global() or :global at the middle of compound selector + for (const selector of relative_selector.selectors) { + if (selector.type === 'PseudoClassSelector' && selector.name === 'global') { + remove_global_pseudo_class(selector, null, context.state); + } + } } if (relative_selector.metadata.scoped) { @@ -306,13 +313,6 @@ const visitors = { } } - // for any :global() or :global at the middle of compound selector - for (const selector of relative_selector.selectors) { - if (selector.type === 'PseudoClassSelector' && selector.name === 'global') { - remove_global_pseudo_class(selector, null, context.state); - } - } - if (relative_selector.selectors.some((s) => s.type === 'NestingSelector')) { continue; } diff --git a/packages/svelte/src/compiler/types/css.d.ts b/packages/svelte/src/compiler/types/css.d.ts index 97ac7fc0d3..ba4079ecd0 100644 --- a/packages/svelte/src/compiler/types/css.d.ts +++ b/packages/svelte/src/compiler/types/css.d.ts @@ -87,8 +87,8 @@ export namespace Css { /** * `true` if the whole selector is unscoped, e.g. `:global(...)` or `:global` or `:global.x`. * Selectors like `:global(...).x` are not considered global, because they still need scoping. - * Selectors like `:global(...):is/where/not/has(...)` are considered global even if they aren't - * strictly speaking (we should consolidate the logic around this at some point). + * Selectors like `:global(...):is/where/not/has(...)` are only considered global if all their + * children are global. */ is_global: boolean; /** `:root`, `:host`, `::view-transition`, or selectors after a `:global` */ diff --git a/packages/svelte/tests/css/samples/has/_config.js b/packages/svelte/tests/css/samples/has/_config.js index 8b8e7760f5..e5dc5f3459 100644 --- a/packages/svelte/tests/css/samples/has/_config.js +++ b/packages/svelte/tests/css/samples/has/_config.js @@ -44,6 +44,20 @@ export default test({ character: 401 } }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector ":global(.foo):has(.unused)"', + start: { + line: 40, + column: 1, + character: 422 + }, + end: { + line: 40, + column: 27, + character: 448 + } + }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(y):has(.unused)"', @@ -141,6 +155,34 @@ export default test({ column: 11, character: 1336 } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector ":has(.unused)"', + start: { + line: 129, + column: 2, + character: 1409 + }, + end: { + line: 129, + column: 15, + character: 1422 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector "&:has(.unused)"', + start: { + line: 135, + column: 2, + character: 1480 + }, + end: { + line: 135, + column: 16, + character: 1494 + } } ] }); diff --git a/packages/svelte/tests/css/samples/has/expected.css b/packages/svelte/tests/css/samples/has/expected.css index 4cb8d76bf6..8eda676f93 100644 --- a/packages/svelte/tests/css/samples/has/expected.css +++ b/packages/svelte/tests/css/samples/has/expected.css @@ -27,9 +27,9 @@ /* (unused) x:has(.unused) { color: red; }*/ - .foo:has(.unused.svelte-xyz) { + /* (unused) :global(.foo):has(.unused) { color: red; - } + }*/ x.svelte-xyz:has(y:where(.svelte-xyz) /* (unused) .unused*/) { color: green; @@ -111,3 +111,18 @@ /* (unused) x:has(~ y) { color: red; }*/ + + .foo { + .svelte-xyz:has(x:where(.svelte-xyz)) { + color: green; + } + /* (unused) :has(.unused) { + color: red; + }*/ + &:has(x.svelte-xyz) { + color: green; + } + /* (unused) &:has(.unused) { + color: red; + }*/ + } diff --git a/packages/svelte/tests/css/samples/has/input.svelte b/packages/svelte/tests/css/samples/has/input.svelte index 8eb6296f05..3487b64e8c 100644 --- a/packages/svelte/tests/css/samples/has/input.svelte +++ b/packages/svelte/tests/css/samples/has/input.svelte @@ -121,4 +121,19 @@ x:has(~ y) { color: red; } + + :global(.foo) { + :has(x) { + color: green; + } + :has(.unused) { + color: red; + } + &:has(x) { + color: green; + } + &:has(.unused) { + color: red; + } + } diff --git a/packages/svelte/tests/css/samples/is/_config.js b/packages/svelte/tests/css/samples/is/_config.js index e4c24eb756..eee617e7e4 100644 --- a/packages/svelte/tests/css/samples/is/_config.js +++ b/packages/svelte/tests/css/samples/is/_config.js @@ -30,20 +30,6 @@ export default test({ character: 125 } }, - { - code: 'css_unused_selector', - message: 'Unused CSS selector ".unused"', - start: { - line: 14, - column: 7, - character: 117 - }, - end: { - line: 14, - column: 14, - character: 124 - } - }, { code: 'css_unused_selector', message: 'Unused CSS selector ":global(.foo) :is(.unused)"', @@ -60,16 +46,30 @@ export default test({ }, { code: 'css_unused_selector', - message: 'Unused CSS selector ".unused"', + message: 'Unused CSS selector ":global(.foo):is(.unused)"', start: { - line: 28, - column: 19, - character: 292 + line: 34, + column: 1, + character: 363 }, end: { - line: 28, + line: 34, column: 26, - character: 299 + character: 388 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector ":is(.unused)"', + start: { + line: 52, + column: 2, + character: 636 + }, + end: { + line: 52, + column: 14, + character: 648 } } ] diff --git a/packages/svelte/tests/css/samples/is/expected.css b/packages/svelte/tests/css/samples/is/expected.css index be8deeff28..639f0d5392 100644 --- a/packages/svelte/tests/css/samples/is/expected.css +++ b/packages/svelte/tests/css/samples/is/expected.css @@ -22,6 +22,12 @@ /* (unused) :global(.foo) :is(.unused) { color: red; }*/ + .foo:is(x.svelte-xyz) { + color: green; + } + /* (unused) :global(.foo):is(.unused) { + color: red; + }*/ x.svelte-xyz :is(html *) { color: green; @@ -32,3 +38,12 @@ y.svelte-xyz :is(x:where(.svelte-xyz) :where(.svelte-xyz)) { color: green; /* matches z */ } + + .foo { + :is(x.svelte-xyz) { + color: green; + } + /* (unused) :is(.unused) { + color: red; + }*/ + } diff --git a/packages/svelte/tests/css/samples/is/input.svelte b/packages/svelte/tests/css/samples/is/input.svelte index 06aa0669b9..b5ae663087 100644 --- a/packages/svelte/tests/css/samples/is/input.svelte +++ b/packages/svelte/tests/css/samples/is/input.svelte @@ -28,6 +28,12 @@ :global(.foo) :is(.unused) { color: red; } + :global(.foo):is(x) { + color: green; + } + :global(.foo):is(.unused) { + color: red; + } x :is(:global(html *)) { color: green; @@ -38,4 +44,13 @@ y :is(x *) { color: green; /* matches z */ } + + :global(.foo) { + :is(x) { + color: green; + } + :is(.unused) { + color: red; + } + } diff --git a/packages/svelte/tests/css/samples/not-selector-global/expected.css b/packages/svelte/tests/css/samples/not-selector-global/expected.css index ea9ff39486..b815dcf9aa 100644 --- a/packages/svelte/tests/css/samples/not-selector-global/expected.css +++ b/packages/svelte/tests/css/samples/not-selector-global/expected.css @@ -27,3 +27,12 @@ span:not(p span) { color: green; } + + .x { + .svelte-xyz:not(.foo) { + color: green; + } + &:not(.foo) { + color: green; + } + } diff --git a/packages/svelte/tests/css/samples/not-selector-global/input.svelte b/packages/svelte/tests/css/samples/not-selector-global/input.svelte index 1f0e6cd6db..1d2f7a9bca 100644 --- a/packages/svelte/tests/css/samples/not-selector-global/input.svelte +++ b/packages/svelte/tests/css/samples/not-selector-global/input.svelte @@ -34,4 +34,13 @@ :global(span:not(p span)) { color: green; } + + :global(.x) { + :not(.foo) { + color: green; + } + &:not(.foo) { + color: green; + } + } \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/root/_config.js b/packages/svelte/tests/css/samples/root/_config.js index f47bee71df..fad7fc536d 100644 --- a/packages/svelte/tests/css/samples/root/_config.js +++ b/packages/svelte/tests/css/samples/root/_config.js @@ -1,3 +1,76 @@ import { test } from '../../test'; -export default test({}); +export default test({ + warnings: [ + { + code: 'css_unused_selector', + message: 'Unused CSS selector ":root .unused"', + start: { + line: 18, + column: 2, + character: 190 + }, + end: { + line: 18, + column: 15, + character: 203 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector ":root:has(.unused)"', + start: { + line: 25, + column: 2, + character: 269 + }, + end: { + line: 25, + column: 20, + character: 287 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector ".unused"', + start: { + line: 37, + column: 4, + character: 401 + }, + end: { + line: 37, + column: 11, + character: 408 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector ":has(.unused)"', + start: { + line: 43, + column: 4, + character: 480 + }, + end: { + line: 43, + column: 17, + character: 493 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector "&:has(.unused)"', + start: { + line: 49, + column: 4, + character: 566 + }, + end: { + line: 49, + column: 18, + character: 580 + } + } + ] +}); diff --git a/packages/svelte/tests/css/samples/root/expected.css b/packages/svelte/tests/css/samples/root/expected.css index a107696250..b4c82c258e 100644 --- a/packages/svelte/tests/css/samples/root/expected.css +++ b/packages/svelte/tests/css/samples/root/expected.css @@ -1,9 +1,52 @@ + :root { - color: red; + color: green; } .foo:root { - color: blue; + color: green; } :root.foo { color: green; } + :root.unknown { + color: green; + } + + :root h1.svelte-xyz { + color: green; + } + /* (unused) :root .unused { + color: red; + }*/ + + :root:has(h1:where(.svelte-xyz)) { + color: green; + } + /* (unused) :root:has(.unused) { + color: red; + }*/ + + :root:not(.x) { + color: green; + } + + :root { + h1.svelte-xyz { + color: green; + } + /* (unused) .unused { + color: red; + }*/ + .svelte-xyz:has(h1:where(.svelte-xyz)) { + color: green; + } + /* (unused) :has(.unused) { + color: red; + }*/ + &:has(h1.svelte-xyz) { + color: green; + } + /* (unused) &:has(.unused) { + color: red; + }*/ + } diff --git a/packages/svelte/tests/css/samples/root/expected.html b/packages/svelte/tests/css/samples/root/expected.html index 1d90ab5df7..5c30de1c29 100644 --- a/packages/svelte/tests/css/samples/root/expected.html +++ b/packages/svelte/tests/css/samples/root/expected.html @@ -1 +1 @@ -

Hello!

\ No newline at end of file +

Hello!

\ No newline at end of file diff --git a/packages/svelte/tests/css/samples/root/input.svelte b/packages/svelte/tests/css/samples/root/input.svelte index 979c9d4f0a..e06a1a36fa 100644 --- a/packages/svelte/tests/css/samples/root/input.svelte +++ b/packages/svelte/tests/css/samples/root/input.svelte @@ -1,13 +1,55 @@

Hello!