index.cjs 4.3 KB

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