index.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. 'use strict';
  2. const isKeyframeSelector = require('../../utils/isKeyframeSelector');
  3. const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
  4. const isStandardSyntaxSelector = require('../../utils/isStandardSyntaxSelector');
  5. const parseSelector = require('../../utils/parseSelector');
  6. const report = require('../../utils/report');
  7. const resolveNestedSelector = require('postcss-resolve-nested-selector');
  8. const ruleMessages = require('../../utils/ruleMessages');
  9. const validateOptions = require('../../utils/validateOptions');
  10. const { isBoolean, isRegExp, isString } = require('../../utils/validateTypes');
  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').Rule<string | RegExp, { resolveNestedSelector: boolean }>} */
  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. const { selector, selectors } = ruleNode;
  45. if (!isStandardSyntaxRule(ruleNode)) {
  46. return;
  47. }
  48. if (selectors.some((s) => isKeyframeSelector(s))) {
  49. return;
  50. }
  51. // Only bother resolving selectors that have an interpolating &
  52. if (shouldResolveNestedSelectors && hasInterpolatingAmpersand(selector)) {
  53. for (const nestedSelector of resolveNestedSelector(selector, ruleNode)) {
  54. if (!isStandardSyntaxSelector(nestedSelector)) {
  55. continue;
  56. }
  57. parseSelector(nestedSelector, result, ruleNode, (s) => checkSelector(s, ruleNode));
  58. }
  59. } else {
  60. parseSelector(selector, result, ruleNode, (s) => checkSelector(s, ruleNode));
  61. }
  62. });
  63. /**
  64. * @param {import('postcss-selector-parser').Root} selectorNode
  65. * @param {import('postcss').Rule} ruleNode
  66. */
  67. function checkSelector(selectorNode, ruleNode) {
  68. selectorNode.walkClasses((classNode) => {
  69. const { value, sourceIndex: index } = classNode;
  70. if (normalizedPattern.test(value)) {
  71. return;
  72. }
  73. const selector = String(classNode);
  74. // TODO: `selector` may be resolved. So, getting its raw value may be pretty hard.
  75. // It means `endIndex` may be inaccurate (though non-standard selectors).
  76. //
  77. // For example, given ".abc { &_x {} }".
  78. // Then, an expected raw `selector` is "&_x",
  79. // but, an actual `selector` is ".abc_x".
  80. const endIndex = index + selector.length;
  81. report({
  82. result,
  83. ruleName,
  84. message: messages.expected,
  85. messageArgs: [selector, primary],
  86. node: ruleNode,
  87. index,
  88. endIndex,
  89. });
  90. });
  91. }
  92. };
  93. };
  94. /**
  95. * An "interpolating ampersand" means an "&" used to interpolate
  96. * within another simple selector, rather than an "&" that
  97. * stands on its own as a simple selector.
  98. *
  99. * @param {string} selector
  100. * @returns {boolean}
  101. */
  102. function hasInterpolatingAmpersand(selector) {
  103. for (const [i, char] of Array.from(selector).entries()) {
  104. if (char !== '&') {
  105. continue;
  106. }
  107. const prevChar = selector.charAt(i - 1);
  108. if (prevChar && !isCombinator(prevChar)) {
  109. return true;
  110. }
  111. const nextChar = selector.charAt(i + 1);
  112. if (nextChar && !isCombinator(nextChar)) {
  113. return true;
  114. }
  115. }
  116. return false;
  117. }
  118. /**
  119. * @param {string} x
  120. * @returns {boolean}
  121. */
  122. function isCombinator(x) {
  123. return /[\s+>~]/.test(x);
  124. }
  125. rule.ruleName = ruleName;
  126. rule.messages = messages;
  127. rule.meta = meta;
  128. module.exports = rule;