New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[selectors-4] reconsider specificity rule for :matches() #1027

Open
dbaron opened this Issue Feb 11, 2017 · 23 comments

Comments

Projects
None yet
10 participants
@dbaron
Member

dbaron commented Feb 11, 2017

The rules on specificity in selectors-4 say:

The specificity of a :matches() pseudo-class is replaced by the specificity of its selector list argument. (The full selector’s specificity is equivalent to expanding out all the combinations in full, without :matches().)

I just realized two things:

  1. this isn't worded in a particularly clear way
  2. I'm actually someone nervous about the performance implications of the only logical way to make it clearer.

What is unclear, I think, is that the concept of specificity of a selector list requires both a selector list and an element being matched, since it uses the highest specificity form that matches the element. The algorithm doesn't make it clear that the element being matched is passed to the algorithm.

But that, in turn, pointed out to me that since :matches() can appear anywhere in a selector (between combinators), there might be multiple elements that could match a given matches. In order to not expose the order in which an implementation searches the subtree to find the set of elements that match the combinators, the specification also needs to require that the specificity of a complex selector be the highest-specificity way of matching that complex selector. This is because the new rule for :matches() introduces the possibility that different ways of matching a complex selector (i.e., pairings between elements and the compound selectors in the complex selector) have different specificity.

This, in turn, requires changes to how implementations match combinators, so that they search the tree for the highest-specificity way. I believe this may have substantive performance implications for at least some combinations of combinators, although I haven't actually worked the problem through yet. Somebody should!

This, in turn, makes me wonder whether we should reconsider whether the new specificity rule for :matches() is actually a good idea. (Another option is marking it at risk, but that has its own problems.)

If we do keep it, I'd like to ensure we add tests that exercise the performance-sensitive cases.

@dbaron dbaron added the selectors-4 label Feb 11, 2017

@dbaron dbaron changed the title from [selectors-44] reconsider specificity rule for :matches() to [selectors-4] reconsider specificity rule for :matches() Feb 11, 2017

@SelenIT

This comment has been minimized.

Show comment
Hide comment
@SelenIT

SelenIT Apr 5, 2017

Collaborator

the new rule for :matches() introduces the possibility that different ways of matching a complex selector (i.e., pairings between elements and the compound selectors in the complex selector) have different specificity

@dbaron, could you please provide an example of this possibility?

My understanding so far is that :matches() is basically just kind of syntactic sugar for shortening long lists of selectors that have common parts. So, when a browser checks an element against something like .menu > :matches(a, .current) > .icon, under the hood it does the same work as for checking the same element against .menu > a > .icon, .menu > .current > .icon. Yes, it can possibly introduce performance issues for complex selectors with several :matches(), when the browser would have to check the element against all the combinations of their arguments (so the numbers of the variants would multiply), but it still would be in fact just a very long list of the usual selectors, at least one of which is the most specific. Am I wrong?

Also, given that :matches() has already been implemented in WebKit for about a year and a half, maybe they can provide some feedback about how they dealt with these issues?

Collaborator

SelenIT commented Apr 5, 2017

the new rule for :matches() introduces the possibility that different ways of matching a complex selector (i.e., pairings between elements and the compound selectors in the complex selector) have different specificity

@dbaron, could you please provide an example of this possibility?

My understanding so far is that :matches() is basically just kind of syntactic sugar for shortening long lists of selectors that have common parts. So, when a browser checks an element against something like .menu > :matches(a, .current) > .icon, under the hood it does the same work as for checking the same element against .menu > a > .icon, .menu > .current > .icon. Yes, it can possibly introduce performance issues for complex selectors with several :matches(), when the browser would have to check the element against all the combinations of their arguments (so the numbers of the variants would multiply), but it still would be in fact just a very long list of the usual selectors, at least one of which is the most specific. Am I wrong?

Also, given that :matches() has already been implemented in WebKit for about a year and a half, maybe they can provide some feedback about how they dealt with these issues?

@tabatkins

This comment has been minimized.

Show comment
Hide comment
@tabatkins

tabatkins Apr 5, 2017

Member

Yes, :matches() is solely syntactic sugar (tho with complex selector arguments, the expanded form can get very verbose - try to expand out :matches(.a .b .c, .d .e .f) fully and correctly).

Member

tabatkins commented Apr 5, 2017

Yes, :matches() is solely syntactic sugar (tho with complex selector arguments, the expanded form can get very verbose - try to expand out :matches(.a .b .c, .d .e .f) fully and correctly).

@upsuper

This comment has been minimized.

Show comment
Hide comment
@upsuper

upsuper Dec 21, 2017

Member

Yes, :matches() is solely syntactic sugar (tho with complex selector arguments, the expanded form can get very verbose - try to expand out :matches(.a .b .c, .d .e .f) fully and correctly).

This example isn't very verbose. Consider .a .b .c :matches(.d .e .f), which can be very verbose.

Member

upsuper commented Dec 21, 2017

Yes, :matches() is solely syntactic sugar (tho with complex selector arguments, the expanded form can get very verbose - try to expand out :matches(.a .b .c, .d .e .f) fully and correctly).

This example isn't very verbose. Consider .a .b .c :matches(.d .e .f), which can be very verbose.

@Loirooriol

This comment has been minimized.

Show comment
Hide comment
@Loirooriol

Loirooriol Dec 31, 2017

Collaborator

@SelenIT

could you please provide an example of this possibility?

If I understand correctly, an example could be

<div id="a"></div>
<div id="b">
  <div id="c"></div>
  <div id="d" class="foo">
    <span id="e">Styles are being assigned to this element</span>
  </div>
</div>
:matches(#a, *) + :matches(.foo, *) span

An implementation could detect that

  1. #e matches span with specificity (0,0,1).
  2. #d matches both .foo and *, so it matches :matches(.foo, *) with specificity (0,1,0).
  3. #c matches * but not #a, so it matches :matches(#a, *) with specificity (0,0,0).

So the whole selector is matched with specificity (0,0,0)+(0,1,0)+(0,0,1) = (0,1,1)

However, there is another way to match the selector:

  1. #e matches span with specificity (0,0,1).
  2. #b matches * but not .foo, so it matches :matches(.foo, *) with specificity (0,0,0).
  3. #a matches both * #a and *, so it matches :matches(#a, *) with specificity (1,0,0).

So the whole selector is matched with specificity (1,0,0)+(0,0,0)+(0,0,1) = (1,0,1)

It would be bad if different implementations calculated the specificity of :matches on different elements, because this could affect the total specificity. :nth-* are also affected:

the specificity of an :nth-child(), :nth-last-child(), :nth-of-type(), or :nth-last-of-type() selector is the specificity of the pseudo class itself (counting as one pseudo-class selector) plus the specificity of its selector list argument (if any)

I see various possible solutions:

  • Properly specify on which elements the pseudo-classes are calculated, e.g. the ones that maximize the total specificity. This might have performance problems.
  • Ignore the selector list argument and let the specificity be (0,1,0) like a normal pseudo-class. :-moz-any and :-webkit-any seem to behave like this.
  • Remove :something/:is and define the specificity of :matches to be (0,0,0).
Collaborator

Loirooriol commented Dec 31, 2017

@SelenIT

could you please provide an example of this possibility?

If I understand correctly, an example could be

<div id="a"></div>
<div id="b">
  <div id="c"></div>
  <div id="d" class="foo">
    <span id="e">Styles are being assigned to this element</span>
  </div>
</div>
:matches(#a, *) + :matches(.foo, *) span

An implementation could detect that

  1. #e matches span with specificity (0,0,1).
  2. #d matches both .foo and *, so it matches :matches(.foo, *) with specificity (0,1,0).
  3. #c matches * but not #a, so it matches :matches(#a, *) with specificity (0,0,0).

So the whole selector is matched with specificity (0,0,0)+(0,1,0)+(0,0,1) = (0,1,1)

However, there is another way to match the selector:

  1. #e matches span with specificity (0,0,1).
  2. #b matches * but not .foo, so it matches :matches(.foo, *) with specificity (0,0,0).
  3. #a matches both * #a and *, so it matches :matches(#a, *) with specificity (1,0,0).

So the whole selector is matched with specificity (1,0,0)+(0,0,0)+(0,0,1) = (1,0,1)

It would be bad if different implementations calculated the specificity of :matches on different elements, because this could affect the total specificity. :nth-* are also affected:

the specificity of an :nth-child(), :nth-last-child(), :nth-of-type(), or :nth-last-of-type() selector is the specificity of the pseudo class itself (counting as one pseudo-class selector) plus the specificity of its selector list argument (if any)

I see various possible solutions:

  • Properly specify on which elements the pseudo-classes are calculated, e.g. the ones that maximize the total specificity. This might have performance problems.
  • Ignore the selector list argument and let the specificity be (0,1,0) like a normal pseudo-class. :-moz-any and :-webkit-any seem to behave like this.
  • Remove :something/:is and define the specificity of :matches to be (0,0,0).
@SelenIT

This comment has been minimized.

Show comment
Hide comment
@SelenIT

SelenIT Dec 31, 2017

Collaborator

@Loirooriol, I don’t see this ambiguity in the spec. Since the spec for :matches() specificity says

The full selector’s specificity is equivalent to expanding out all the combinations in full

this example will be expanded as

#a + .foo span, #a + * span, * + .foo span, * + * span { ... }

and by the rules of the selectors list specificity, its specificity will be the largest one of the individual selectors in the list that match the element, in this example it would be (1, 0, 1) of #a + * span. There is only one way to expand :matches() into selector list, and any selector list has only one maximum specificity value of its parts (even if several parts have the same specificity), so there is no place for the ambiguity to occur.

Do I miss something?

Collaborator

SelenIT commented Dec 31, 2017

@Loirooriol, I don’t see this ambiguity in the spec. Since the spec for :matches() specificity says

The full selector’s specificity is equivalent to expanding out all the combinations in full

this example will be expanded as

#a + .foo span, #a + * span, * + .foo span, * + * span { ... }

and by the rules of the selectors list specificity, its specificity will be the largest one of the individual selectors in the list that match the element, in this example it would be (1, 0, 1) of #a + * span. There is only one way to expand :matches() into selector list, and any selector list has only one maximum specificity value of its parts (even if several parts have the same specificity), so there is no place for the ambiguity to occur.

Do I miss something?

@Loirooriol

This comment has been minimized.

Show comment
Hide comment
@Loirooriol

Loirooriol Dec 31, 2017

Collaborator

Yes, with that interpretation the specificity is not ambiguous, but as dbaron said, the definition "isn't worded in a particularly clear way". And may not be the best choice if it has performance problems (expanding :matches can produce exponentially-long selectors).

Collaborator

Loirooriol commented Dec 31, 2017

Yes, with that interpretation the specificity is not ambiguous, but as dbaron said, the definition "isn't worded in a particularly clear way". And may not be the best choice if it has performance problems (expanding :matches can produce exponentially-long selectors).

@fantasai

This comment has been minimized.

Show comment
Hide comment
@fantasai

fantasai Jan 1, 2018

Contributor

Attempted to address the clarity issue in 88b91c0; the specificity rules were written with only compound selectors in mind, and so for complex arguments the existing prose indeed didn't make sense. I don't think it's perfect now, but should be better.

That doesn't address the perf concerns, though. @Loirooriol's comment #1027 (comment) summarizes some of the options; I'll repeat them here and add the missing one:

  • Specificity of :matches() is the specificity of the most specific selector that can match. This is what the spec says now, and is the only logical conclusion of treating :matches() as syntactic sugar to the extent that it can be (i.e. when it contains compound selectors only).
  • Specificity of :matches() is the specificity of any other pseudo-class. This is in conflict with how :not() works already, and also means that S:nth-child(n) and :nth-child(n of S) have different specificities, which is imho not reasonable. (They are functionally different, but have the same weight of meaning.)
  • Specificity of :matches() is zero. Same problems as (0,1,0).
  • Specificity of :matches() is that of its most specific argument. We lose the equivalency of :matches(a,b,c) and a,b,c, but we maintain consistency with :not(), and S:nth-child(n) & :nth-child(n of S) maintain equivalent specificities.
Contributor

fantasai commented Jan 1, 2018

Attempted to address the clarity issue in 88b91c0; the specificity rules were written with only compound selectors in mind, and so for complex arguments the existing prose indeed didn't make sense. I don't think it's perfect now, but should be better.

That doesn't address the perf concerns, though. @Loirooriol's comment #1027 (comment) summarizes some of the options; I'll repeat them here and add the missing one:

  • Specificity of :matches() is the specificity of the most specific selector that can match. This is what the spec says now, and is the only logical conclusion of treating :matches() as syntactic sugar to the extent that it can be (i.e. when it contains compound selectors only).
  • Specificity of :matches() is the specificity of any other pseudo-class. This is in conflict with how :not() works already, and also means that S:nth-child(n) and :nth-child(n of S) have different specificities, which is imho not reasonable. (They are functionally different, but have the same weight of meaning.)
  • Specificity of :matches() is zero. Same problems as (0,1,0).
  • Specificity of :matches() is that of its most specific argument. We lose the equivalency of :matches(a,b,c) and a,b,c, but we maintain consistency with :not(), and S:nth-child(n) & :nth-child(n of S) maintain equivalent specificities.
@SelenIT

This comment has been minimized.

Show comment
Hide comment
@SelenIT

SelenIT Jan 2, 2018

Collaborator

The last option looks promising because the specificity of the selector can be determined without expanding it, but would it play a significant role in practice? If checking for selector matching would require expanding it anyway, will calculating the specificity dynamically necessary need any overhead in terms of performance?

Maybe @victoriasu can provide some details from the implementer's perspective?

Collaborator

SelenIT commented Jan 2, 2018

The last option looks promising because the specificity of the selector can be determined without expanding it, but would it play a significant role in practice? If checking for selector matching would require expanding it anyway, will calculating the specificity dynamically necessary need any overhead in terms of performance?

Maybe @victoriasu can provide some details from the implementer's perspective?

@Loirooriol

This comment has been minimized.

Show comment
Hide comment
@Loirooriol

Loirooriol Jan 2, 2018

Collaborator

@fantasai While definitely useful, I think 88b91c0 still does not clarify that the "calculating a selector’s specificity" algorithm requires an element, and which element is used for the argument of :matches and friends. But I guess this can wait until this issue is resolved.

And thanks for adding the missing option, I thought I included it but it seems I forgot. It may be the nicest one. I'm not an expert but I expect that matching :matches without expanding it should be feasible, @SelenIT. Otherwise, I think it should be confined to the snapshot profile.

Collaborator

Loirooriol commented Jan 2, 2018

@fantasai While definitely useful, I think 88b91c0 still does not clarify that the "calculating a selector’s specificity" algorithm requires an element, and which element is used for the argument of :matches and friends. But I guess this can wait until this issue is resolved.

And thanks for adding the missing option, I thought I included it but it seems I forgot. It may be the nicest one. I'm not an expert but I expect that matching :matches without expanding it should be feasible, @SelenIT. Otherwise, I think it should be confined to the snapshot profile.

@victoriasu

This comment has been minimized.

Show comment
Hide comment
@victoriasu

victoriasu Jan 15, 2018

@SelenIT I am planning on expanding :matches for the specificity. Webkit appears to also use the expanded specificity from what I see when running: https://jsfiddle.net/victoriaytsu/vwsfsfr6/

victoriasu commented Jan 15, 2018

@SelenIT I am planning on expanding :matches for the specificity. Webkit appears to also use the expanded specificity from what I see when running: https://jsfiddle.net/victoriaytsu/vwsfsfr6/

@tabatkins

This comment has been minimized.

Show comment
Hide comment
@tabatkins

tabatkins Jan 16, 2018

Member

[:matches(.a .b .c, .d .e .f)] isn't very verbose. Consider .a .b .c :matches(.d .e .f), which can be very verbose.

Oh shoot, you're right, I meant to write :matches(.a, .b, .c):matches(.d, .e, .f), not a single comma-separated one. (As written, my example is equivalent to a plain .a .b .c, .d .e .f selector list.)

Member

tabatkins commented Jan 16, 2018

[:matches(.a .b .c, .d .e .f)] isn't very verbose. Consider .a .b .c :matches(.d .e .f), which can be very verbose.

Oh shoot, you're right, I meant to write :matches(.a, .b, .c):matches(.d, .e, .f), not a single comma-separated one. (As written, my example is equivalent to a plain .a .b .c, .d .e .f selector list.)

@SelenIT

This comment has been minimized.

Show comment
Hide comment
@SelenIT

SelenIT Feb 6, 2018

Collaborator

As far as I understand, we have 2 implementations of :matches() per the existing spec (option 1 from the above list) since yesterday, one shipped and one experimental behind the flag. (Well done, @victoriasu!)

Is the way of calculating its specificity still subject to change?

Collaborator

SelenIT commented Feb 6, 2018

As far as I understand, we have 2 implementations of :matches() per the existing spec (option 1 from the above list) since yesterday, one shipped and one experimental behind the flag. (Well done, @victoriasu!)

Is the way of calculating its specificity still subject to change?

@Loirooriol

This comment has been minimized.

Show comment
Hide comment
@Loirooriol

Loirooriol Feb 7, 2018

Collaborator

So at first glance at the Chromium's code it seems that very few expansions are allowed:
https://chromium-review.googlesource.com/c/chromium/src/+/879982/15/third_party/WebKit/Source/core/css/CSSSelectorList.cpp#165

Effectively https://jsfiddle.net/u9q1ogc7/ demonstrates the failure. Text should be green.

I think :matches matching should not use expansion and the specificity should be fixed.

Collaborator

Loirooriol commented Feb 7, 2018

So at first glance at the Chromium's code it seems that very few expansions are allowed:
https://chromium-review.googlesource.com/c/chromium/src/+/879982/15/third_party/WebKit/Source/core/css/CSSSelectorList.cpp#165

Effectively https://jsfiddle.net/u9q1ogc7/ demonstrates the failure. Text should be green.

I think :matches matching should not use expansion and the specificity should be fixed.

@dbaron

This comment has been minimized.

Show comment
Hide comment
@dbaron

dbaron May 11, 2018

Member

FWIW, @Loirooriol 's previous comment led to bug 817835.

Today @emilio and I filed bug 842157 about how Chromium's expansion approach produces incorrect results, e.g., on this test (which WebKit passes). :matches() with combinators inside it fundamentally allows branchy selectors that can't be written straightforwardly in pre-:matches() CSS syntax.

Member

dbaron commented May 11, 2018

FWIW, @Loirooriol 's previous comment led to bug 817835.

Today @emilio and I filed bug 842157 about how Chromium's expansion approach produces incorrect results, e.g., on this test (which WebKit passes). :matches() with combinators inside it fundamentally allows branchy selectors that can't be written straightforwardly in pre-:matches() CSS syntax.

@tabatkins

This comment has been minimized.

Show comment
Hide comment
@tabatkins

tabatkins May 14, 2018

Member

So this seems reasonable to me. Slightly unfortunate, but reasonable. Agenda+ing for confirmation.

Member

tabatkins commented May 14, 2018

So this seems reasonable to me. Slightly unfortunate, but reasonable. Agenda+ing for confirmation.

@jonathantneal

This comment has been minimized.

Show comment
Hide comment
@jonathantneal

jonathantneal May 23, 2018

Contributor

If, like me, you had trouble following how :matches should work in this thread, consider the following CSS:

head ~ :matches(html > *) {}

If I understand correctly, that selector should match body, and it should not be equivalent to the following expansion:

head ~ html > * { /* nonsense */ }

Chrome (and postcss-selector-matches) have incorrectly interpreted :matches to follow the later behavior.


Anyway, this is just here to save people like me an hour or so of processing time. And if I misunderstood, please correct me.

Contributor

jonathantneal commented May 23, 2018

If, like me, you had trouble following how :matches should work in this thread, consider the following CSS:

head ~ :matches(html > *) {}

If I understand correctly, that selector should match body, and it should not be equivalent to the following expansion:

head ~ html > * { /* nonsense */ }

Chrome (and postcss-selector-matches) have incorrectly interpreted :matches to follow the later behavior.


Anyway, this is just here to save people like me an hour or so of processing time. And if I misunderstood, please correct me.

@SelenIT

This comment has been minimized.

Show comment
Hide comment
@SelenIT

SelenIT May 23, 2018

Collaborator

@jonathantneal, I agree with your understanding! This selector says "all following siblings of the head element that are also children of the html element", i.e. this selector is equivalent to

html > head ~ * {
  /* targeting elements that are siblings of `head` _and_ children of `html`
     implies that `head` itself must be child of `html`, too */
 }

However, your example made me realize that expanding the brackets of :matches() can be rather non-trivial when both nesting and sibling combinators come into play. Before, I only considered simpler nesting examples like

:matches(.a .b .c):matches(.d .e) { ... }

(based on examples above) which would be expanded out as

.a .b .d .c.e,
.a .b.d .c.e,
.a .d .b .c.e,
.a.d .b .c.e.
.d .a .b .c.e {
   /* target elements with both 'c' and 'e' classes inside '.a .b' and '.d' in the same time */
}

which becomes rather verbose, but still easier to figure out.

So some implementation feedback from the WebKit team would be really appreciated!

Collaborator

SelenIT commented May 23, 2018

@jonathantneal, I agree with your understanding! This selector says "all following siblings of the head element that are also children of the html element", i.e. this selector is equivalent to

html > head ~ * {
  /* targeting elements that are siblings of `head` _and_ children of `html`
     implies that `head` itself must be child of `html`, too */
 }

However, your example made me realize that expanding the brackets of :matches() can be rather non-trivial when both nesting and sibling combinators come into play. Before, I only considered simpler nesting examples like

:matches(.a .b .c):matches(.d .e) { ... }

(based on examples above) which would be expanded out as

.a .b .d .c.e,
.a .b.d .c.e,
.a .d .b .c.e,
.a.d .b .c.e.
.d .a .b .c.e {
   /* target elements with both 'c' and 'e' classes inside '.a .b' and '.d' in the same time */
}

which becomes rather verbose, but still easier to figure out.

So some implementation feedback from the WebKit team would be really appreciated!

@SelenIT

This comment has been minimized.

Show comment
Hide comment
@SelenIT

SelenIT May 23, 2018

Collaborator

Considering the selector in the test from the @dbaron's comment above: if there were classes instead of type selectors there

.h3 ~ :matches(.h1 ~ p.test):matches(.h2 ~ p.test) { ... }

then it would be expanded as

.h1 ~ .h2 ~ .h3 ~ p.test,
.h1 ~ .h3 ~ .h2 ~ p.test,
.h2 ~ .h1 ~ .h3 ~ p.test,
.h2 ~ .h3 ~ .h1 ~ p.test,
.h3 ~ .h1 ~ .h2 ~ p.test,
.h3 ~ .h2 ~ .h1 ~ p.test,
.h1.h2 ~ .h3 ~ p.test,
.h1.h3 ~ .h2 ~ p.test,
.h2.h3 ~ .h1 ~ p.test,
.h1 ~ .h2.h3 ~ p.test,
.h2 ~ .h1.h3 ~ p.test,
.h3 ~ .h1.h2 ~ p.test,
.h1.h2.h3 ~ p.test { /* p.test preceded by all .h1, .h2, and .h3 in any possible order */ }

With type selectors (as in the original example) only the first 6 combinations make sense, so the equivalent expanded result can be shorter.

Collaborator

SelenIT commented May 23, 2018

Considering the selector in the test from the @dbaron's comment above: if there were classes instead of type selectors there

.h3 ~ :matches(.h1 ~ p.test):matches(.h2 ~ p.test) { ... }

then it would be expanded as

.h1 ~ .h2 ~ .h3 ~ p.test,
.h1 ~ .h3 ~ .h2 ~ p.test,
.h2 ~ .h1 ~ .h3 ~ p.test,
.h2 ~ .h3 ~ .h1 ~ p.test,
.h3 ~ .h1 ~ .h2 ~ p.test,
.h3 ~ .h2 ~ .h1 ~ p.test,
.h1.h2 ~ .h3 ~ p.test,
.h1.h3 ~ .h2 ~ p.test,
.h2.h3 ~ .h1 ~ p.test,
.h1 ~ .h2.h3 ~ p.test,
.h2 ~ .h1.h3 ~ p.test,
.h3 ~ .h1.h2 ~ p.test,
.h1.h2.h3 ~ p.test { /* p.test preceded by all .h1, .h2, and .h3 in any possible order */ }

With type selectors (as in the original example) only the first 6 combinations make sense, so the equivalent expanded result can be shorter.

@tabatkins

This comment has been minimized.

Show comment
Hide comment
@tabatkins

tabatkins May 23, 2018

Member

Correct all around; expanding :matches() out to the full set of equivalent :matches()-less selectors can result in a combinatorial explosion of selectors. This is why Sass, which has a virtually-identical problem with its @extend rule, instead just heuristically determines the "most likely to be useful" selectors, and only expands into those.

Member

tabatkins commented May 23, 2018

Correct all around; expanding :matches() out to the full set of equivalent :matches()-less selectors can result in a combinatorial explosion of selectors. This is why Sass, which has a virtually-identical problem with its @extend rule, instead just heuristically determines the "most likely to be useful" selectors, and only expands into those.

@ericwilligers

This comment has been minimized.

Show comment
Hide comment
@ericwilligers

ericwilligers May 23, 2018

Contributor

html > head + :matches(html > head + body) is like html > head + body, but with what specificity?

The specificity of the :matches() pseudo-class is replaced by the specificity of its argument.

suggests specificity (0,0,5)

Thus, a selector written with :matches() has equivalent specificity to the equivalent selector written without :matches()

suggests specificity (0,0,3), unless we consider the "equivalent selector written without :matches()" to mean html:not(:not(html)) > head:not(:not(head)) + body

The specificity of the :not() pseudo-class is replaced by the specificity of the most specific selector in its argument; thus it has the exact behavior of :not(:matches(argument)).

This appears to imply that the specificity of the :matches() pseudo-class is replaced by the specificity of the most specific selector in its argument, so html > head + :matches(body > head + html, *) is like html > head + * but with specificity (0,0,5).

Contributor

ericwilligers commented May 23, 2018

html > head + :matches(html > head + body) is like html > head + body, but with what specificity?

The specificity of the :matches() pseudo-class is replaced by the specificity of its argument.

suggests specificity (0,0,5)

Thus, a selector written with :matches() has equivalent specificity to the equivalent selector written without :matches()

suggests specificity (0,0,3), unless we consider the "equivalent selector written without :matches()" to mean html:not(:not(html)) > head:not(:not(head)) + body

The specificity of the :not() pseudo-class is replaced by the specificity of the most specific selector in its argument; thus it has the exact behavior of :not(:matches(argument)).

This appears to imply that the specificity of the :matches() pseudo-class is replaced by the specificity of the most specific selector in its argument, so html > head + :matches(body > head + html, *) is like html > head + * but with specificity (0,0,5).

@tabatkins

This comment has been minimized.

Show comment
Hide comment
@tabatkins

tabatkins May 23, 2018

Member

The current spec defines that it has the specificity of the matched branch, exactly as if you'd fully expanded the :matches() away.

The proposal in this thread is that it instead has a fixed specificity, probably identical to :not(), so the selector would have [0,0,5] specificity.

Member

tabatkins commented May 23, 2018

The current spec defines that it has the specificity of the matched branch, exactly as if you'd fully expanded the :matches() away.

The proposal in this thread is that it instead has a fixed specificity, probably identical to :not(), so the selector would have [0,0,5] specificity.

@css-meeting-bot

This comment has been minimized.

Show comment
Hide comment
@css-meeting-bot

css-meeting-bot May 30, 2018

Member

The Working Group just discussed reconsider specificity rule for :matches(), and agreed to the following:

  • RESOLVED: Make specificity of :not() :has() and :matches() not depend on matching
The full IRC log of that discussion <dael> Topic: reconsider specificity rule for :matches()
<dael> github: https://github.com//issues/1027
<dael> dbaron: I originally filed this, but don't have a strong opinion on decision. Spec needs to be clear on which
<dael> TabAtkins: Other people have argued one direction: :matches() can introduce some thorny issues on selector inheritence. matches specificity is as specific as the most specific branch. More then one :matches with combinators in the branches...you get...you get a combinatorial explosion. You get 100s or 1000s of selectosr without going deep
<dael> TabAtkins: Naive calc is expensive for memory and unbounded costs.
<fantasai> List of options for considerations - https://github.com//issues/1027#issuecomment-354655842
<dael> TabAtkins: Suggestion was don't bother with that. Resolve it the same as :not and :has where it'sspecificity of the most specific branch. So if you put an ID or a tag it'll b e that. THat's straight forward and matches other similar pseudo classes
<dael> TabAtkins: Only problem is that pre-processors doing :matches ahead can only do it with expanding. @extend in SASS will result in a specificity change. It's not a backwards commpat issue but may be a problem with people or SASS trying to switch to doing the new stuff.
<dael> astearns: [reads dbaron comment]
<dael> TabAtkins: I believe it's correct.
<dael> fantasai: :not takes specificity of most specific arg that didn't match.
<dael> TabAtkins: :not takesa full selector list
<dael> TabAtkins: There's a note. "is replacecd by specificity of most specific element" That note is a liar. That's not true according to spec.
<dael> ??: POinted out a few lies in my comment on the issue
<astearns> s/??/ericwilligers
<dael> TabAtkins: If you look at section 16 :matches and :has uses the brancht hat matches and :not uses the most specific regardless of matching
<fantasai> s/That note is a liar/Also says it has the exact behavior of :not(:matches(argument)), which is a lie./
<dael> frremy: I have another proposal, we don't allow combintators inside :matches()
<dael> fantasai: We had that for a while. original matches had everything. impl said too complex, we tooki t out, impl then said they want it. So I thinkw e have impl that handle complex selectors
<dael> fantasai: The biggest use case is commas.
<dael> frremy: Commas is the whole point of :match I said combinators
<dael> TabAtkins: Combinators are the difficulty
<dael> frremy: :match without combinators is easy.
<fantasai> i/fantasai: The biggest use case/[some confusion about combinators vs commas]/
<dael> TabAtkins: Without combinators, jsut making it compound, doesn't simplify. Still have branches. Look at HTML on list bullets. It's a big list. If you do a simple :matches() rule you still h ave combinatorial branching.
<dael> emilio: Removing combinators makes it simplier
<TabAtkins> `:matches(a, #foo) :matches(a, #foo) :matches(a, #foo)` <= naively expands to 8 choices anyway
<TabAtkins> `:matches(a, #foo, .bar) :matches(a, #foo, .bar) :matches(a, #foo, .bar)` <= naively expands to *27* choices anyway
<dael> dbaron: Thing that's still hard is if you leave commas and you can have multiple matches and have you have backtrack to find the right one. As you walk up ancestors you might match the first on the element and a match for the second with ID but have to try ID ID path
<dael> frremy: Oh, I see
<dael> emilio: Making specificity a property of the selector is nice, i think
<dael> TabAtkins: I see the difficulty and I'm happy to simplify it
<dael> frremy: I think proposal i s in the right direction. Easier to impl i f only compute specificity of the selector as a selector. I can see why people would be confused, but I think it's simplier
<dael> ericwilligers: Same for :not and make it most specific if it matches or not?
<dael> frremy: Yes
<dael> dbaron: I'd be more concerned if I thought specificity was more useful, but I thinik most people fight with it.
<dael> astearns: I'm hearing at least 3 things
<dbaron> s/concerned/concerned with this proposal/
<dael> astearns: 1) places where current spec lies. Need resolutions on those?
<dael> TabAtkins: Won't be a lie once we resolve
<dael> astearns: 2) Removing combinators in :matches()
<dael> TabAtkins: I'd like to keep that separate and reject it.
<TabAtkins> `:matches(.a .b .c, .d .e .f)` expands even faster, of course - expands to over a dozen combination, don't wanna compute the actual number right now because it's non-trivial
<dael> astearns: 3) What we're doing for :matches() and :not(). Is there consensus?
<dael> dbaron: Consesnus to make specificity is only for the selector and not the element
<dbaron> s/only for/only a function of/
<dael> astearns: specificity on :not and :matches depends on selector and not any possible matching.
<dael> TabAtkins: Should do for has as well
<dbaron> s/for has/for :has()/
<dael> astearns: Do not consider matching when determining specificity of :not :matches and d:has
<dael> ericwilligers: Doesn't know why has needs specificity
<dael> TabAtkins: You can't right now, but in theory an impl could allow it.
<TabAtkins> proposed resolution: :matches() and :has() should only consider their selector arguments (using most specific argument) rather than which branch matched, like :not() currently does.
<dael> astearns: Having a non-testable assertion is annoing
<dael> fantasai: Of course has needs specificity.
<dael> TabAtkins: You can only use it i n JS so specifificty doesn't do anything
<dael> fantasai: but if we ever use it in stylesheet
<dael> TabAtkins: Current spec has an assertion, we should make it accurate.
<TabAtkins> s/accurate/consistent/
<dael> astearns: Objections to making specificity of :not :has and :matches not depend on matching
<dael> RESOLVED: Make specificity of :not() :has() and :matches() not depend on matching
Member

css-meeting-bot commented May 30, 2018

The Working Group just discussed reconsider specificity rule for :matches(), and agreed to the following:

  • RESOLVED: Make specificity of :not() :has() and :matches() not depend on matching
The full IRC log of that discussion <dael> Topic: reconsider specificity rule for :matches()
<dael> github: https://github.com//issues/1027
<dael> dbaron: I originally filed this, but don't have a strong opinion on decision. Spec needs to be clear on which
<dael> TabAtkins: Other people have argued one direction: :matches() can introduce some thorny issues on selector inheritence. matches specificity is as specific as the most specific branch. More then one :matches with combinators in the branches...you get...you get a combinatorial explosion. You get 100s or 1000s of selectosr without going deep
<dael> TabAtkins: Naive calc is expensive for memory and unbounded costs.
<fantasai> List of options for considerations - https://github.com//issues/1027#issuecomment-354655842
<dael> TabAtkins: Suggestion was don't bother with that. Resolve it the same as :not and :has where it'sspecificity of the most specific branch. So if you put an ID or a tag it'll b e that. THat's straight forward and matches other similar pseudo classes
<dael> TabAtkins: Only problem is that pre-processors doing :matches ahead can only do it with expanding. @extend in SASS will result in a specificity change. It's not a backwards commpat issue but may be a problem with people or SASS trying to switch to doing the new stuff.
<dael> astearns: [reads dbaron comment]
<dael> TabAtkins: I believe it's correct.
<dael> fantasai: :not takes specificity of most specific arg that didn't match.
<dael> TabAtkins: :not takesa full selector list
<dael> TabAtkins: There's a note. "is replacecd by specificity of most specific element" That note is a liar. That's not true according to spec.
<dael> ??: POinted out a few lies in my comment on the issue
<astearns> s/??/ericwilligers
<dael> TabAtkins: If you look at section 16 :matches and :has uses the brancht hat matches and :not uses the most specific regardless of matching
<fantasai> s/That note is a liar/Also says it has the exact behavior of :not(:matches(argument)), which is a lie./
<dael> frremy: I have another proposal, we don't allow combintators inside :matches()
<dael> fantasai: We had that for a while. original matches had everything. impl said too complex, we tooki t out, impl then said they want it. So I thinkw e have impl that handle complex selectors
<dael> fantasai: The biggest use case is commas.
<dael> frremy: Commas is the whole point of :match I said combinators
<dael> TabAtkins: Combinators are the difficulty
<dael> frremy: :match without combinators is easy.
<fantasai> i/fantasai: The biggest use case/[some confusion about combinators vs commas]/
<dael> TabAtkins: Without combinators, jsut making it compound, doesn't simplify. Still have branches. Look at HTML on list bullets. It's a big list. If you do a simple :matches() rule you still h ave combinatorial branching.
<dael> emilio: Removing combinators makes it simplier
<TabAtkins> `:matches(a, #foo) :matches(a, #foo) :matches(a, #foo)` <= naively expands to 8 choices anyway
<TabAtkins> `:matches(a, #foo, .bar) :matches(a, #foo, .bar) :matches(a, #foo, .bar)` <= naively expands to *27* choices anyway
<dael> dbaron: Thing that's still hard is if you leave commas and you can have multiple matches and have you have backtrack to find the right one. As you walk up ancestors you might match the first on the element and a match for the second with ID but have to try ID ID path
<dael> frremy: Oh, I see
<dael> emilio: Making specificity a property of the selector is nice, i think
<dael> TabAtkins: I see the difficulty and I'm happy to simplify it
<dael> frremy: I think proposal i s in the right direction. Easier to impl i f only compute specificity of the selector as a selector. I can see why people would be confused, but I think it's simplier
<dael> ericwilligers: Same for :not and make it most specific if it matches or not?
<dael> frremy: Yes
<dael> dbaron: I'd be more concerned if I thought specificity was more useful, but I thinik most people fight with it.
<dael> astearns: I'm hearing at least 3 things
<dbaron> s/concerned/concerned with this proposal/
<dael> astearns: 1) places where current spec lies. Need resolutions on those?
<dael> TabAtkins: Won't be a lie once we resolve
<dael> astearns: 2) Removing combinators in :matches()
<dael> TabAtkins: I'd like to keep that separate and reject it.
<TabAtkins> `:matches(.a .b .c, .d .e .f)` expands even faster, of course - expands to over a dozen combination, don't wanna compute the actual number right now because it's non-trivial
<dael> astearns: 3) What we're doing for :matches() and :not(). Is there consensus?
<dael> dbaron: Consesnus to make specificity is only for the selector and not the element
<dbaron> s/only for/only a function of/
<dael> astearns: specificity on :not and :matches depends on selector and not any possible matching.
<dael> TabAtkins: Should do for has as well
<dbaron> s/for has/for :has()/
<dael> astearns: Do not consider matching when determining specificity of :not :matches and d:has
<dael> ericwilligers: Doesn't know why has needs specificity
<dael> TabAtkins: You can't right now, but in theory an impl could allow it.
<TabAtkins> proposed resolution: :matches() and :has() should only consider their selector arguments (using most specific argument) rather than which branch matched, like :not() currently does.
<dael> astearns: Having a non-testable assertion is annoing
<dael> fantasai: Of course has needs specificity.
<dael> TabAtkins: You can only use it i n JS so specifificty doesn't do anything
<dael> fantasai: but if we ever use it in stylesheet
<dael> TabAtkins: Current spec has an assertion, we should make it accurate.
<TabAtkins> s/accurate/consistent/
<dael> astearns: Objections to making specificity of :not :has and :matches not depend on matching
<dael> RESOLVED: Make specificity of :not() :has() and :matches() not depend on matching
@Loirooriol

This comment has been minimized.

Show comment
Hide comment
@Loirooriol

Loirooriol May 30, 2018

Collaborator

Don't forget about :nth-child and :nth-last-child.

Collaborator

Loirooriol commented May 30, 2018

Don't forget about :nth-child and :nth-last-child.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment