Stylelint: Fix false positives for pseudo classes in no-descending-specificity

Created on 10 Apr 2017  Â·  31Comments  Â·  Source: stylelint/stylelint

Based on I'm using sass to develop css,
I don't want to define the class (.b) without parents.
however, it will happen error with this rule.

is it(as below example) not usual for define class?

but I still need this rule to validate the other situations.

Which rule, if any, is this issue related to?

no-descending-specificity

What CSS is needed to reproduce this issue?

.a {
    .b {}
}

.a--modify {
    .b {}
}

What stylelint configuration is needed to reproduce this issue?

no-descending-specificity

{
  "rules": {
    "no-descending-specificity": true
  }
}

Which version of stylelint are you using?

version: 7.8.0

What actually happened (e.g. what warnings or errors you are getting)?

Expected selector ".a--modify .b" to come before selector ".a .b"
Even I switch the order, it still happened.

good first issue ready to implement bug

Most helpful comment

Could you explain further?

Defo. I'm not 100% sure myself. This rule is somewhat tricky.

I think something is amiss. I'm unsure whether it's a bug, a misalignment between the docs and rule behaviour, or, perhaps, an option is needed to allow @monochrome-yeh code style of:

.a .b {}
.a .b:focus {}

.a--alt .b {} /* rules says this should come before `a. .b:focus` */
.a--alt .b:focus {}

This currently warns because:

  1. .b is compared to .b:focus.
  2. .a .b:focus has a specificity of 0,3,0 and .a--alt .b, which follows it, has specificity of 0,2,0.

However, it feels like this is a valid code style because .b is a descendant _with a different parent_. The intent of the rule is to enforce that "Stylesheets are most legible when overriding selectors always come after the selectors they override" - however, in this situation, I think there is no risk of override because of the different parent.

So, it feels like the rule should continue to warn if there is:

  1. no parent
  2. or the parents are the same
  3. or one of the parents is of a different type of selector e.g. _id selector_ (#a) vs _class selector_ (.a)

For example, these should continue to warn:

.a:hover {}
.a {}
.a .b:hover {}
.a .b {}
.a .b {}
#a .b {}

However, if:

  1. there are parent selectors
  2. and there are the same number of parent selectors
  3. and these parent selectors are of the same selector types e.g. _class selectors_
  4. but they are different selectors e.g. .a vs .c

I'm wondering whether it shouldn't warn e.g. this is ok:

.a .b:hover {}
.c .b {}

I think I'm finding the fact that pseudo-classes are treated differently than chained classes a bit confusing. The docs read:

Here's how it works: This rule looks at the last compound selector in every full selector, and then compares it with other selectors in the stylesheet that end in the same way.

However, I think the rule treats .a and a:focus as the same compound selector even though they aren't. It makes sense to do this because it allows the rule to catch a:focus {} a {}, but it feels like it might introduce the, arguable, false positive outlined above.

All 31 comments

.a--modify .b and .a .b have the same specificity. Warning won't be thrown, because there is no violation.

I got exceptions from them.
there's my specific code as below:


.c-menu {
    .c-menu__content {
        line-height: 15px;

        display: block;
        color: grey;
        background-color: white;

        &:focus,
        &:hover {
            color: white;
            background-color: grey;

            input,
            button,
            textarea {
                color: grey;
                background-color: white;
            }
        }
    }
}

.c-menu--alt {
    background-color: grey;

    .c-menu__content {
        color: white;
        background-color: grey;

        &:focus,
        &:hover {
            color: grey;
            background-color: white;

            input,
            radio,
            textarea {
                color: white;
                background-color: grey;
            }
        }
    }
}

2017-04-10 2 10 33

According to the docs, this is what is happening:

  1. .c-menu--alt .c-menu__content {} and .c-menu .c-menu__content:focus {} are deemed to have the same last compound selector i.e. .c-menu__content.
  2. .c-menu--alt .c-menu__content {} has a specificity of 0,0,2,0 and .c-menu .c-menu__content:focus {} has one of 0,0,3,0

Therefore .c-menu--alt .c-menu__content {} is expected to come before .c-menu .c-menu__content:focus {}, but in your stylelint it does not.

I guess the first step to resolving this issue is to confirm number 1 i.e. .c-menu--alt .c-menu__content {} and .c-menu .c-menu__content:focus {} have the same last compound selector? I'm not sure psuedo classes should be excluded from the compound selectors? It's a complicated rule though and so a second pair of eyes on this is appreciated.

okay, I got it,
thanks for your attention.

okay, I got it, thanks for your attention.

On further investigation, I think you have uncovered a bug.

According to the specs, _pseudo classes_ are _simple selectors_:

Like other simple selectors, pseudo-classes are allowed in all compound selectors contained in a selector, and must follow the type selector or universal selector, if present. [Emphasis mine]

And a _compound selector_ as:

A compound selector is a sequence of simple selectors that are not separated by a combinator.

As such, I think .c-menu--alt .c-menu__content {} and .c-menu .c-menu__content:focus {} should _not_ be compared.

I'm wondering if this code should only target _pseudo elements_. As per the rule's README:

There's one other important feature: Selectors targeting pseudo-elements are not considered comparable to similar selectors without the pseudo-element, because they target other elements on the rendered page. For example, a::before {} will not be compared to a:hover {}, because a::before targets a pseudo-element whereas a:hover targets the actual .

I'm going to label this as a bug. @monochrome-yeh Feel free to investigate the code further, and, if that confirms my thoughts, please consider contributing a fix.

As such, I think .c-menu--alt .c-menu__content {} and .c-menu .c-menu__content:focus {} should not be compared.

@jeddy3: I don't understand why you've arrived at this conclusion. Could you explain further?

Could you explain further?

Defo. I'm not 100% sure myself. This rule is somewhat tricky.

I think something is amiss. I'm unsure whether it's a bug, a misalignment between the docs and rule behaviour, or, perhaps, an option is needed to allow @monochrome-yeh code style of:

.a .b {}
.a .b:focus {}

.a--alt .b {} /* rules says this should come before `a. .b:focus` */
.a--alt .b:focus {}

This currently warns because:

  1. .b is compared to .b:focus.
  2. .a .b:focus has a specificity of 0,3,0 and .a--alt .b, which follows it, has specificity of 0,2,0.

However, it feels like this is a valid code style because .b is a descendant _with a different parent_. The intent of the rule is to enforce that "Stylesheets are most legible when overriding selectors always come after the selectors they override" - however, in this situation, I think there is no risk of override because of the different parent.

So, it feels like the rule should continue to warn if there is:

  1. no parent
  2. or the parents are the same
  3. or one of the parents is of a different type of selector e.g. _id selector_ (#a) vs _class selector_ (.a)

For example, these should continue to warn:

.a:hover {}
.a {}
.a .b:hover {}
.a .b {}
.a .b {}
#a .b {}

However, if:

  1. there are parent selectors
  2. and there are the same number of parent selectors
  3. and these parent selectors are of the same selector types e.g. _class selectors_
  4. but they are different selectors e.g. .a vs .c

I'm wondering whether it shouldn't warn e.g. this is ok:

.a .b:hover {}
.c .b {}

I think I'm finding the fact that pseudo-classes are treated differently than chained classes a bit confusing. The docs read:

Here's how it works: This rule looks at the last compound selector in every full selector, and then compares it with other selectors in the stylesheet that end in the same way.

However, I think the rule treats .a and a:focus as the same compound selector even though they aren't. It makes sense to do this because it allows the rule to catch a:focus {} a {}, but it feels like it might introduce the, arguable, false positive outlined above.

there are parent selectors
and there are the same number of parent selectors
and these parent selectors are of the same selector types e.g. class selectors
but they are different selectors e.g. .a vs .c

I think this list is right, worth implementing.

I think this list is right, worth implementing.

Good stuff.

@monochrome-yeh This issue is labelled as status: help wanted. Please consider contributing a fix if you'd like to see this issue resolved.

Was this fixed somewhere already?
Have many problems with selectors that shouldn't even be connected, like:

Expected selector ".card.card-simple:hover .card-footer button" to come before selector ".img-split .img-split-el .img-split-cont:hover      no-descending-specificity
           .img-split-btn button"

@trainoasis PR welcome

I found this ticket while trying to solve the same issue. However, I don't think this is a bug. I think the linter is doing exactly what it's supposed to be doing.

As with @trainoasis example above, developers know the rules that they are implementing in the html, however those rules are not represented in the CSS and as far as the linter is concerned it correctly knows that the html _could_ be written so those rules overlap.

Here's an example: https://codepen.io/nosilleg/pen/gdZbda

What I'm after is a way of trying to communicate in the CSS that rules are supposed to be mutually exclusive. I've only just started looking at this, but at this stage I'm not hopeful. While I think it might be possible with a combination of :not() and > selectors, I don't think the resulting CSS or html would be very nice or flexible.

e.g.

div.a > .b {}
div.a--alt:not(.a) > .b {}

Those should never match the same nodes. But as soon as you stop using > classes become unpredictable again.

I think the "right" solution is to make exceptions for this rule in your CSS and comment why you think it will never conflict. e.g. /* .a and .a--alt are mutually exclusive, so this rule will never conflict with '.a--alt .b:focus' */ However, making that exception means that the line will be excluded from matching against things that may actually conflict. So I'm not sure there are any great solutions.

@nosilleg Thank for digging into this and for your clear explanation.

So I'm not sure there are any great solutions.

I suspect the same.

We already caveat at the rule does "the best it can" and I think that might be the best we can do for now.

  &__form {
    padding: 16px;

    form {
      input,
      .select2-container {
        @include grid-span(12);

        margin-left: 0;
        margin-bottom: 16px;

        &:last-of-type {
          margin-bottom: 0;
        }

        @include media($tablet-breakpoint) {
          @include grid-span(6);
          @include grid-flow(2n);
        }

        @include media(940px) {
          @include grid-reflow(1n);
          @include grid-span(3);

          margin-bottom: 0;
        }
      }

      .select2-container {
        height: 51px;

        &:first-of-type {
          margin-left: 0;
        }

        .select2-selection {
          height: 100%;
        }
      }
    }
  }
 48:7  ×  Expected selector ".instant-quote__form form .select2-container" to come before   no-descending-specificity
          selector ".instant-quote__form form .select2-container:last-of-type"

Is this related? I don't see the error I am getting as valid here.

EDIT:

Should add I get exact same error which I would expect to get by putting the selector before the selector it complains about like so:

```

form {
  .select2-container {
    height: 51px;

    &:first-of-type {
      margin-left: 0;
    }

    .select2-selection {
      height: 100%;
    }
  }

  input,
  .select2-container {
    @include grid-span(12);

    margin-left: 0;
    margin-bottom: 16px;

    &:last-of-type {
      margin-bottom: 0;
    }

    @include media($tablet-breakpoint) {
      @include grid-span(6);
      @include grid-flow(2n);
    }

    @include media(940px) {
      @include grid-reflow(1n);
      @include grid-span(3);

      margin-bottom: 0;
    }
  }
}

}```

Is this related?

Yes. It's related to pseudo-classes.

As an aside, the rule was designed for standard CSS e.g.:

a {
  color: red;
}

a:hover {
  color: blue
}

You might not find it valuable for such deeply-nested Scss code.

@stylelint/core This rule is quite problematic and we often get queries about it. I'm wondering whether we should update the documentation to suggest people turn if off if they are writing deeply-nested code.

I think we should fix it, 99% people use nested CSS (by postcss plugin, using SCSS/SASS/LESS and css-in-js supports nested), also this rule very helpfull

I think we should fix it

I'm not sure it can be fixed, though. A lot of the discussions above have been attempts to find a solution, but it doesn't feel like we're any closer. I think we should communicate the limitation of the rule in the documentation as to help manage user expectations. If we do eventually find a solution, we can always remove the caveat from the docs.

Somebody can provide here all examples with current problems and expected results? I can try to find solution

@evilebottnawi If you have any questions about my comment or the codepen, let me know. For me it's pretty clear that this is unfixable due to the limitations of CSS.

https://github.com/stylelint/stylelint/issues/2489#issuecomment-422715538

@gsc89 The issue you are having is compounded by the fact that you have a duplicate rule.

  &__form {
    padding: 16px;

    form {
      input,
      .select2-container {
        ...
      }

      .select2-container {
      ....
      }
    }
  }

Because you have the input included you can not combine the two rules, but doing so would probably allow you to fix your issue. You could possibly define the rules and @extend them into a input{} and then a single .select2-continer{} to get around the issue.

You mention that you get the "exact same error", but I'm guessing the error is different and swapped between first-of-type and last-of-type between your two code blocks.

But in any case, this is a valid lint error because the CSS specificity order is not reflected in the order the code is in.

Maybe it will help you find a way to solution

Another sample with give error:

.navbar-dark {
  .navbar-toggler-icon {
    background: blue;
  }

  &.collapsed {
    .navbar-toggler-icon {
      background: blue;
    }
  }
}

.navbar-light {
  .navbar-toggler-icon {
    background: red;
  }

  &.collapsed {
    .navbar-toggler-icon {
      background: red;
    }
  }
}

When you write same code with this syntax, it will not give an error:

.navbar-toggler-icon {
  .navbar-dark & {
    background: blue;
  }

  .navbar-light & {
    background: red;
  }

  .navbar-dark.collapsed & {
    background: blue;
  }

  .navbar-light.collapsed & {
    background: red;
  }
}

https://discourse.roots.io/t/stylelint-disable-certain-rules-issue-with-no-descending-specificity/12860/10

Any updates about this issue? It throws an error in my styled-components like mentioned -> here

PR welcome

@dawidk92 As discussed above, this isn't a bug, even though the ticket remains open and marked as a bug.

I'll comment on your specific issue on the linked page.

This warning can occur if you are mixing up pseudo-classes (such as :hover) and pseudo-elements (such as ::before). Try changing any :before to ::before etc.

As discussed above, this isn't a bug, even though the ticket remains open and marked as a bug.

What is needed to close this issue? A while back I suggested:

This rule is quite problematic and we often get queries about it. I'm wondering whether we should update the documentation to suggest people turn if off if they are writing deeply-nested code.

Is this a valid solution? The rule is very useful if users write shallow nesting, which arguably users should be doing anyway. We can add a caveat about turning the rule off for those users who don't.

@nosilleg You seem to have a grip on the workings of this rule. Is there a caveat you would add and how would you phrase it?

@jeddy3 I agree.

I've created a PR. I'm limited in my Dev capacity for the next week, but should be able to check email every couple of days.

Here's a fascinating scenario:

@mixin my-table {
  tfoot tr:first-child {
    th, td {
      border-top: 1px solid red;
    }
  }

  &.expand-last-column {
    width: 100%;

    th, td {
      white-space: nowrap;

      &:last-child {
        width: 100%;
      }
    }
  }
}

If you put > tfoot > tr:first-child first, stylelint complains that it expected &.expand-last-column first; but if you put &.expand-last-column first, then stylelint complains it expected > tfoot > tr:first-child first.

[tfoot tr:first-child first]
 140:5   ✖  Expected selector "table.expand-last-column th" to come before selector "table tfoot tr:first-child th"   no-descending-specificity
 140:9   ✖  Expected selector "table.expand-last-column td" to come before selector "table tfoot tr:first-child td"   no-descending-specificity

[&.expand-last-column first]
 145:5   ✖  Expected selector "table tfoot tr:first-child th" to come before selector "table.expand-last-column th:last-child"   no-descending-specificity
 145:9   ✖  Expected selector "table tfoot tr:first-child td" to come before selector "table.expand-last-column td:last-child"   no-descending-specificity

Extremely inconsistent!

@soundasleep

CSS specificity is complex, which is why this rule exists. There is no inconsistency in the errors you are seeing. There are 6 selectors mentioned in your error messages and when considered together the correct specificity order for the 3 th is:

table.expand-last-column th {}
table tfoot tr:first-child th {}
table.expand-last-column th:last-child {}

The same applies for the td selectors.

This can be confirmed with any CSS specificity calculator.

The way that you are nesting the rules makes putting things in specificity order difficult. You need to decide if you want your code to be short, or if you want it to be easier to understand which selectors take priority. In your situation you can't have both.

Is there any changed regarding this?

@The-Code-Monkey

The CSS spec has not changed during this time, so this remains "not a bug". Everything is working as it should with regards to this GitHub issue.

Please see my last comment if you need more details. https://github.com/stylelint/stylelint/issues/2489#issuecomment-518947126

Please also see the documentation for the rule that outlines where confusion can occur. no-descending-specificity#dom-limitations

Was this page helpful?
0 / 5 - 0 ratings