| "use strict"; |
| |
| const _ = require("lodash"); |
| const hasBlock = require("../../utils/hasBlock"); |
| const isStandardSyntaxRule = require("../../utils/isStandardSyntaxRule"); |
| const optionsMatches = require("../../utils/optionsMatches"); |
| const parser = require("postcss-selector-parser"); |
| const report = require("../../utils/report"); |
| const ruleMessages = require("../../utils/ruleMessages"); |
| const validateOptions = require("../../utils/validateOptions"); |
| |
| const ruleName = "max-nesting-depth"; |
| |
| const messages = ruleMessages(ruleName, { |
| expected: depth => `Expected nesting depth to be no more than ${depth}` |
| }); |
| |
| const rule = function(max, options) { |
| const isIgnoreAtRule = node => |
| node.type === "atrule" && |
| optionsMatches(options, "ignoreAtRules", node.name); |
| |
| return (root, result) => { |
| validateOptions( |
| result, |
| ruleName, |
| { |
| actual: max, |
| possible: [_.isNumber] |
| }, |
| { |
| optional: true, |
| actual: options, |
| possible: { |
| ignore: ["blockless-at-rules", "pseudo-classes"], |
| ignoreAtRules: [_.isString, _.isRegExp] |
| } |
| } |
| ); |
| |
| root.walkRules(checkStatement); |
| root.walkAtRules(checkStatement); |
| |
| function checkStatement(statement) { |
| if (isIgnoreAtRule(statement)) { |
| return; |
| } |
| |
| if (!hasBlock(statement)) { |
| return; |
| } |
| |
| if (statement.selector && !isStandardSyntaxRule(statement)) { |
| return; |
| } |
| |
| const depth = nestingDepth(statement); |
| |
| if (depth > max) { |
| report({ |
| ruleName, |
| result, |
| node: statement, |
| message: messages.expected(max) |
| }); |
| } |
| } |
| }; |
| |
| function nestingDepth(node, level) { |
| level = level || 0; |
| const parent = node.parent; |
| |
| if (isIgnoreAtRule(parent)) { |
| return 0; |
| } |
| |
| // The nesting depth level's computation has finished |
| // when this function, recursively called, receives |
| // a node that is not nested -- a direct child of the |
| // root node |
| if ( |
| parent.type === "root" || |
| (parent.type === "atrule" && parent.parent.type === "root") |
| ) { |
| return level; |
| } |
| |
| function containsPseudoClassesOnly(selector) { |
| const normalized = parser().processSync(selector, { lossless: false }); |
| const selectors = normalized.split(","); |
| |
| return selectors.every( |
| selector => selector.startsWith("&:") && selector[2] !== ":" |
| ); |
| } |
| |
| if ( |
| (optionsMatches(options, "ignore", "blockless-at-rules") && |
| node.type === "atrule" && |
| node.every(child => child.type !== "decl")) || |
| (optionsMatches(options, "ignore", "pseudo-classes") && |
| node.type === "rule" && |
| containsPseudoClassesOnly(node.selector)) |
| ) { |
| return nestingDepth(parent, level); |
| } |
| |
| // Unless any of the conditions above apply, we want to |
| // add 1 to the nesting depth level and then check the parent, |
| // continuing to add and move up the hierarchy |
| // until we hit the root node |
| return nestingDepth(parent, level + 1); |
| } |
| }; |
| |
| rule.ruleName = ruleName; |
| rule.messages = messages; |
| module.exports = rule; |