postcss / postcss-bem-linter

A BEM linter for postcss
MIT License
572 stars 35 forks source link

Validate nested rulesets #85

Closed simonsmith closed 8 years ago

simonsmith commented 8 years ago

A first stab at #77

It only supports one level of nesting, which seems ideal for pseudo classes and such. It does mean things like this will fail though:

.Component {
  &--modifier {
    .Component-element {}
}

Not sure if going any further sends the bem linter into postcss-nested territory but if a nested feature lands in the bem linter with caveats it might not be as useful. Open to thoughts on that.

@dryoma @giuseppeg

Happy to add documentation once this is ready

giuseppeg commented 8 years ago

Cool Simon! Before I even take a look at the code I was thinking that maybe we shouldn't allow modifers nesting. Modifiers should be explicit.

/*
 A modifier creates something like a "context" where
 only -children, states etc are allowed.
*/
.Component--modifier { 
  &.is-state {}

  & .Component-child { /* should & be optional here? */
     &.is-active {}
     &:hover {}
     &[attr] {}
  }
}

This may be not trivial to implement but aligns well with the suitcss methodology imho.

dryoma commented 8 years ago

Modifiers should be explicit.

I agree.

As for postcss-nested I am a bit sceptical. stylelint authors recommend to lint code before applying any plugins. I should say, for a reason. I think it's giving way to errors added by those plugins (if they have errors). As an example, just now tried to lint this:

/** @define MyComponent */

.ns-MyComponent {
  display: inline; // the semicolon is lost somewhere between postcss-nested and stylelint

  &.is-open {
    display: none;
  }

  @media (max-width: 1000px) {
    display: table;
  }
}

with

...
  .pipe(gulpPostCSS([
      nested(),
      stylelint(),
      reporter({
        clearMessages: true,
        throwError: true
      })
    ], {
      syntax: scssSyntax
    }))

and it falsely found an error: 4:20 ‼ Expected a trailing semicolon (declaration-block-trailing-semicolon) [stylelint] If I remove the @media rule, the error goes away.

simonsmith commented 8 years ago

Modifiers should be explicit.

Let me see if I understand by example:

.Component {
  color: red;

  &.is-stateName {}
  &-descendant {}
  &:pseudo-class {}
  &:pseudo-element {}
  &::pseudo-element {}
  &[attr] {}
}

/* modifier needs to start a new block? */

.Component--modifier {
  .Component-descendant {}
  &.is-stateName {}
  &:pseudo-class {}
}

/* How about modifiers on descendants? */

.Component {
  &-descendant--modifier {}
}

I also wonder if this whole thing should be recursive, or whether we should allow one level of nesting? It gets complex the more you think about it :)

simonsmith commented 8 years ago

As for postcss-nested I am a bit sceptical

Agreed, it's not ideal. I was just wondering if bem-linter should be implementing almost that whole plugin as once you support nesting users may try to put @media blocks in rulesets a la Sass for example.

Sounds like we would be aiming to allow nesting with specific rules though.

dryoma commented 8 years ago

I get it that it's a PostCSS, not PostSass plugin. Still, I think that it is quite natural for a user to expect postcss-bem-linter to support nesting as well, since it can be used as a plugin to stylelint or alongside it, and stylelint can support SCSS syntax via postcss-scss (and it's especially great it works with nesting) and doesn't need any specific preprocessing plugins for this (like postcss-nesting in question).

Otherwise it would require a separate pipelilne for linting selectors. Not to mention the plugins may cause errors or be in some other way inacurate themselves.

simonsmith commented 8 years ago

@dryoma Agreed yes, but I also wonder if bem-linter should take this chance to lay down some rules and stop the nested hell you sometimes see in Sass and LESS

davidtheclark commented 8 years ago

I think using postcss-resolve-nested-selector might solve some of the complexity concern.

I also think that people who want to restrict nesting depth could use stylelint. I think it's nice to keep this particular plugin focused solely on selectors.

simonsmith commented 8 years ago

@davidtheclark Ah nice! Exactly what I needed. I'll look at implementing that and leave the depth concern to stylelint as you mentioned

dryoma commented 8 years ago

I also think that people who want to restrict nesting depth could use stylelint.

I agree, it would be sensible to let stylelint worry about nesting (which it does perfectly, even with SCSS).

giuseppeg commented 8 years ago

@simonsmith are you just unwrapping and validating the nested rules then? If so I think that the branch is ok!

By the way are you familiar with the CSS Nesting Module Level 3 proposal by Tab Atkins? Would it make sense to try to add support for this too? I mean we are introducing new rules and if we want to move away from them or remove some in the future that'd require a breaking change.

simonsmith commented 8 years ago

@giuseppeg Yeah, I'm thinking do the minimum here which is just unwrapping all the selectors and validating them. Changes to those rules can happen outside this PR.

I'm currently refactoring to use postcss-resolve-nested-selector (via @davidtheclark) which supports postcss-nested (Sass) and postcss-nesting (CSS3) so that should cover it.

Will push some changes for review shortly.

simonsmith commented 8 years ago

Okay, pushed some changes.

The idea now is to detect if a rule is nested and if so, traverse up to the component root rule and construct the selector from there down to the current rule. The reason for this is so we always have the correct rule to output as the warning if linting fails.

See what you think

simonsmith commented 8 years ago

@davidtheclark Look okay to you?

davidtheclark commented 8 years ago

Just had a couple of comments I added.

simonsmith commented 8 years ago

I've pushed some more changes and added some more tests. Main change was to move the logic into a getSelectors function so it could be isolated in the tests. I'll post my commit message as an explanation:

This allows certainty that selectors are being unwrapped as expected. By
following the 'black box' style used in other tests it can lead to false
positives. For example:

.Component {
  .Component-descendant {
    color: green;
  }
}

In the above code what should be linted is
`.Component .Component-descendant` but it's quite possible due to an
error that just `.Component` is tested. If this happens it will still
pass the black box test as `.Component` is valid. This can lead to hard
to find bugs with unwrapping nesting selectors.

The extra tests give it a bit more confidence that this is working as expected. Have also added some CSS Nesting Module fixtures.

davidtheclark commented 8 years ago

Looks good! Just that one comment. Anybody object to merging after it's addressed?

giuseppeg commented 8 years ago

Looks good to me, feel free to ignore my last round of comments if you want ;)

davidtheclark commented 8 years ago

Thanks all. I'm fine with merging, so ping me when you think it's ready @simonsmith.

simonsmith commented 8 years ago

@davidtheclark I'm happy with this now. Commits are pretty tidy. Need anything added to the README.md?

davidtheclark commented 8 years ago

I think it's fine. Released in 2.5.0! Thanks again everybody!

simonsmith commented 8 years ago

🎉

dryoma commented 8 years ago

Great, thanks @simonsmith !

marcustisater commented 8 years ago

Wohoo! Great work @simonsmith! 🎉 😄

brianmcallister commented 7 years ago

Little late to the party, but this doesn't appear to be working for me in v2.6.0. I am using stylelint-selector-bem-pattern, but I've installed 2.6.0 of this package specifically. If you think this is more of a problem with the stylelint plugin, I'd be happy to post this over there.

Here's my Sass file:

_test.scss

/* postcss-bem-linter: define test */
.test {
  &--red {
    color: rgba(255, 0 0);
  }

  .child {
    color: #ffffff;
  }
}

and here's my config:

module.exports = {
  plugins: [
    'stylelint-selector-bem-pattern'
  ],
  rules: {
    'plugin/selector-bem-pattern': {
      componentName: /[-a-z]+$/,
      componentSelectors: '^\\.{componentName}(?:[-_a-z]+)?$'
    },
  }
};

and my output:

src/stylesheets/modules/_test.scss
 7:3  ✖  Invalid component selector ".test .child"   plugin/selector-bem-pattern

Am I just doing something wrong? I started with a super simple regex for now, just to validate that this works.

simonsmith commented 7 years ago

Hey @brianmcallister,

Do you mind adding that as a separate issue so I don't lose track of it. Thanks

brianmcallister commented 7 years ago

Yep! No problem.