# Extend Specificity It's valuable to be able to optimize away selectors generated by `@extend` if they match subsets of the elements matched by other selectors in the same style rule. However, optimizing *every* such selector can end up having unexpected consequences when it changes the specificity with which the style rule applies to a given element. This proposal lays out restrictions on the specificity of selectors that result from an `@extend`. First of all, let's define the function `extend(S, A, B)` to be the result of taking a selector `S` and extending it by replacing all instances of `A` with `A, B` and resolving the result a la `@extend`. Here are some uncontroversial examples: ``` extend(a, a, b) = a, b extend(a.foo, a, b) = a.foo, b.foo extend(c, a, b) = c ``` ## Specificity of the Base Selector Note that so far, it's always the case that `extend(S, A, B)[0] = S`. However, consider `extend(a.foo, .foo, a)`. One interpretation of this would give the result as `a.foo, a`. However, `a` matches a strict superset of the elements that `a.foo` matches, so another interpretation could give the result as just `a`. `a` and `a.foo, a` are semantically identical **except** for specificity. Let's define a new function to talk about this: `spec(S)` is the specificity of a selector `S`. So `spec(a.foo) = 11`, while `spec(a) = 1`. The nature of CSS means that differences in specificity can lead to practical differences in styling, so to some degree we clearly need to consider specificity as part of the semantics of the selectors we deal with. This is the broad point of this issue. Let's get back to the example of `extend(a.foo, .foo, a)`. The first selector in the result, `extend(a.foo, .foo, a)[0]`, corresponds to the selector written by the user with the goal of directly styling a set of elements. Allowing the specificity of this selector to change because an `@extend` was added elsewhere in the stylesheet is semantic change at a distance, which is clearly something we shouldn't allow. Thus, it should be the case that `extend(a.foo, .foo, a)[0] = a.foo` and in general that `spec(extend(S, A, B)[0]) >= spec(S)`. In most cases, the first generated selector should be identical to `S`. However, this isn't possible when dealing with the `:not()` pseudo-selector. For example, ``` scss :not(.foo) {...} .bar {@extend .foo} ``` Because `:not` specifically declares selectors that the rule **doesn't** apply to, extending those selectors will necessarily increase the specificity of the base selector. The example above should compile to ``` css :not(.foo):not(.bar) {...} ``` This new selector has higher specificity than the original. As such, we must allow the generated selector to have higher specificity than the original in some cases. ### First Law of Extend: `spec(extend(S, A, B)[0]) >= spec(S)` This is not always the behavior in Sass, either in master or in stable; this is clearly a bug that should be fixed. ## Specificity of Generated Selectors Now that we've established what `spec(extend(S, A, B)[0])` should look like, it's time to think about what `spec(extend(S, A, B)[1])` should look like as well. In order to allow our users to reason about the styling of their page, the specificity of the generated selectors should clearly be as consistent as possible. In an ideal world, if `@extend` were supported natively in the browser, the specificity would be equivalent to that of the original selector; that is, `spec(extend(S, A, B)[1]) = spec(S)`. However, that's not always possible: ``` extend(a, a, b.foo) = a, b.foo spec(a) < spec(b.foo) extend(a.foo, a.foo, b) = a.foo, b spec(a.foo) > spec(b) ``` Since consistency is desirable, we might be tempted instead to say that `spec(extend(S, A, B)[1]) = spec(B)`. But that's not always possible either: ``` extend(a.foo, a, b) = a.foo, b.foo spec(b) < spec(b.foo) ``` There is one guarantee we can make, though: `spec(extend(S, A, B)[1]) >= spec(B)`, since everything in `S` is either merged with or added to `B`. ### Second Law of Extend: `spec(extend(S, A, B)[1]) >= spec(B)` ## Implications for Optimization The ultimate goal of this discussion is, of course, that we want to be able to perform certain optimizations on the generated selectors in order to reduce output size, but we don't want these optimizations to break the guarantees we offer our users. Which optimizations do the guarantees outlines above allow us, and which do they forbid? One optimization that we've been doing for a long time is `extend(a.foo, .foo, a) = a`, as discussed above. This violates the first law, since `a != a.foo`. Another optimization added in [8f4869e] is `extend(a, a, a.foo) = a`. This violates the second law, since `spec(a) < spec(a.foo)`. [8f4869e]: https://github.com/sass/ruby-sass/commit/8f4869e608e70d7f468bb463ebfe7a939d834e27 However, many of the optimizations added in [8f4869e] do still work. For example, `extend(.bar a, a, a.foo) = .bar a` works because `spec(.bar a) = spec(a.foo)`. ## Conclusion As long as we make the `@extend` optimizer specificity-aware, we can retain a number of useful optimizations while still providing the same guarantees that they have without any optimizations. That's my proposal: that we support all the optimizations we can while still abiding by the two Laws of Extend outlined above.