index.js 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. 'use strict';
  2. const isContextFunctionalPseudoClass = require('../../utils/isContextFunctionalPseudoClass');
  3. const isNonNegativeInteger = require('../../utils/isNonNegativeInteger');
  4. const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
  5. const optionsMatches = require('../../utils/optionsMatches');
  6. const parseSelector = require('../../utils/parseSelector');
  7. const report = require('../../utils/report');
  8. const resolvedNestedSelector = require('postcss-resolve-nested-selector');
  9. const ruleMessages = require('../../utils/ruleMessages');
  10. const validateOptions = require('../../utils/validateOptions');
  11. const { isRegExp, isString } = require('../../utils/validateTypes');
  12. const ruleName = 'selector-max-attribute';
  13. const messages = ruleMessages(ruleName, {
  14. expected: (selector, max) =>
  15. `Expected "${selector}" to have no more than ${max} attribute ${
  16. max === 1 ? 'selector' : 'selectors'
  17. }`,
  18. });
  19. const meta = {
  20. url: 'https://stylelint.io/user-guide/rules/list/selector-max-attribute',
  21. };
  22. /** @type {import('stylelint').Rule} */
  23. const rule = (primary, secondaryOptions) => {
  24. return (root, result) => {
  25. const validOptions = validateOptions(
  26. result,
  27. ruleName,
  28. {
  29. actual: primary,
  30. possible: isNonNegativeInteger,
  31. },
  32. {
  33. actual: secondaryOptions,
  34. possible: {
  35. ignoreAttributes: [isString, isRegExp],
  36. },
  37. optional: true,
  38. },
  39. );
  40. if (!validOptions) {
  41. return;
  42. }
  43. /**
  44. * @param {import('postcss-selector-parser').Container<unknown>} selectorNode
  45. * @param {import('postcss').Rule} ruleNode
  46. */
  47. function checkSelector(selectorNode, ruleNode) {
  48. const count = selectorNode.reduce((total, childNode) => {
  49. // Only traverse inside actual selectors and context functional pseudo-classes
  50. if (childNode.type === 'selector' || isContextFunctionalPseudoClass(childNode)) {
  51. checkSelector(childNode, ruleNode);
  52. }
  53. if (childNode.type !== 'attribute') {
  54. // Not an attribute node -> ignore
  55. return total;
  56. }
  57. if (optionsMatches(secondaryOptions, 'ignoreAttributes', childNode.attribute)) {
  58. // it's an attribute that is supposed to be ignored
  59. return total;
  60. }
  61. total += 1;
  62. return total;
  63. }, 0);
  64. if (selectorNode.type !== 'root' && selectorNode.type !== 'pseudo' && count > primary) {
  65. const selector = selectorNode.toString();
  66. report({
  67. ruleName,
  68. result,
  69. node: ruleNode,
  70. message: messages.expected(selector, primary),
  71. word: selector,
  72. });
  73. }
  74. }
  75. root.walkRules((ruleNode) => {
  76. if (!isStandardSyntaxRule(ruleNode)) {
  77. return;
  78. }
  79. for (const selector of ruleNode.selectors) {
  80. for (const resolvedSelector of resolvedNestedSelector(selector, ruleNode)) {
  81. parseSelector(resolvedSelector, result, ruleNode, (container) =>
  82. checkSelector(container, ruleNode),
  83. );
  84. }
  85. }
  86. });
  87. };
  88. };
  89. rule.ruleName = ruleName;
  90. rule.messages = messages;
  91. rule.meta = meta;
  92. module.exports = rule;