stylelint: stylelint-declaration-block-order

Stylelint doesn’t support linting rules structure. I want to add this feature using PostCSS Sorting. It will require adding linting functionality in PostCSS Sorting, which I’ll add when things got clearer 😃 Also when stylelint can check rule structure, it can be great to integrate PostCSS Sorting in stylefmt.

  1. Should I add this functionality as a rule or plugin? If it will be as a plugin, can it be used in stylefmt? Stylefmt support only stylelint’s config. /cc @morishitter
  2. What name rule or plugin should have? As a plugin name @jeddy3 suggested stylelint-block-structure.
  3. What to do with tests? PostCSS Sorting has a lot of tests. Should all they be copied to rule/plugin?
  4. What to do with options description? Options description is a long document. Should I maintain it in both places (postcss-sorting repository and stylelint/plugin repository)?
  5. I have no idea what primary option should be for this rule.

P. S. Nice issue number 😃

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 2
  • Comments: 52 (52 by maintainers)

Most upvoted comments

Yes, I understand that it should concern only about an order. And now you open my eyes about existing rules for empty lines 😃

Fantastic. I’m glad we’re on the same page now.

Can you explain, please, what atRuleBlockContent will be for?

Scratch that property. I misunderstood @bjankord’s requirements. I don’t use SCSS and so I hadn’t fully grokked some of the terms used in “specifically checking the order of @include declarations without inner @content, properties, @include declarations with inner @content”. I now think that the atRuleHasBlock property is all that’s needed to achieve this e.g.

{
  "plugins": {
    "block-non-properties-order": "stylelint-block-non-properties-order"
  },
  "rules": {
    "plugin/block-non-properties-order": [
      "dollar-variables",  // predefined keyword for dollar variables
      {
        atRuleName: "extend", // user-defined at-rule with a specific name
      },
      {
        atRuleName: "include", // user-defined at-rule with a specific name without a block
        atRuleHasBlock: false
      },
      {
        atRuleName: "include", // user-defined at-rule with a specific name with a block
        atRuleHasBlock: true
      },
      "declarations", // predefined keyword for declarations 
      "nested-rules",  // predefined keyword for nested rules
      "at-rules" // predefined keyword for non-specific at-rules
     ]
   }
}

Will allow this:

a {
  $var: 10px;
  @extend silly-links;
  @include larger-text;
  @include colors { color: red; }
}

But warn for this:

a {
  $var: 10px;
  @extend silly-links;
  @include colors { color: red; }
  @include larger-text;
}
5:2 Expected blockless @include to come before @include with block (plugin/block-non-properties-order)

Damn, you’re good! I didn’t think about that, and it’s definitely will be the case when browsers will have better custom properties support.

This is why every stylelint rule is scoped to the smallest and most specific thing possible e.g. number (number-max-precision) / unit (unit-blacklist) → <length> type (length-zero-no-unit) → value (value-keyword-case) / property (property-no-vendor-prefix) → declaration (declaration-no-important) → rule (rule-nested-empty-line-before). It keeps things flexible and future proof.

I finally started to work on this plugin 😃 I have a problem with development, though. I want to use Chrome Dev Tools to debug my plugin (node --inspect run.js in Node 6.3.0+), because console.log() isn’t enough. But I don’t know how to teach stylelint that current folder is a plugin and use it. I have this run.js:

const stylelint = require('stylelint');

const config = {
    plugins: {
        'declaration-block-order': 'stylelint-declaration-block-order'
    },
    rules: {
        'plugin/declaration-block-order': [
            'custom-properties',
            'dollar-variables',
            'declarations',
            'rules',
            'at-rules',
        ],
    },
};

stylelint.lint({
    code: `
        a {
            display: none;
            --width: 10px;
        }
    `,
    config,
}).then(function (data) {
    console.log(data);
}, function (err) {
    console.log(err);
});

P. S. Developing stylelint plugin is not a straightforward task 😥

I summed up config with all options plugin will have. Do I miss something?

{
  "plugins": {
    "block-non-properties-order": "stylelint-block-non-properties-order"
  },
  "rules": {
    "plugin/block-non-properties-order": [
      "custom-properties", // predefined keyword for custom properties
      "dollar-variables",  // predefined keyword for dollar variables
      "declarations", // predefined keyword for declarations
      "rules",  // predefined keyword for rules
      "at-rules", // predefined keyword for non-specific at-rules
      {
        type: "atRule",
        name: "nest", // user-defined at-rule with a specific name
      },
      {
        type: "atRule",
        name: "include", // user-defined at-rule with a specific name without a block
        hasBlock: false
      },
      {
        type: "atRule",
        name: "include", // user-defined at-rule with a specific name with a block
        hasBlock: true
      },
      {
        type: "rule", // user-defined rule with specific selector regex
        selector: "/^&:\w/" // in this case matching pseudo-classes
      },
      {
        type: "rule", // user-defined rule with specific selector regex
        selector: "/^&::\w/" // in this case matching pseudo-elements
      }
    ]
  }
}

Can you think of any other useful ways that this object could be extended?

While this plugin is on my mind, I’ll answer my own question and throw a suggestion out there.

I like to order my nested rules and nested media queries in a specific order. I believe this helps to keep things findable and scannable for larger teams, especially when there are many nested rules within a block. We use the following order:

  1. pseudo-classes e.g. &:hover, and &:not()
  2. pseudo-elements e.g. &::before and &::after
  3. directly nested descendent rules e.g. & .a {}
  4. at-nest nested rules e.g. @nest .a & {}
  5. at-media rules @media (width < 100px) {}

For example:

a {
  color: red;
  display: flex;

  &:hover {
    color: blue;
  }

  &::before {
    content: "x";
  }

  & .b {
    flex: 1;
  }

  @nest .c & {
    display: none;
  }

  @media (width < 10em) {
    a {
      justify-content: center;
    }
  }
}

The object within the config array could be expanded to accommodate this, as so: (I’ve kept the extends and include at-rules in for illustrative purposes only, as they are not something I would normally use)

{
  "plugins": {
    "block-non-properties-order": "stylelint-block-non-properties-order"
  },
  "rules": {
    "plugin/block-non-properties-order": [
      {
        type: "atRule"
        name: "extend", // user-defined at-rule with a specific name
      },
      {
        type: "atRule"
        name: "include", // user-defined at-rule with a specific name without a block
        hasBlock: false
      },
      {
        type: "atRule"
        name: "include", // user-defined at-rule with a specific name with a block
        hasBlock: true
      },
      "declarations", // predefined keyword for declarations
      {
        type: "rule", // user-defined rule with specific selector regex
        selector: "/^&:\d/" // in this case matching pseudo-classes
      }, {
        type: "rule", // user-defined rule with specific selector regex
        selector: "/^&::\d/" // in this case matching pseudo-elements
      },
      "rules",  // predefined keyword for rules (used here for directly nested descendent rules)
      {
        type: "atRule"
        name: "nest", // user-defined at-rule with a specific name
      },
      "at-rules" // predefined keyword for non-specific at-rules (used here for @media)
     ]
   }
}

Incidentally, this is a great illustration of why this will be community plugin rather than a core rule i.e. it doesn’t have a “clear and unambiguous finished state”.

One of the advantages of being a plugin is that you can iterate often and release breaking changes to the config syntax. So, it’s probably best to release something that works for at-rules first and maybe tackle the above, more involved, use-case at a later date.

Do you feel that you now have enough information to make a start on this plugin? I’m keen to see how you get on, and so would you mind posting a link to your repo once you’ve something to show? There is a “Writing plugins” section of the developer guide that you might find useful. In terms of reference material, the stylelint-scss team is doing a great job and any of their plugins would make a great blueprint. In particular, there is their at-mixin-pattern plugin, which has a great set of tests that use various configurations and syntax options. And, of course, there is also the 7.0.0 version of declaration-block-properties-order for reference.

Does anyone else have any feedback on the proposal or shall we get the ball rolling?

At some point there will be necessity to check if there is an empty line before some “entities”.

My broader proposal above hinges upon the assumption that this isn’t the case. There are already individual and laser-focused rules for linting empty lines before things. The addition of the emptyLineBefore option to declaration-block-properties-order was a mistake, and why it is being removed in 7.0.0. This is to ensure that the declaration-block-properties-order rule, like all the other rules, has a singular purpose (whether that’s linting for an empty line, or checking the order of things). This is one of the core design philosophies of stylelint, and one of the main reasons why there are no overlaps between the rules, despite there being over 140 of them.

In my original proposal, I showed an example config that illustrated how all these individual whitespace and empty line rules work together. For clarity, I’ll focus on just the empty lines rules below:

{
  "rules": {
    "at-rule-empty-line-before": [ "always", {
      "except": [ "blockless-group", "first-nested" ],
      "ignore": ["after-comment"],
    } ],
    "comment-empty-line-before": [ "always", {
      "except": ["first-nested"],
      "ignore": ["stylelint-commands"],
    } ],
    "function-max-empty-lines": 0,
    "max-empty-lines": 1,
    "rule-nested-empty-line-before": [ "always-multi-line", {
      "ignore": ["after-comment"],
    } ],
    "selector-max-empty-lines": 0,
  }
}

I believe these laser-focused rules, with the help of their secondary options, already work well together to lint empty lines within blocks. In my original proposal I mentioned that there are a couple of holes to fill, and I’ve created separate issues for those:

Hopefully the above explanation has made it a little clearer why I believe this new plugin should only be focused on the order of non-properties within a block, and not concerned with whitespace or empty lines?

Maybe this option should accept objects too

I think you’re on to something with accepting objects though, but for a different reason than controlling whitespace. I think you’re right that objects are more extensible, which will be especially useful for the user-defined at-rules. For example:

{
  "plugins": {
    "block-non-properties-order": "stylelint-block-non-properties-order"
  },
  "rules": {
    "plugin/block-non-properties-order": [
      "custom-properties", // predefined keyword for custom properties
      "dollar-variables",  // predefined keyword for dollar variables
      {
        atRuleName: "apply", // user-defined at-rule with a specific name
      },
      {
        atRuleName: "mixin", // user-defined at-rule with a specific name and parameter
        atRuleParameter: "clearfix"
      },
      "declarations", // predefined keyword for declarations 
      "nested-rules",  // predefined keyword for nested rules
      "at-rules" // predefined keyword for non-specific at-rules
     ]
   }
}

I like this approach as:

  1. It clearly distinguishes between predefined keywords (e.g. "at-rules", "nested-rules", "declarations", "custom-properties", "dollar-variables" etc.) and user-defined at-rules.
  2. It’s extensible for the user-defined at-rules.

Off hand, I can think of the following ways to extend that object:

{
  atRuleName: "mixin", // user-defined at-rule with a specific name and parameter
  atRuleParameter: "/^clear/", // accept regex as well as strings
  atRuleBlockContent: "/@content/", // regex for the contents of the at-rule's block
  atRuleHasBlock: true // bool for whether the at-rule has a block or not
}

I believe this makes achieving @bjankord’s requirements viable.

Can you think of any other useful ways that this object could be extended?

Also instead of dollar-variables it’s better to use variables.

In the example above I’ve used two separate keywords, "custom-properties" and "dollar-variables", as this is in-keeping with how we categorise things. I believe this fine-grain categorisation is more future proof e.g. it’s possible to use custom properties and dollar variables side-by-side i.e. dollar variables for build-time variables, and custom properties for computed-time variables.

P. S. I want to make this plugin. Please, don’t take this opportunity from me 😃

Don’t worry, I have no intention of writing or maintaining this plugin 😃 My interest here is to ensure that the plugin works together with the other rules and that overlapping functionality is avoided.

what is your proposal for stylelint-block-non-properties-order? What should it do? What options should it have?

The gist of stylelint-block-non-properties-order is outlined in my comment above. However, it’s interwoven into a larger proposal about how rules work together. For everyone’s sanity, including my own, I’ll pull out and extend on the stylelint-block-non-properties-order stuff below 😃

The first thing to keep in mind is that we still need to finalise the name and exact options. What is decided on is the scope of the plugin and how the plugin will work together with the existing rules and with these two new rules: https://github.com/stylelint/stylelint/issues/1574 & https://github.com/stylelint/stylelint/issues/1573. How and why this works is covered in my comment above and by examples I gave.

OK, so my suggestion for the general outline of this plugin is:

  • Name: stylelint-block-non-properties-order
  • Primary option: ["array", "of", "predefined-keywords", "and", "userdefined-at-rule-names"]
  • Secondary options: none
  • Message: Expected "${first}" to come before "${second}"

Its singular purpose is to lint the order of non-properties (excluding comments) within a block. It is not concerned with any whitespace nor the order of properties.

The inspiration (and internals) for the keywords within the primary object array could be taken from PostCSS sorting’s at-rule, nested rule and variable options.

An example rule config using all of these keywords could be:

{
  "plugins": {
    "block-non-properties-order": "stylelint-block-non-properties-order"
  },
  "rules": {
    "plugin/block-non-properties-order": [
      "dollar-variables",  // predefined keyword for dollar variables i.e. "$variables" from PostCSS Sorting
      "@apply", // user-defined at-rule with a specific name
      "@mixin clearfix", // user-defined at-rule with a specific name and parameter
      "declarations", // predefined keyword for declarations* 
      "nested-rules",  // predefined keyword for nested rules i.e. ">child" from PostCSS Sorting
      "at-rules" // predefined keyword for non-specific at-rules i.e. "@atrule" from PostCSS Sorting
     ]
   }
}

*To reiterate this plugin isn’t concerned with the order of properties/declarations. Only that the declarations appear at the right place within the block.

And again, the keywords within the primary option array are just suggestions. We need to ask ourselves whether they allow people enough control to specify exactly the order of non-properties they want? e.g. does it allow for this?

Some examples, given the config above…

a {
  @media (width < 10px) {
     b { color :red }
  }
  display: block;
}

Would yield:

5:2 Expected declaration to come before at-rule (plugin/block-non-properties-order)

And:

a {
  display: block;
  @media (width < 10px) {
    b { color :red }
  }

  &:hover {
    display: inline;
  }
}

Would yield:

7:2 Expected nested rule to come before at-rule (plugin/block-non-properties-order)

@bjankord Thank you for your input. I especially appreciate that you looked through the spider’s web of historical issues to get a grip on this 😃

As you can see there are a number of use-cases to consider and fair few competing interests. I think I have a proposal that might be satisfactory to all parties, and I’ll try to outline that below.

My comment is simply consider making two plugins, one concerned with ordering (including ordering of non-property nested statements) and one to handle line spacing concerns…[vs] 1 plugin that does both the ordering of properties / nested statements like @extends, @includes, @media etc. and the line space linting sounds good to me too.

I believe this to be the crux of the issue. We’ve been very careful in the past to create very focused rules who’s functionality does not overlap. Currently we already have one sorting rule (for properties), a number of non-overlapping empty line rules and number of non-overlapping newline rules in core. My new proposal is that, where possible, any new plugin complements these existing rules, rather than overlaps them.

I think the following example and config will help us all understand what can be linted with the existing rules, and where there are holes to fill:

/* cssnext example */
a {
  @apply: --my-custom-property-set;
  @apply: --my-other-custom-property-set;

  color: red;
  display: block;

  /* inline comment */
  float: right;

  &:hover,
  &:focus {
    background-size: 
      0,
      0;
    color: blue;
  }

  /* inline comment */
  @nest .class & { 
     color: green;
     transform: translate(
       1,
       1
     );
  }

  @nest .other-class & { 
     color: yellow;
  }

  @media (width < 10em) {
     & {
       color: blue;
     }
  }
}

And with the following abbreviated config:

{
  "rules": {
    "at-rule-empty-line-before": [ "always", {
      "except": [ "blockless-group", "first-nested" ],
      "ignore": ["after-comment"],
    } ],
    "at-rule-semicolon-newline-after": "always",
    "block-closing-brace-newline-after": "always",
    "block-closing-brace-newline-before": "always-multi-line",
    "block-opening-brace-newline-after": "always-multi-line",
    "block-opening-brace-space-before": "always",
    "comment-empty-line-before": [ "always", {
      "except": ["first-nested"],
      "ignore": ["stylelint-commands"],
    } ],
    "declaration-block-properties-order": [
      "background-size",
      "color",
      "display",
      "float",
      "transform"
    ],
    "declaration-block-semicolon-newline-after": "always-multi-line",
    "function-comma-newline-after": "always-multi-line",
    "function-max-empty-lines": 0,
    "max-empty-lines": 1,
    "media-query-list-comma-newline-after": "always-multi-line",
    "rule-nested-empty-line-before": [ "always-multi-line", {
      "ignore": ["after-comment"],
    } ],
    "selector-list-comma-newline-after": "always",
    "selector-max-empty-lines": 0,
    "value-list-comma-newline-after": "always-multi-line",
  }
}

As you can see, we can already lint the vast majority of newlines and empty lines, including:

  • Allowing and disallowing empty lines before different types of statements, including options for dealing with blockless at-rules and preceding comments.
  • Limiting the number of empty lines, including within selectors and functions.

And we can lint the order of properties.

So, I think we are only lacking three features:

  1. Linting the empty line before the closing brace of a block (stackoverflow question).
  2. Linting the order of non-properties.
  3. Linting the empty lines before declarations (and, in a more advanced use-case, before property groups).

I think we can simply address the first one with a new block-closing-brace-empty-line-before: "always"|"never" rule. This rule would only check multi-line blocks and check whether the closing brace is preceded by an empty line.

I think the second feature is the responsibility of plugin called stylelint-block-non-properties-order: [] (and the focus of this issue). The inspiration (and internals) for which could be taken from PostCSS sorting’s at-rule, nested rule and variable options. This plugin would not be concerned with any whitespace nor the order of properties. An example rule config for the above CSS example could be:

{
  "plugins": {
    "block-non-properties-order": "stylelint-block-non-properties-order"
  },
  "rules": {
    "plugin/block-non-properties-order": [
      "@apply",
      "declarations",
      "nested-rules",
      "@nest",
      "@media"
     ]
   }
}

The above example shows the use of pre-defined keywords (like "declarations" and "nested-rules") as well as user-defined at-rule names (like "@apply", "@nest" and "@media").

Lastly, I think we can address the simpler use-case of the third feature in core with a new declaration-empty-line-before": [ "always"|"never", { "except": ["first-nested"], "ignore": ["after-comment"] } ] rule (ref).

Whereas, I think linting empty lines before property groups should be a plugin as there are all sorts of edge-cases in and around this feature. One option could be a plugin (perhaps called stylelint-property-groups-structure) that builds on top of declaration-block-properties-order. This kind of mixes the concerns of empty lines with ordering, but I can’t see away around this in this very specific situation. I’m OK with the mixing of concerns in community-land, but only if it’s absolutely necessary.

I’m just trying to emphasize the need for the ordering of properties / nested statements like @extends, @includes, @media etc. functionality.

I realise that this proposal is quite a radical departure from my original suggestion of a stylelint-block-structure plugin, but having taken a step back from the problem these last few days I feel confident that this proposal is the way forward. If we’re happy to go down the route I proposed above, then I’ll create separate issues for the block-closing-brace-empty-line-before and declaration-empty-line-before rules, and we can keep this issue focused on the ordering of non-properties within a block.

@hudochenkov Thanks for kickstarting the discussion about this! I’ll try to answer each of your questions.

Should I add this functionality as a rule or plugin?

Definitely a plugin. We had some recent discussions about the sustainability of development. These accumulated into the decision to remove some features in 7.0, including from the existing declaration-block-properties-order rule, and push a number of proposed rules and options to plugins.

What name rule or plugin should have?

Each rule/plugin follows these naming conventions. block is the thing that this rule is scoped to. I chose structure because it’s unique in stylelint as no other plugin/rule deals with both whitespace and the order of things. Perhaps there’s a better word for it though?

What to do with tests? What to do with options description? I have no idea what primary option should be for this rule.

We can look to the existing plugin stylelint-selector-bem-pattern for precedence here i.e. “just a couple of quick tests to ensure postcss-bem-linter is getting the hard work done”, docs that say look at the postcss-bem-linter docs for options, and a primary option that is a object containing what options postcss-bem-linter expects.

Right, I think that’s probably enough to keep the ball rolling. Anyone else have anything to add?

P. S. Nice issue number 😃

Agreed 😃

@hudochenkov What do you think about the name declaration-block-order? I now think non-properties is an odd thing to include in the name as other things might also be excluded if specific rules are written in the future e.g. declaration-block-custom-properties-order or scss/declaration-block-dollar-variables-order.