3 const _ = require("lodash");
4 const isStandardSyntaxRule = require("../../utils/isStandardSyntaxRule");
5 const isStandardSyntaxSelector = require("../../utils/isStandardSyntaxSelector");
6 const optionsMatches = require("../../utils/optionsMatches");
7 const parseSelector = require("../../utils/parseSelector");
8 const report = require("../../utils/report");
9 const resolvedNestedSelector = require("postcss-resolve-nested-selector");
10 const ruleMessages = require("../../utils/ruleMessages");
11 const specificity = require("specificity");
12 const validateOptions = require("../../utils/validateOptions");
14 const ruleName = "selector-max-specificity";
16 const messages = ruleMessages(ruleName, {
17 expected: (selector, specificity) =>
18 `Expected "${selector}" to have a specificity no more than "${specificity}"`
21 // Return an array representation of zero specificity. We need a new array each time so that it can mutated
22 const zeroSpecificity = () => [0, 0, 0, 0];
24 // Calculate the sum of given array of specificity arrays
25 const specificitySum = specificities => {
26 const sum = zeroSpecificity();
27 specificities.forEach(specificityArray => {
28 specificityArray.forEach((value, i) => {
35 const rule = function(max, options) {
36 return (root, result) => {
37 const validOptions = validateOptions(
44 // Check that the max specificity is in the form "a,b,c"
45 const pattern = new RegExp("^\\d+,\\d+,\\d+$");
46 return pattern.test(max);
53 ignoreSelectors: [_.isString]
62 // Calculate the specificity of a simple selector (type, attribute, class, id, or pseudos's own value)
63 const simpleSpecificity = selector => {
64 if (optionsMatches(options, "ignoreSelectors", selector)) {
65 return zeroSpecificity();
67 return specificity.calculate(selector)[0].specificityArray;
70 // Calculate the the specificity of the most specific direct child
71 const maxChildSpecificity = node =>
72 node.reduce((max, child) => {
73 const childSpecificity = nodeSpecificity(child); // eslint-disable-line no-use-before-define
74 return specificity.compare(childSpecificity, max) === 1
77 }, zeroSpecificity());
79 // Calculate the specificity of a pseudo selector including own value and children
80 const pseudoSpecificity = node => {
81 // `node.toString()` includes children which should be processed separately,
82 // so use `node.value` instead
83 const ownValue = node.value;
84 const ownSpecificity =
85 ownValue === ":not" || ownValue === ":matches"
86 ? // :not and :matches don't add specificity themselves, but their children do
88 : simpleSpecificity(ownValue);
90 return specificitySum([ownSpecificity, maxChildSpecificity(node)]);
93 // Calculate the specificity of a node parsed by `postcss-selector-parser`
94 const nodeSpecificity = node => {
100 return simpleSpecificity(node.toString());
102 return pseudoSpecificity(node);
104 // Calculate the sum of all the direct children
105 return specificitySum(node.map(nodeSpecificity));
107 return zeroSpecificity();
111 const maxSpecificityArray = ("0," + max).split(",").map(parseFloat);
112 root.walkRules(rule => {
113 if (!isStandardSyntaxRule(rule)) {
116 if (!isStandardSyntaxSelector(rule.selector)) {
119 // Using rule.selectors gets us each selector in the eventuality we have a comma separated set
120 rule.selectors.forEach(selector => {
121 resolvedNestedSelector(selector, rule).forEach(resolvedSelector => {
123 // Skip non-standard syntax selectors
124 if (!isStandardSyntaxSelector(resolvedSelector)) {
127 parseSelector(resolvedSelector, result, rule, selectorTree => {
128 // Check if the selector specificity exceeds the allowed maximum
131 maxChildSpecificity(selectorTree),
139 message: messages.expected(resolvedSelector, max),
145 result.warn("Cannot parse selector", {
147 stylelintType: "parseError"
156 rule.ruleName = ruleName;
157 rule.messages = messages;
158 module.exports = rule;