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
pull/14297/head
Simon H 10 months ago committed by GitHub
parent ac9b7de058
commit 45fa678242
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: account for `:has(...)` as part of `:root`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent nested pseudo class from being marked as unused

@ -4,6 +4,7 @@
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { is_keyframes_node } from '../../css.js'; import { is_keyframes_node } from '../../css.js';
import { is_global, is_unscoped_pseudo_class } from './utils.js';
/** /**
* @typedef {Visitors< * @typedef {Visitors<
@ -15,27 +16,6 @@ import { is_keyframes_node } from '../../css.js';
* >} CssVisitors * >} CssVisitors
*/ */
/**
* True if is `:global(...)` or `:global`
* @param {Css.RelativeSelector} relative_selector
* @returns {relative_selector is Css.RelativeSelector & { selectors: [Css.PseudoClassSelector, ...Array<Css.PseudoClassSelector | Css.PseudoElementSelector>] }}
*/
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` * True if is `:global`
* @param {Css.SimpleSelector} simple_selector * @param {Css.SimpleSelector} simple_selector
@ -119,11 +99,14 @@ const css_visitors = {
node.metadata.rule?.metadata.parent_rule && node.metadata.rule?.metadata.parent_rule &&
node.children[0]?.selectors[0]?.type === 'NestingSelector' 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( const parent_is_global = node.metadata.rule.metadata.parent_rule.prelude.children.some(
(child) => child.children.length === 1 && child.children[0].metadata.is_global (child) => child.children.length === 1 && child.children[0].metadata.is_global
); );
// mark `&:hover` in `:global(.foo) { &:hover { color: green }}` as used // 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; node.metadata.used = true;
} }
} }
@ -156,9 +139,23 @@ const css_visitors = {
].includes(first.name)); ].includes(first.name));
} }
node.metadata.is_global_like ||= !!node.selectors.find( node.metadata.is_global_like ||=
node.selectors.some(
(child) => child.type === 'PseudoClassSelector' && child.name === 'root' (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(); context.next();
}, },

@ -1,7 +1,7 @@
/** @import { Visitors } from 'zimmerframe' */ /** @import { Visitors } from 'zimmerframe' */
/** @import * as Compiler from '#compiler' */ /** @import * as Compiler from '#compiler' */
import { walk } from 'zimmerframe'; 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 { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js';
import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.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 * @param {Compiler.Css.ComplexSelector} node
*/ */
function truncate(node) { function truncate(node) {
@ -182,21 +182,22 @@ function truncate(node) {
// not after a :global selector // not after a :global selector
!metadata.is_global_like && !metadata.is_global_like &&
!(first.type === 'PseudoClassSelector' && first.name === 'global' && first.args === null) && !(first.type === 'PseudoClassSelector' && first.name === 'global' && first.args === null) &&
// not a :global(...) without a :has/is/where/not(...) modifier // not a :global(...) without a :has/is/where(...) modifier that is scoped
(!metadata.is_global || !metadata.is_global
selectors.some(
(selector) =>
selector.type === 'PseudoClassSelector' &&
selector.args !== null &&
(selector.name === 'has' ||
selector.name === 'is' ||
selector.name === 'where' ||
selector.name === 'not')
))
); );
}); });
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 * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
*/ */
function mark(relative_selector, element) { function mark(relative_selector, element) {
if (!is_outer_global(relative_selector)) {
relative_selector.metadata.scoped = true; relative_selector.metadata.scoped = true;
}
element.metadata.scoped = true; element.metadata.scoped = true;
} }
@ -415,6 +418,21 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */ /** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
let sibling_elements; // do them lazy because it's rarely used and expensive to calculate 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( walk(
/** @type {Compiler.SvelteNode} */ (element.fragment), /** @type {Compiler.SvelteNode} */ (element.fragment),
{ is_child: true }, { is_child: true },
@ -460,7 +478,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
const descendants = const descendants =
left_most_combinator.name === '+' || left_most_combinator.name === '~' 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 === '>' : left_most_combinator.name === '>'
? child_elements ? child_elements
: descendant_elements; : descendant_elements;
@ -481,20 +499,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
} }
if (!matched) { 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; return false;
} }
} }
@ -507,9 +511,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
switch (selector.type) { switch (selector.type) {
case 'PseudoClassSelector': { case 'PseudoClassSelector': {
if (name === 'host' || name === 'root') { if (name === 'host' || name === 'root') return false;
return false;
}
if ( if (
name === 'global' && name === 'global' &&
@ -578,23 +580,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
} }
if (!matched) { 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; 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); const parent = /** @type {Compiler.Css.Rule} */ (rule.metadata.parent_rule);
for (const complex_selector of parent.prelude.children) { 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; complex_selector.metadata.used = true;
matched = true; matched = true;
} }
@ -681,8 +669,11 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
return true; 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} */ /** @type {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.Root | null} */
let parent = get_element_parent(element); 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; return sibling_elements;
} }

@ -24,7 +24,12 @@ const visitors = {
} }
}, },
ComplexSelector(node, context) { 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 content = context.state.stylesheet.content;
const text = content.styles.substring(node.start - content.start, node.end - content.start); const text = content.styles.substring(node.start - content.start, node.end - content.start);
w.css_unused_selector(node, text); w.css_unused_selector(node, text);

@ -1,4 +1,4 @@
/** @import { AST } from '#compiler' */ /** @import { AST, Css } from '#compiler' */
/** @import { Node } from 'estree' */ /** @import { Node } from 'estree' */
const UNKNOWN = {}; const UNKNOWN = {};
@ -33,3 +33,85 @@ export function get_possible_values(chunk) {
if (values.has(UNKNOWN)) return null; if (values.has(UNKNOWN)) return null;
return values; 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<Css.PseudoClassSelector | Css.PseudoElementSelector>] }}
*/
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<Css.PseudoClassSelector | Css.PseudoElementSelector>] }}
*/
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'
))
);
}

@ -292,6 +292,13 @@ const visitors = {
context.state.code.prependRight(global.start, '&'); context.state.code.prependRight(global.start, '&');
} }
continue; 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) { 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')) { if (relative_selector.selectors.some((s) => s.type === 'NestingSelector')) {
continue; continue;
} }

@ -87,8 +87,8 @@ export namespace Css {
/** /**
* `true` if the whole selector is unscoped, e.g. `:global(...)` or `:global` or `:global.x`. * `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(...).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 * Selectors like `:global(...):is/where/not/has(...)` are only considered global if all their
* strictly speaking (we should consolidate the logic around this at some point). * children are global.
*/ */
is_global: boolean; is_global: boolean;
/** `:root`, `:host`, `::view-transition`, or selectors after a `:global` */ /** `:root`, `:host`, `::view-transition`, or selectors after a `:global` */

@ -44,6 +44,20 @@ export default test({
character: 401 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', code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(y):has(.unused)"', message: 'Unused CSS selector "x:has(y):has(.unused)"',
@ -141,6 +155,34 @@ export default test({
column: 11, column: 11,
character: 1336 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
}
} }
] ]
}); });

@ -27,9 +27,9 @@
/* (unused) x:has(.unused) { /* (unused) x:has(.unused) {
color: red; color: red;
}*/ }*/
.foo:has(.unused.svelte-xyz) { /* (unused) :global(.foo):has(.unused) {
color: red; color: red;
} }*/
x.svelte-xyz:has(y:where(.svelte-xyz) /* (unused) .unused*/) { x.svelte-xyz:has(y:where(.svelte-xyz) /* (unused) .unused*/) {
color: green; color: green;
@ -111,3 +111,18 @@
/* (unused) x:has(~ y) { /* (unused) x:has(~ y) {
color: red; 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;
}*/
}

@ -121,4 +121,19 @@
x:has(~ y) { x:has(~ y) {
color: red; color: red;
} }
:global(.foo) {
:has(x) {
color: green;
}
:has(.unused) {
color: red;
}
&:has(x) {
color: green;
}
&:has(.unused) {
color: red;
}
}
</style> </style>

@ -32,44 +32,44 @@ export default test({
}, },
{ {
code: 'css_unused_selector', code: 'css_unused_selector',
message: 'Unused CSS selector ".unused"', message: 'Unused CSS selector ":global(.foo) :is(.unused)"',
start: { start: {
line: 14, line: 28,
column: 7, column: 1,
character: 117 character: 274
}, },
end: { end: {
line: 14, line: 28,
column: 14, column: 27,
character: 124 character: 300
} }
}, },
{ {
code: 'css_unused_selector', code: 'css_unused_selector',
message: 'Unused CSS selector ":global(.foo):is(.unused)"', message: 'Unused CSS selector ":global(.foo):is(.unused)"',
start: { start: {
line: 28, line: 34,
column: 1, column: 1,
character: 274 character: 363
}, },
end: { end: {
line: 28, line: 34,
column: 27, column: 26,
character: 300 character: 388
} }
}, },
{ {
code: 'css_unused_selector', code: 'css_unused_selector',
message: 'Unused CSS selector ".unused"', message: 'Unused CSS selector ":is(.unused)"',
start: { start: {
line: 28, line: 52,
column: 19, column: 2,
character: 292 character: 636
}, },
end: { end: {
line: 28, line: 52,
column: 26, column: 14,
character: 299 character: 648
} }
} }
] ]

@ -16,6 +16,12 @@
color: green; color: green;
} }
.foo :is(x.svelte-xyz) {
color: green;
}
/* (unused) :global(.foo) :is(.unused) {
color: red;
}*/
.foo:is(x.svelte-xyz) { .foo:is(x.svelte-xyz) {
color: green; color: green;
} }
@ -32,3 +38,12 @@
y.svelte-xyz :is(x:where(.svelte-xyz) :where(.svelte-xyz)) { y.svelte-xyz :is(x:where(.svelte-xyz) :where(.svelte-xyz)) {
color: green; /* matches z */ color: green; /* matches z */
} }
.foo {
:is(x.svelte-xyz) {
color: green;
}
/* (unused) :is(.unused) {
color: red;
}*/
}

@ -22,6 +22,12 @@
color: green; color: green;
} }
:global(.foo) :is(x) {
color: green;
}
:global(.foo) :is(.unused) {
color: red;
}
:global(.foo):is(x) { :global(.foo):is(x) {
color: green; color: green;
} }
@ -38,4 +44,13 @@
y :is(x *) { y :is(x *) {
color: green; /* matches z */ color: green; /* matches z */
} }
:global(.foo) {
:is(x) {
color: green;
}
:is(.unused) {
color: red;
}
}
</style> </style>

@ -27,3 +27,12 @@
span:not(p span) { span:not(p span) {
color: green; color: green;
} }
.x {
.svelte-xyz:not(.foo) {
color: green;
}
&:not(.foo) {
color: green;
}
}

@ -34,4 +34,13 @@
:global(span:not(p span)) { :global(span:not(p span)) {
color: green; color: green;
} }
:global(.x) {
:not(.foo) {
color: green;
}
&:not(.foo) {
color: green;
}
}
</style> </style>

@ -1,3 +1,76 @@
import { test } from '../../test'; 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
}
}
]
});

@ -1,9 +1,52 @@
:root { :root {
color: red; color: green;
} }
.foo:root { .foo:root {
color: blue; color: green;
} }
:root.foo { :root.foo {
color: green; 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;
}*/
}

@ -1 +1 @@
<h1>Hello!</h1> <h1 class="svelte-xyz">Hello!</h1>

@ -1,13 +1,55 @@
<style> <style>
:root { :root {
color: red; color: green;
} }
.foo:root { .foo:root {
color: blue; color: green;
} }
:root.foo { :root.foo {
color: green; color: green;
} }
:root.unknown {
color: green;
}
:root h1 {
color: green;
}
:root .unused {
color: red;
}
:root:has(h1) {
color: green;
}
:root:has(.unused) {
color: red;
}
:root:not(.x) {
color: green;
}
:root {
h1 {
color: green;
}
.unused {
color: red;
}
:has(h1) {
color: green;
}
:has(.unused) {
color: red;
}
&:has(h1) {
color: green;
}
&:has(.unused) {
color: red;
}
}
</style> </style>
<h1>Hello!</h1> <h1>Hello!</h1>

Loading…
Cancel
Save