sassstylelint

Is it possible in stylelint to limit nesting within media queries


I've seem max-nesting-depth but I don't think it will work for my use case.

Here's some examples of valid and invalid code:

.classname {
  color: red; /* fine */

  .second-class {
    color: blue; /* also fine */

    .third-class {
     color: green; /* also fine */
    }
  }

  @media (min-width: 100px) {
    color: blue; /* fine */
  }

  @media (min-width: 200px) {
    color: green; /* fine */

    .second-class { // <-- prevent this
      background-color: blue; /* NOT fine */

      .third-class {
        background-color: blue; /* also NOT fine */
      }
    }
  }
}

We have some code examples in our projects which mix a top level media query with nested selectors and media queries nested deep in selectors and I want to lint it so the latter is preferred.


Solution

  • You can use the stylelint-no-restricted-syntax Stylelint plugin for this specific use case, as Stylelint's built-in rules are designed for more general patterns. The plugin enables you to query the AST (the code's structure) to disallow specific patterns:

    {
      "extends": ["stylelint-config-standard"],
      "plugins": ["stylelint-no-restricted-syntax"],
      "rules": {
        "plugin/no-restricted-syntax": [
          [
            {
              "selector": "atrule rule",
              "message": "Unexpected rule inside of at-rule"
            }
          ]
        ]
      }
    }
    

    This will disallow any rules that are descendants of at-rules. Demo.

    Alternatively, and if you want more control, you can write a Stylelint plugin to do the same thing:

    import stylelint from "stylelint";
    
    const {
      createPlugin,
      utils: { report, ruleMessages, validateOptions },
    } = stylelint;
    
    const ruleName = "plugin/no-rules-inside-atrule";
    
    const messages = ruleMessages(ruleName, {
      rejected: () => "Unexpected rule inside at-rule",
    });
    
    /** @type {import('stylelint').Rule} */
    const ruleFunction = (primary) => {
      return (root, result) => {
        const validOptions = validateOptions(result, ruleName, {
          actual: primary,
          possible: [true],
        });
        if (!validOptions) return;
        root.walkAtRules((atRule) => {
          atRule.walkRules((ruleNode) => {
            report({
              message: messages.rejected(),
              node: ruleNode,
              result,
              ruleName,
            });
          });
        });
      };
    };
    
    ruleFunction.ruleName = ruleName;
    ruleFunction.messages = messages;
    
    export default createPlugin(ruleName, ruleFunction);
    
    /** @type {import('stylelint').Config} */
    export default {
      extends: ["stylelint-config-standard"],
      plugins: ["./no-rules-inside-atrule.mjs"],
      rules: {
        "plugin/no-rules-inside-atrule": true,
      },
    };