fix: better account for render tags when pruning CSS (#14456)

Fixes #14399

Add a mechanism to connect render tags to snippets to know where to walk when coming across render tags

---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
pull/14468/head
Rich Harris 9 months ago committed by GitHub
parent dcef8ff45a
commit c4ac0e01e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: better account for render tags when pruning CSS

@ -314,7 +314,10 @@ function open(parser) {
name
},
parameters: function_expression.params,
body: create_fragment()
body: create_fragment(),
metadata: {
sites: new Set()
}
});
parser.stack.push(block);
parser.fragments.push(block.body);
@ -605,7 +608,8 @@ function special(parser) {
metadata: {
dynamic: false,
args_with_call_expression: new Set(),
path: []
path: [],
snippets: new Set()
}
});
}

@ -1,16 +1,9 @@
/** @import { Visitors } from 'zimmerframe' */
/** @import * as Compiler from '#compiler' */
import { walk } from 'zimmerframe';
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';
/**
* @typedef {{
* element: Compiler.AST.RegularElement | Compiler.AST.SvelteElement;
* from_render_tag: boolean;
* }} State
*/
/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */
const NODE_PROBABLY_EXISTS = 0;
@ -53,78 +46,32 @@ const nesting_selector = {
/**
*
* @param {Compiler.Css.StyleSheet} stylesheet
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag} element
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
*/
export function prune(stylesheet, element) {
if (element.type === 'RenderTag') {
const parent = get_element_parent(element);
if (!parent) return;
walk(stylesheet, { element: parent, from_render_tag: true }, visitors);
} else {
walk(stylesheet, { element, from_render_tag: false }, visitors);
}
}
/** @type {Visitors<Compiler.Css.Node, State>} */
const visitors = {
Rule(node, context) {
if (node.metadata.is_global_block) {
context.visit(node.prelude);
} else {
context.next();
}
},
ComplexSelector(node, context) {
const selectors = get_relative_selectors(node);
const inner = selectors[selectors.length - 1];
if (context.state.from_render_tag) {
// We're searching for a match that crosses a render tag boundary. That means we have to both traverse up
// the element tree (to see if we find an entry point) but also remove selectors from the end (assuming
// they are part of the render tag we don't see). We do all possible combinations of both until we find a match.
/** @type {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null} */
let element = context.state.element;
while (element) {
const selectors_to_check = selectors.slice();
while (selectors_to_check.length > 0) {
selectors_to_check.pop();
if (
apply_selector(
selectors_to_check,
/** @type {Compiler.Css.Rule} */ (node.metadata.rule),
element,
context.state
)
) {
mark(inner, element);
node.metadata.used = true;
return;
}
}
element = get_element_parent(element);
walk(/** @type {Compiler.Css.Node} */ (stylesheet), null, {
Rule(node, context) {
if (node.metadata.is_global_block) {
context.visit(node.prelude);
} else {
context.next();
}
},
ComplexSelector(node) {
const selectors = get_relative_selectors(node);
if (
apply_selector(selectors, /** @type {Compiler.Css.Rule} */ (node.metadata.rule), element)
) {
node.metadata.used = true;
}
} else if (
apply_selector(
selectors,
/** @type {Compiler.Css.Rule} */ (node.metadata.rule),
context.state.element,
context.state
)
) {
mark(inner, context.state.element);
node.metadata.used = true;
}
// note: we don't call context.next() here, we only recurse into
// selectors that don't belong to rules (i.e. inside `:is(...)` etc)
// when we encounter them below
}
};
// note: we don't call context.next() here, we only recurse into
// selectors that don't belong to rules (i.e. inside `:is(...)` etc)
// when we encounter them below
}
});
}
/**
* Retrieves the relative selectors (minus the trailing globals) from a complex selector.
@ -147,6 +94,7 @@ function get_relative_selectors(node) {
has_explicit_nesting_selector = true;
}
});
// if we found one we can break from the others
if (has_explicit_nesting_selector) break;
}
@ -199,89 +147,63 @@ function truncate(node) {
* @param {Compiler.Css.RelativeSelector[]} relative_selectors
* @param {Compiler.Css.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @param {State} state
* @returns {boolean}
*/
function apply_selector(relative_selectors, rule, element, state) {
function apply_selector(relative_selectors, rule, element) {
const parent_selectors = relative_selectors.slice();
const relative_selector = parent_selectors.pop();
if (!relative_selector) return false;
const matched =
!!relative_selector &&
relative_selector_might_apply_to_node(relative_selector, rule, element) &&
apply_combinator(relative_selector, parent_selectors, rule, element);
const possible_match = relative_selector_might_apply_to_node(
relative_selector,
rule,
element,
state
);
if (!possible_match) {
return false;
}
if (relative_selector.combinator) {
return apply_combinator(
relative_selector.combinator,
relative_selector,
parent_selectors,
rule,
element,
state
);
}
if (matched) {
if (!is_outer_global(relative_selector)) {
relative_selector.metadata.scoped = true;
}
// if this is the left-most non-global selector, mark it — we want
// `x y z {...}` to become `x.blah y z.blah {...}`
const parent = parent_selectors[parent_selectors.length - 1];
if (!parent || is_global(parent, rule)) {
mark(relative_selector, element);
element.metadata.scoped = true;
}
return true;
return matched;
}
/**
* @param {Compiler.Css.Combinator} combinator
* @param {Compiler.Css.RelativeSelector} relative_selector
* @param {Compiler.Css.RelativeSelector[]} parent_selectors
* @param {Compiler.Css.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @param {State} state
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @returns {boolean}
*/
function apply_combinator(combinator, relative_selector, parent_selectors, rule, element, state) {
const name = combinator.name;
function apply_combinator(relative_selector, parent_selectors, rule, node) {
if (!relative_selector.combinator) return true;
const name = relative_selector.combinator.name;
switch (name) {
case ' ':
case '>': {
let parent_matched = false;
let crossed_component_boundary = false;
const path = element.metadata.path;
const path = node.metadata.path;
let i = path.length;
while (i--) {
const parent = path[i];
if (parent.type === 'Component' || parent.type === 'SvelteComponent') {
crossed_component_boundary = true;
}
if (parent.type === 'SnippetBlock') {
// We assume the snippet might be rendered in a place where the parent selectors match.
// (We could do more static analysis and check the render tag reference to see if this snippet block continues
// with elements that actually match the selector, but that would be a lot of work for little gain)
return true;
for (const site of parent.metadata.sites) {
if (apply_combinator(relative_selector, parent_selectors, rule, site)) {
return true;
}
}
return false;
}
if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
if (apply_selector(parent_selectors, rule, parent, state)) {
// TODO the `name === ' '` causes false positives, but removing it causes false negatives...
if (name === ' ' || crossed_component_boundary) {
mark(parent_selectors[parent_selectors.length - 1], parent);
}
if (apply_selector(parent_selectors, rule, parent)) {
parent_matched = true;
}
@ -294,7 +216,7 @@ function apply_combinator(combinator, relative_selector, parent_selectors, rule,
case '+':
case '~': {
const siblings = get_possible_element_siblings(element, name === '+');
const siblings = get_possible_element_siblings(node, name === '+');
let sibling_matched = false;
@ -302,18 +224,16 @@ function apply_combinator(combinator, relative_selector, parent_selectors, rule,
if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') {
// `{@render foo()}<p>foo</p>` 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, state)) {
mark(relative_selector, element);
} else if (apply_selector(parent_selectors, rule, possible_sibling)) {
sibling_matched = true;
}
}
return (
sibling_matched ||
(get_element_parent(element) === null &&
(get_element_parent(node) === null &&
parent_selectors.every((selector) => is_global(selector, rule)))
);
}
@ -324,19 +244,6 @@ function apply_combinator(combinator, relative_selector, parent_selectors, rule,
}
}
/**
* Mark both the compound selector and the node it selects as encapsulated,
* for transformation in a later step
* @param {Compiler.Css.RelativeSelector} relative_selector
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
*/
function mark(relative_selector, element) {
if (!is_outer_global(relative_selector)) {
relative_selector.metadata.scoped = true;
}
element.metadata.scoped = true;
}
/**
* Returns `true` if the relative selector is global, meaning
* it's a `:global(...)` or unscopeable selector, or
@ -388,10 +295,9 @@ const regex_backslash_and_following_character = /\\(.)/g;
* @param {Compiler.Css.RelativeSelector} relative_selector
* @param {Compiler.Css.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @param {State} state
* @returns {boolean }
* @returns {boolean}
*/
function relative_selector_might_apply_to_node(relative_selector, rule, element, state) {
function relative_selector_might_apply_to_node(relative_selector, rule, element) {
// Sort :has(...) selectors in one bucket and everything else into another
const has_selectors = [];
const other_selectors = [];
@ -416,7 +322,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
// 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 rules = 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) =>
@ -429,10 +335,12 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
descendant_elements.push(element);
}
walk(
/** @type {Compiler.SvelteNode} */ (element.fragment),
{ is_child: true },
{
/**
* @param {Compiler.SvelteNode} node
* @param {{ is_child: boolean }} state
*/
function walk_children(node, state) {
walk(node, state, {
_(node, context) {
if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
descendant_elements.push(node);
@ -445,12 +353,18 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
} else {
context.next();
}
} else if (node.type === 'RenderTag') {
for (const snippet of node.metadata.snippets) {
walk_children(snippet.body, context.state);
}
} else {
context.next();
}
}
}
);
});
}
walk_children(element.fragment, { is_child: true });
// :has(...) is special in that it means "look downwards in the CSS tree". Since our matching algorithm goes
// upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the
@ -486,7 +400,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
if (
selectors.length === 0 /* is :global(...) */ ||
(element.metadata.scoped && selector_matched) ||
apply_selector(selectors, rule, element, state)
apply_selector(selectors, rule, element)
) {
complex_selector.metadata.used = true;
selector_matched = matched = true;
@ -516,7 +430,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
) {
const args = selector.args;
const complex_selector = args.children[0];
return apply_selector(complex_selector.children, rule, element, state);
return apply_selector(complex_selector.children, rule, element);
}
// We came across a :global, everything beyond it is global and therefore a potential match
@ -565,7 +479,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
if (is_global) {
complex_selector.metadata.used = true;
matched = true;
} else if (apply_selector(relative, rule, element, state)) {
} else if (apply_selector(relative, rule, element)) {
complex_selector.metadata.used = true;
matched = true;
} else if (complex_selector.children.length > 1 && (name == 'is' || name == 'where')) {
@ -649,7 +563,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
for (const complex_selector of parent.prelude.children) {
if (
apply_selector(get_relative_selectors(complex_selector), parent, element, state) ||
apply_selector(get_relative_selectors(complex_selector), parent, element) ||
complex_selector.children.every((s) => is_global(s, parent))
) {
complex_selector.metadata.used = true;
@ -703,17 +617,28 @@ function get_following_sibling_elements(element, include_self) {
// ...then walk them, starting from the node after the one
// containing the element in question
for (const node of nodes.slice(nodes.indexOf(start) + 1)) {
/** @param {Compiler.SvelteNode} node */
function get_siblings(node) {
walk(node, null, {
RegularElement(node) {
siblings.push(node);
},
SvelteElement(node) {
siblings.push(node);
},
RenderTag(node) {
for (const snippet of node.metadata.snippets) {
get_siblings(snippet.body);
}
}
});
}
for (const node of nodes.slice(nodes.indexOf(start) + 1)) {
get_siblings(node);
}
if (include_self) {
siblings.push(element);
}
@ -858,7 +783,7 @@ function unquote(str) {
}
/**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag} node
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @returns {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null}
*/
function get_element_parent(node) {
@ -877,7 +802,7 @@ function get_element_parent(node) {
}
/**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} node
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @param {boolean} adjacent_only
* @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>}
*/
@ -929,7 +854,28 @@ function get_possible_element_siblings(node, adjacent_only) {
current = path[i];
if (!current || !is_block(current)) break;
if (!current) break;
if (
current.type === 'Component' ||
current.type === 'SvelteComponent' ||
current.type === 'SvelteSelf'
) {
continue;
}
if (current.type === 'SnippetBlock') {
for (const site of current.metadata.sites) {
const siblings = get_possible_element_siblings(site, adjacent_only);
add_to_map(siblings, result);
if (adjacent_only && current.metadata.sites.size === 1 && has_definite_elements(siblings)) {
return result;
}
}
}
if (!is_block(current)) break;
if (current.type === 'EachBlock' && fragment === current.body) {
// `{#each ...}<a /><b />{/each}` — `<b>` can be previous sibling of `<a />`

@ -39,15 +39,14 @@ export function get_possible_values(chunk) {
* @param {Css.Rule | null} rule
*/
export function get_parent_rules(rule) {
const parents = [];
const rules = [];
let parent = rule?.metadata.parent_rule;
while (parent) {
parents.push(parent);
parent = parent.metadata.parent_rule;
while (rule) {
rules.push(rule);
rule = rule.metadata.parent_rule;
}
return parents;
return rules;
}
/**

@ -438,7 +438,9 @@ export function analyze_component(root, source, options) {
: '',
keyframes: []
},
source
source,
snippet_renderers: new Map(),
snippets: new Set()
};
if (!runes) {
@ -698,6 +700,16 @@ export function analyze_component(root, source, options) {
);
}
for (const [node, resolved] of analysis.snippet_renderers) {
if (!resolved) {
node.metadata.snippets = analysis.snippets;
}
for (const snippet of node.metadata.snippets) {
snippet.metadata.sites.add(node);
}
}
if (
analysis.uses_render_tags &&
(analysis.uses_slots || (!analysis.custom_element && analysis.slot_names.size > 0))
@ -726,8 +738,6 @@ export function analyze_component(root, source, options) {
}
outer: for (const node of analysis.elements) {
if (node.type === 'RenderTag') continue;
if (node.metadata.scoped) {
// Dynamic elements in dom mode always use spread for attributes and therefore shouldn't have a class attribute added to them
// TODO this happens during the analysis phase, which shouldn't know anything about client vs server

@ -4,6 +4,7 @@ import { unwrap_optional } from '../../../utils/ast.js';
import * as e from '../../../errors.js';
import { validate_opening_tag } from './shared/utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
import { is_resolved_snippet } from './shared/snippets.js';
/**
* @param {AST.RenderTag} node
@ -13,13 +14,25 @@ export function RenderTag(node, context) {
validate_opening_tag(node, context.state, '@');
node.metadata.path = [...context.path];
context.state.analysis.elements.push(node);
const callee = unwrap_optional(node.expression).callee;
node.metadata.dynamic =
callee.type !== 'Identifier' || context.state.scope.get(callee.name)?.kind !== 'normal';
const binding = callee.type === 'Identifier' ? context.state.scope.get(callee.name) : null;
node.metadata.dynamic = binding?.kind !== 'normal';
/**
* If we can't unambiguously resolve this to a declaration, we
* must assume the worst and link the render tag to every snippet
*/
let resolved = callee.type === 'Identifier' && is_resolved_snippet(binding);
if (binding?.initial?.type === 'SnippetBlock') {
// if this render tag unambiguously references a local snippet, our job is easy
node.metadata.snippets.add(binding.initial);
}
context.state.analysis.snippet_renderers.set(node, resolved);
context.state.analysis.uses_render_tags = true;
const raw_args = unwrap_optional(node.expression).arguments;

@ -8,6 +8,8 @@ import * as e from '../../../errors.js';
* @param {Context} context
*/
export function SnippetBlock(node, context) {
context.state.analysis.snippets.add(node);
validate_block_not_empty(node.body, context);
if (context.state.analysis.runes) {

@ -9,12 +9,63 @@ import {
validate_slot_attribute
} from './attribute.js';
import { mark_subtree_dynamic } from './fragment.js';
import { is_resolved_snippet } from './snippets.js';
/**
* @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node
* @param {Context} context
*/
export function visit_component(node, context) {
node.metadata.path = [...context.path];
// link this node to all the snippets that it could render, so that we can prune CSS correctly
node.metadata.snippets = new Set();
// 'resolved' means we know which snippets this component might render. if it is `false`,
// then `node.metadata.snippets` is populated with every locally defined snippet
// once analysis is complete
let resolved = true;
for (const attribute of node.attributes) {
if (attribute.type === 'SpreadAttribute' || attribute.type === 'BindDirective') {
resolved = false;
continue;
}
if (attribute.type !== 'Attribute' || !is_expression_attribute(attribute)) {
continue;
}
const expression = get_attribute_expression(attribute);
// given an attribute like `foo={bar}`, if `bar` resolves to an import or a prop
// then we know it doesn't reference a locally defined snippet. if it resolves
// to a `{#snippet bar()}` then we know _which_ snippet it resolves to. in all
// other cases, we can't know (without much more complex static analysis) which
// snippets the component might render, so we treat the component as unresolved
if (expression.type === 'Identifier') {
const binding = context.state.scope.get(expression.name);
resolved &&= is_resolved_snippet(binding);
if (binding?.initial?.type === 'SnippetBlock') {
node.metadata.snippets.add(binding.initial);
}
} else {
resolved = false;
}
}
if (resolved) {
for (const child of node.fragment.nodes) {
if (child.type === 'SnippetBlock') {
node.metadata.snippets.add(child);
}
}
}
context.state.analysis.snippet_renderers.set(node, resolved);
mark_subtree_dynamic(context.path);
for (const attribute of node.attributes) {

@ -0,0 +1,17 @@
/** @import { Binding } from '#compiler' */
/**
* Returns `true` if a binding unambiguously resolves to a specific
* snippet declaration, or is external to the current component
* @param {Binding | null} binding
*/
export function is_resolved_snippet(binding) {
return (
!binding ||
binding.declaration_kind === 'import' ||
binding.kind === 'prop' ||
binding.kind === 'rest_prop' ||
binding.kind === 'bindable_prop' ||
binding?.initial?.type === 'SnippetBlock'
);
}

@ -37,7 +37,7 @@ export interface ComponentAnalysis extends Analysis {
instance: Js;
template: Template;
/** Used for CSS pruning and scoping */
elements: Array<AST.RegularElement | AST.SvelteElement | AST.RenderTag>;
elements: Array<AST.RegularElement | AST.SvelteElement>;
runes: boolean;
exports: Array<{ name: string; alias: string | null }>;
/** Whether the component uses `$$props` */
@ -72,6 +72,17 @@ export interface ComponentAnalysis extends Analysis {
keyframes: string[];
};
source: string;
/**
* Every render tag/component, and whether it could be definitively resolved or not
*/
snippet_renderers: Map<
AST.RenderTag | AST.Component | AST.SvelteComponent | AST.SvelteSelf,
boolean
>;
/**
* Every snippet that is declared locally
*/
snippets: Set<AST.SnippetBlock>;
}
declare module 'estree' {

@ -166,6 +166,9 @@ export namespace AST {
dynamic: boolean;
args_with_call_expression: Set<number>;
path: SvelteNode[];
/** The set of locally-defined snippets that this render tag could correspond to,
* used for CSS pruning purposes */
snippets: Set<SnippetBlock>;
};
}
@ -278,6 +281,10 @@ export namespace AST {
metadata: {
scopes: Record<string, Scope>;
dynamic: boolean;
/** The set of locally-defined snippets that this component tag could render,
* used for CSS pruning purposes */
snippets: Set<SnippetBlock>;
path: SvelteNode[];
};
}
@ -318,6 +325,10 @@ export namespace AST {
/** @internal */
metadata: {
scopes: Record<string, Scope>;
/** The set of locally-defined snippets that this component tag could render,
* used for CSS pruning purposes */
snippets: Set<SnippetBlock>;
path: SvelteNode[];
};
}
@ -369,6 +380,10 @@ export namespace AST {
/** @internal */
metadata: {
scopes: Record<string, Scope>;
/** The set of locally-defined snippets that this component tag could render,
* used for CSS pruning purposes */
snippets: Set<SnippetBlock>;
path: SvelteNode[];
};
}
@ -438,6 +453,12 @@ export namespace AST {
expression: Identifier;
parameters: Pattern[];
body: Fragment;
/** @internal */
metadata: {
/** The set of components/render tags that could render this snippet,
* used for CSS pruning */
sites: Set<Component | SvelteComponent | SvelteSelf | RenderTag>;
};
}
export interface Attribute extends BaseNode {

@ -2,6 +2,6 @@
background-color: red;
}
main.svelte-xyz div > button:where(.svelte-xyz) {
main.svelte-xyz div:where(.svelte-xyz) > button:where(.svelte-xyz) {
background-color: blue;
}

@ -7,7 +7,7 @@
h2.svelte-xyz span:where(.svelte-xyz) {
color: red;
}
h2.svelte-xyz > span > b:where(.svelte-xyz) {
h2.svelte-xyz > span:where(.svelte-xyz) > b:where(.svelte-xyz) {
color: red;
}
h2.svelte-xyz span:where(.svelte-xyz) b:where(.svelte-xyz) {

@ -0,0 +1,4 @@
h1.svelte-xyz ~ p:where(.svelte-xyz) {
color: green;
}

@ -0,0 +1,13 @@
{#snippet foo()}
<p>this should be green</p>
{/snippet}
<h1>Hello</h1>
{@render foo()}
<style>
h1 ~ p {
color: green;
}
</style>

@ -2,12 +2,6 @@ import { test } from '../../test';
export default test({
warnings: [
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".a ~ .b"',
start: { character: 110, column: 1, line: 10 },
end: { character: 117, column: 8, line: 10 }
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".b ~ .c"',
@ -17,26 +11,26 @@ export default test({
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".c ~ .f"',
start: { character: 164, column: 1, line: 12 },
end: { character: 171, column: 8, line: 12 }
start: { character: 162, column: 1, line: 12 },
end: { character: 169, column: 8, line: 12 }
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".f ~ .g"',
start: { character: 191, column: 1, line: 13 },
end: { character: 198, column: 8, line: 13 }
start: { character: 187, column: 1, line: 13 },
end: { character: 194, column: 8, line: 13 }
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".b ~ .f"',
start: { character: 218, column: 1, line: 14 },
end: { character: 225, column: 8, line: 14 }
start: { character: 212, column: 1, line: 14 },
end: { character: 219, column: 8, line: 14 }
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".b ~ .g"',
start: { character: 245, column: 1, line: 15 },
end: { character: 252, column: 8, line: 15 }
start: { character: 237, column: 1, line: 15 },
end: { character: 244, column: 8, line: 15 }
}
]
});

@ -1,10 +1,11 @@
.d.svelte-xyz ~ .e:where(.svelte-xyz) { color: green; }
.a.svelte-xyz ~ .g:where(.svelte-xyz) { color: green; }
.a.svelte-xyz ~ .b:where(.svelte-xyz) { color: green; }
/* no match */
/* (unused) .a ~ .b { color: green; }*/
/* (unused) .b ~ .c { color: green; }*/
/* (unused) .c ~ .f { color: green; }*/
/* (unused) .f ~ .g { color: green; }*/
/* (unused) .b ~ .f { color: green; }*/
/* (unused) .b ~ .g { color: green; }*/
/* (unused) .b ~ .c { color: red; }*/
/* (unused) .c ~ .f { color: red; }*/
/* (unused) .f ~ .g { color: red; }*/
/* (unused) .b ~ .f { color: red; }*/
/* (unused) .b ~ .g { color: red; }*/

@ -5,14 +5,14 @@
<style>
.d ~ .e { color: green; }
.a ~ .g { color: green; }
.a ~ .b { color: green; }
/* no match */
.a ~ .b { color: green; }
.b ~ .c { color: green; }
.c ~ .f { color: green; }
.f ~ .g { color: green; }
.b ~ .f { color: green; }
.b ~ .g { color: green; }
.b ~ .c { color: red; }
.c ~ .f { color: red; }
.f ~ .g { color: red; }
.b ~ .f { color: red; }
.b ~ .g { color: red; }
</style>
<div class="a"></div>

@ -1,4 +1,4 @@
.match.svelte-xyz > * ~ :where(.svelte-xyz) {
.match.svelte-xyz > :where(.svelte-xyz) ~ :where(.svelte-xyz) {
margin-left: 4px;
}
/* (unused) .not-match > * ~ * {

@ -2,6 +2,6 @@
<div></div>
</div>
<div class="match svelte-xyz">
<div></div>
<div class="svelte-xyz"></div>
<div class="svelte-xyz"></div>
</div>

@ -1,6 +1,6 @@
div.svelte-xyz ~ article:where(.svelte-xyz) { color: green; }
span.svelte-xyz ~ b:where(.svelte-xyz) { color: green; }
div.svelte-xyz span ~ b:where(.svelte-xyz) { color: green; }
div.svelte-xyz span:where(.svelte-xyz) ~ b:where(.svelte-xyz) { color: green; }
.a.svelte-xyz ~ article:where(.svelte-xyz) { color: green; }
div.svelte-xyz ~ .b:where(.svelte-xyz) { color: green; }
.a.svelte-xyz ~ .c:where(.svelte-xyz) { color: green; }

@ -0,0 +1,20 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css_unused_selector',
message: 'Unused CSS selector "z:has(+ y)"',
start: {
line: 23,
column: 1,
character: 217
},
end: {
line: 23,
column: 11,
character: 227
}
}
]
});

@ -0,0 +1,11 @@
x.svelte-xyz:has(y:where(.svelte-xyz)) {
color: green;
}
p.svelte-xyz:has(+ y:where(.svelte-xyz)) {
color: green;
}
/* (unused) z:has(+ y) {
color: red;
}*/

@ -0,0 +1,26 @@
{#snippet foo()}
<y></y>
{/snippet}
<x>
this should be green
{@render foo()}
</x>
<z>
<p>this should be green</p>
{@render foo()}
</z>
<style>
x:has(y) {
color: green;
}
p:has(+ y) {
color: green;
}
z:has(+ y) {
color: red;
}
</style>

@ -0,0 +1,5 @@
<script>
let { children } = $props();
</script>
{@render children()}

@ -0,0 +1,15 @@
<script>
import Child from './Child.svelte';
</script>
<x></x>
<Child>
<y>this should be green</y>
</Child>
<style>
x + y {
color: green;
}
</style>

@ -0,0 +1,5 @@
<script>
let { foo } = $props();
</script>
{@render foo()}

@ -0,0 +1,17 @@
<script>
import Child from './Child.svelte';
</script>
<x></x>
<Child>
{#snippet foo()}
<y>this should be green</y>
{/snippet}
</Child>
<style>
x + y {
color: green;
}
</style>

@ -0,0 +1,4 @@
x.svelte-xyz + z:where(.svelte-xyz) {
color: green;
}

@ -0,0 +1,13 @@
<x></x>
<slot>
<y>fallback content</y>
</slot>
<z>this should be green if the slot fallback is not rendered</z>
<style>
x + z {
color: green;
}
</style>

@ -0,0 +1,4 @@
h1.svelte-xyz + p:where(.svelte-xyz) {
color: green;
}

@ -0,0 +1,13 @@
{#snippet foo()}
<p>this should be green</p>
{/snippet}
<h1>Hello</h1>
{@render foo()}
<style>
h1 + p {
color: green;
}
</style>

@ -0,0 +1,15 @@
<script>
import Child from './Child.svelte';
</script>
<Child>
<div class="a">a</div>
<div class="b" slot="wut">b</div>
<div class="c">c</div>
</Child>
<style>
.a + .c {
color: green;
}
</style>

@ -2,12 +2,6 @@ import { test } from '../../test';
export default test({
warnings: [
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".a + .b"',
start: { character: 83, column: 1, line: 9 },
end: { character: 90, column: 8, line: 9 }
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".b + .c"',
@ -17,8 +11,8 @@ export default test({
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".c + .f"',
start: { character: 137, column: 1, line: 11 },
end: { character: 144, column: 8, line: 11 }
start: { character: 135, column: 1, line: 11 },
end: { character: 142, column: 8, line: 11 }
}
]
});

@ -1,6 +1,7 @@
.d.svelte-xyz + .e:where(.svelte-xyz) { color: green; }
.a.svelte-xyz + .b:where(.svelte-xyz) { color: green; }
/* no match */
/* (unused) .a + .b { color: green; }*/
/* (unused) .b + .c { color: green; }*/
/* (unused) .c + .f { color: green; }*/
/* (unused) .b + .c { color: red; }*/
/* (unused) .c + .f { color: red; }*/

@ -4,11 +4,11 @@
<style>
.d + .e { color: green; }
.a + .b { color: green; }
/* no match */
.a + .b { color: green; }
.b + .c { color: green; }
.c + .f { color: green; }
.b + .c { color: red; }
.c + .f { color: red; }
</style>
<div class="a"></div>

@ -1,4 +1,4 @@
.match.svelte-xyz > * + :where(.svelte-xyz) {
.match.svelte-xyz > :where(.svelte-xyz) + :where(.svelte-xyz) {
margin-left: 4px;
}
/* (unused) .not-match > * + * {

@ -2,6 +2,6 @@
<div></div>
</div>
<div class="match svelte-xyz">
<div></div>
<div class="svelte-xyz"></div>
<div class="svelte-xyz"></div>
</div>

@ -16,7 +16,7 @@
span.svelte-xyz + b:where(.svelte-xyz) {
color: green;
}
div.svelte-xyz span + b:where(.svelte-xyz) {
div.svelte-xyz span:where(.svelte-xyz) + b:where(.svelte-xyz) {
color: green;
}
.a.svelte-xyz + article:where(.svelte-xyz) {

@ -2,18 +2,32 @@ import { test } from '../../test';
export default test({
warnings: [
{
code: 'css_unused_selector',
message: 'Unused CSS selector "p .foo"',
start: {
line: 28,
column: 1,
character: 356
},
end: {
line: 28,
column: 7,
character: 362
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "span div"',
start: {
line: 31,
column: 1,
character: 461
character: 383
},
end: {
line: 31,
column: 9,
character: 469
character: 391
}
}
]

@ -11,9 +11,9 @@
p.svelte-xyz span:where(.svelte-xyz) {
color: green;
}
p.svelte-xyz .foo:where(.svelte-xyz) {
color: purple; /* doesn't match, but our static analysis doesn't handle this currently */
}
/* (unused) p .foo {
color: red;
}*/
/* (unused) span div {
color: red;
}*/

@ -26,7 +26,7 @@
color: green;
}
p .foo {
color: purple; /* doesn't match, but our static analysis doesn't handle this currently */
color: red;
}
span div {
color: red;

Loading…
Cancel
Save