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!