index.mjs 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. import resolveNestedSelector from 'postcss-resolve-nested-selector';
  2. import { isBoolean, isRegExp, isString } from '../../utils/validateTypes.mjs';
  3. import getRuleSelector from '../../utils/getRuleSelector.mjs';
  4. import isKeyframeSelector from '../../utils/isKeyframeSelector.mjs';
  5. import isStandardSyntaxRule from '../../utils/isStandardSyntaxRule.mjs';
  6. import isStandardSyntaxSelector from '../../utils/isStandardSyntaxSelector.mjs';
  7. import parseSelector from '../../utils/parseSelector.mjs';
  8. import report from '../../utils/report.mjs';
  9. import ruleMessages from '../../utils/ruleMessages.mjs';
  10. import validateOptions from '../../utils/validateOptions.mjs';
  11. const ruleName = 'selector-class-pattern';
  12. const messages = ruleMessages(ruleName, {
  13. expected: (selector, pattern) => `Expected "${selector}" to match pattern "${pattern}"`,
  14. });
  15. const meta = {
  16. url: 'https://stylelint.io/user-guide/rules/selector-class-pattern',
  17. };
  18. /** @type {import('stylelint').CoreRules[ruleName]} */
  19. const rule = (primary, secondaryOptions) => {
  20. return (root, result) => {
  21. const validOptions = validateOptions(
  22. result,
  23. ruleName,
  24. {
  25. actual: primary,
  26. possible: [isRegExp, isString],
  27. },
  28. {
  29. actual: secondaryOptions,
  30. possible: {
  31. resolveNestedSelectors: [isBoolean],
  32. },
  33. optional: true,
  34. },
  35. );
  36. if (!validOptions) {
  37. return;
  38. }
  39. const shouldResolveNestedSelectors = Boolean(
  40. secondaryOptions && secondaryOptions.resolveNestedSelectors,
  41. );
  42. const normalizedPattern = isString(primary) ? new RegExp(primary) : primary;
  43. root.walkRules((ruleNode) => {
  44. if (!isStandardSyntaxRule(ruleNode)) {
  45. return;
  46. }
  47. if (ruleNode.selectors.some(isKeyframeSelector)) {
  48. return;
  49. }
  50. // Only bother resolving selectors that have an interpolating &
  51. if (shouldResolveNestedSelectors && hasInterpolatingAmpersand(ruleNode.selector)) {
  52. for (const nestedSelector of resolveNestedSelector(getRuleSelector(ruleNode), ruleNode)) {
  53. if (!isStandardSyntaxSelector(nestedSelector)) {
  54. continue;
  55. }
  56. const selectorRoot = parseSelector(nestedSelector, result, ruleNode);
  57. if (selectorRoot) checkSelector(selectorRoot, ruleNode);
  58. }
  59. } else {
  60. const selectorRoot = parseSelector(getRuleSelector(ruleNode), result, ruleNode);
  61. if (selectorRoot) checkSelector(selectorRoot, ruleNode);
  62. }
  63. });
  64. /**
  65. * @param {import('postcss-selector-parser').Root} selectorNode
  66. * @param {import('postcss').Rule} ruleNode
  67. */
  68. function checkSelector(selectorNode, ruleNode) {
  69. selectorNode.walkClasses((classNode) => {
  70. const { value, sourceIndex: index } = classNode;
  71. if (normalizedPattern.test(value)) {
  72. return;
  73. }
  74. const selector = String(classNode).trim();
  75. // `selector` may be resolved. So, getting its raw value may be pretty hard.
  76. // It means `endIndex` may be inaccurate (though non-standard selectors).
  77. //
  78. // For example, given ".abc { &_x {} }".
  79. // Then, an expected raw `selector` is "&_x",
  80. // but, an actual `selector` is ".abc_x".
  81. // see #6234 and #7482
  82. const endIndex = index + selector.length;
  83. report({
  84. result,
  85. ruleName,
  86. message: messages.expected,
  87. messageArgs: [selector, primary],
  88. node: ruleNode,
  89. index,
  90. endIndex,
  91. });
  92. });
  93. }
  94. };
  95. };
  96. /**
  97. * An "interpolating ampersand" means an "&" used to interpolate
  98. * within another simple selector, rather than an "&" that
  99. * stands on its own as a simple selector.
  100. *
  101. * @param {string} selector
  102. * @returns {boolean}
  103. */
  104. function hasInterpolatingAmpersand(selector) {
  105. for (const [i, char] of Array.from(selector).entries()) {
  106. if (char !== '&') {
  107. continue;
  108. }
  109. const prevChar = selector.charAt(i - 1);
  110. if (prevChar && !isCombinator(prevChar)) {
  111. return true;
  112. }
  113. const nextChar = selector.charAt(i + 1);
  114. if (nextChar && !isCombinator(nextChar)) {
  115. return true;
  116. }
  117. }
  118. return false;
  119. }
  120. /**
  121. * @param {string} x
  122. * @returns {boolean}
  123. */
  124. function isCombinator(x) {
  125. return /[\s+>~]/.test(x);
  126. }
  127. rule.ruleName = ruleName;
  128. rule.messages = messages;
  129. rule.meta = meta;
  130. export default rule;