Repository: sveltejs/svelte (JavaScript)
Date: 2020-01-27 23:02:48+00:00
Task type: Bug Fix

Issue:
:global(...) .child selector is given svelte- suffix if there is a variable class name in the html
If there is an element with a `class={someVariable}` attribute in the markup, `:global(body) .className` child selectors get suffixed, and therefore don't apply to child components.

For example, if you have a parent component that applies a "theme" class to its top-level element, and has child selectors to style matching elements within all its child components depending on the theme, it doesn't work if the theme class is a variable (but does if it's hard-coded as e.g. `<div class="purple">`.

(The variable class is what's relevant - any element with a non-hard-coded class name in the parent component will break it.)

REPL:

- reproduction: https://svelte.dev/repl/0cf466aa625c42ed9dc845d0b46d1905?version=3.17.3
- fixed (by removing variable class name): https://svelte.dev/repl/2f88829ff20846a79e8c395f8ecef741?version=3.17.3


Issue discussion:
I do think there's a bug here, but I don't think it's what you're saying it is.

`.purple :global(.text)` is the selector that you should be using to refer to `.text` elements in any descendant of this component's `.purple` elements. This selector works with or without the `span` you mention.

`:global(.purple) .text` is supposed to match elements in this component with `.text` that have `.purple` among their ancestors. As far as Svelte can tell at compile time, this might match `<span class={cls}></span>` but it won't match `<div class="purple"><Child/></div>`.

What does seem to be a bug to me is that, starting from your example, if you remove the `span`, Svelte doesn't see that as an unused style to be removed. `:global(.purple) .text` shouldn't match anything because there isn't anything in this component that could match `.text`.
> `:global(.purple) .text` is supposed to match elements in this component with `.text` that have `.purple` among their ancestors. As far as Svelte can tell at compile time, this might match `<span class={cls}></span>` but it won't match `<div class="purple"><Child/></div>`.

I think `<div class="purple"><Child/></div>` should match per your description, as the child element has a .text class in it.

Consider the flattened form: `<div class="purple"><span class="text">should be purple?</span></div>`

Child being a svelte component shouldn't change that. I think the right way to go about this though would be `:global(.purple .text)` which works.

If I understand correctly though, anything not `:global()` should be interpreted as a local class, so I do get where you are coming from regarding the issue.
Thanks for clarifying @Conduitry.  I was under the impression that `:global` made the whole selector global, but the behaviour you specify makes more sense.

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
 ## Unreleased
 
 * Fix code generation error with adjacent inline and block comments ([#4312](https://github.com/sveltejs/svelte/issues/4312))
+* Fix detection of unused CSS selectors that begin with a `:global()` but contain a scoped portion ([#4314](https://github.com/sveltejs/svelte/issues/4314))
 
 ## 3.18.0
 
diff --git a/src/compiler/compile/css/Selector.ts b/src/compiler/compile/css/Selector.ts
--- a/src/compiler/compile/css/Selector.ts
+++ b/src/compiler/compile/css/Selector.ts
@@ -32,7 +32,7 @@ export default class Selector {
 		}
 
 		this.local_blocks = this.blocks.slice(0, i);
-		this.used = this.blocks[0].global;
+		this.used = this.local_blocks.length === 0;
 	}
 
 	apply(node: Element, stack: Element[]) {


Test patch:
diff --git a/test/css/samples/global-with-unused-descendant/_config.js b/test/css/samples/global-with-unused-descendant/_config.js
new file mode 100644
--- /dev/null
+++ b/test/css/samples/global-with-unused-descendant/_config.js
@@ -0,0 +1,24 @@
+export default {
+	warnings: [{
+		code: 'css-unused-selector',
+		end: {
+			character: 27,
+			column: 19,
+			line: 2
+		},
+		frame: `
+			1: <style>
+			2:   :global(.foo) .bar {
+			     ^
+			3:     color: red;
+			4:   }
+		`,
+		message: 'Unused CSS selector',
+		pos: 9,
+		start: {
+			character: 9,
+			column: 1,
+			line: 2
+		}
+	}]
+};
diff --git a/test/css/samples/global-with-unused-descendant/expected.css b/test/css/samples/global-with-unused-descendant/expected.css
new file mode 100644
diff --git a/test/css/samples/global-with-unused-descendant/input.svelte b/test/css/samples/global-with-unused-descendant/input.svelte
new file mode 100644
--- /dev/null
+++ b/test/css/samples/global-with-unused-descendant/input.svelte
@@ -0,0 +1,5 @@
+<style>
+	:global(.foo) .bar {
+		color: red;
+	}
+</style>