index.mjs 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. import selectorParser from 'postcss-selector-parser';
  2. const { isRoot, isSelector } = selectorParser;
  3. import { isRegExp, isString } from '../../utils/validateTypes.mjs';
  4. import flattenNestedSelectorsForRule from '../../utils/flattenNestedSelectorsForRule.mjs';
  5. import getStrippedSelectorSource from '../../utils/getStrippedSelectorSource.mjs';
  6. import isContextFunctionalPseudoClass from '../../utils/isContextFunctionalPseudoClass.mjs';
  7. import isNonNegativeInteger from '../../utils/isNonNegativeInteger.mjs';
  8. import isStandardSyntaxRule from '../../utils/isStandardSyntaxRule.mjs';
  9. import optionsMatches from '../../utils/optionsMatches.mjs';
  10. import report from '../../utils/report.mjs';
  11. import ruleMessages from '../../utils/ruleMessages.mjs';
  12. import validateOptions from '../../utils/validateOptions.mjs';
  13. const ruleName = 'selector-max-attribute';
  14. const messages = ruleMessages(ruleName, {
  15. expected: (selector, max) =>
  16. `Expected "${selector}" to have no more than ${max} attribute ${
  17. max === 1 ? 'selector' : 'selectors'
  18. }`,
  19. });
  20. const meta = {
  21. url: 'https://stylelint.io/user-guide/rules/selector-max-attribute',
  22. };
  23. /** @type {import('stylelint').CoreRules[ruleName]} */
  24. const rule = (primary, secondaryOptions) => {
  25. return (root, result) => {
  26. const validOptions = validateOptions(
  27. result,
  28. ruleName,
  29. {
  30. actual: primary,
  31. possible: isNonNegativeInteger,
  32. },
  33. {
  34. actual: secondaryOptions,
  35. possible: {
  36. ignoreAttributes: [isString, isRegExp],
  37. },
  38. optional: true,
  39. },
  40. );
  41. if (!validOptions) {
  42. return;
  43. }
  44. /**
  45. * @param {import('postcss-selector-parser').Selector} resolvedSelectorNode
  46. * @param {import('postcss-selector-parser').Selector} selectorNode
  47. * @param {import('postcss').Rule} ruleNode
  48. */
  49. function checkSelector(resolvedSelectorNode, selectorNode, ruleNode) {
  50. const count = resolvedSelectorNode.reduce((total, childNode) => {
  51. if (childNode.type !== 'attribute') {
  52. // Not an attribute node -> ignore
  53. return total;
  54. }
  55. if (optionsMatches(secondaryOptions, 'ignoreAttributes', childNode.attribute)) {
  56. // it's an attribute that is supposed to be ignored
  57. return total;
  58. }
  59. total += 1;
  60. return total;
  61. }, 0);
  62. if (count > primary) {
  63. const { index, endIndex, selector: selectorStr } = getStrippedSelectorSource(selectorNode);
  64. report({
  65. ruleName,
  66. result,
  67. node: ruleNode,
  68. message: messages.expected,
  69. messageArgs: [selectorStr, primary],
  70. index,
  71. endIndex,
  72. });
  73. }
  74. }
  75. root.walkRules((ruleNode) => {
  76. if (!isStandardSyntaxRule(ruleNode)) return;
  77. flattenNestedSelectorsForRule(ruleNode, result).forEach(({ selector, resolvedSelectors }) => {
  78. resolvedSelectors.walk((childSelector) => {
  79. if (!isSelector(childSelector)) return;
  80. if (
  81. isRoot(childSelector.parent) ||
  82. isContextFunctionalPseudoClass(childSelector.parent)
  83. ) {
  84. checkSelector(childSelector, selector, ruleNode);
  85. }
  86. });
  87. });
  88. });
  89. };
  90. };
  91. rule.ruleName = ruleName;
  92. rule.messages = messages;
  93. rule.meta = meta;
  94. export default rule;