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) =>
  14. `Expected class selector "${selector}" to match pattern "${pattern}"`,
  15. });
  16. const meta = {
  17. url: 'https://stylelint.io/user-guide/rules/list/selector-class-pattern',
  18. };
  19. /** @type {import('stylelint').Rule<string | RegExp, { resolveNestedSelector: boolean }>} */
  20. const rule = (primary, secondaryOptions) => {
  21. return (root, result) => {
  22. const validOptions = validateOptions(
  23. result,
  24. ruleName,
  25. {
  26. actual: primary,
  27. possible: [isRegExp, isString],
  28. },
  29. {
  30. actual: secondaryOptions,
  31. possible: {
  32. resolveNestedSelectors: [isBoolean],
  33. },
  34. optional: true,
  35. },
  36. );
  37. if (!validOptions) {
  38. return;
  39. }
  40. const shouldResolveNestedSelectors = Boolean(
  41. secondaryOptions && secondaryOptions.resolveNestedSelectors,
  42. );
  43. const normalizedPattern = isString(primary) ? new RegExp(primary) : primary;
  44. root.walkRules((ruleNode) => {
  45. const { selector, selectors } = ruleNode;
  46. if (!isStandardSyntaxRule(ruleNode)) {
  47. return;
  48. }
  49. if (selectors.some((s) => isKeyframeSelector(s))) {
  50. return;
  51. }
  52. // Only bother resolving selectors that have an interpolating &
  53. if (shouldResolveNestedSelectors && hasInterpolatingAmpersand(selector)) {
  54. for (const nestedSelector of resolveNestedSelector(selector, ruleNode)) {
  55. if (!isStandardSyntaxSelector(nestedSelector)) {
  56. continue;
  57. }
  58. parseSelector(nestedSelector, result, ruleNode, (s) => checkSelector(s, ruleNode));
  59. }
  60. } else {
  61. parseSelector(selector, result, ruleNode, (s) => checkSelector(s, 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);
  75. // TODO: `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. const endIndex = index + selector.length;
  82. report({
  83. result,
  84. ruleName,
  85. message: messages.expected(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;