index.mjs 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import selectorParser from 'postcss-selector-parser';
  2. const { isAttribute, isClassName, isIdentifier, isPseudoClass, isTag, isUniversal } =
  3. selectorParser;
  4. import { assert } from '../../utils/validateTypes.mjs';
  5. import getRuleSelector from '../../utils/getRuleSelector.mjs';
  6. import isStandardSyntaxRule from '../../utils/isStandardSyntaxRule.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-not-notation';
  12. const messages = ruleMessages(ruleName, {
  13. expected: (type) => `Expected ${type} :not() pseudo-class notation`,
  14. });
  15. const meta = {
  16. url: 'https://stylelint.io/user-guide/rules/selector-not-notation',
  17. fixable: true,
  18. };
  19. /** @typedef {import('postcss-selector-parser').Node} Node */
  20. /** @typedef {import('postcss-selector-parser').Selector} Selector */
  21. /** @typedef {import('postcss-selector-parser').Pseudo} Pseudo */
  22. /** @typedef {import('postcss-selector-parser').Root} Root */
  23. /** @typedef {import('postcss').Rule} Rule */
  24. /** @typedef {import('stylelint').Range} Range */
  25. /**
  26. * @param {Node} node
  27. * @returns {boolean}
  28. */
  29. const isSimpleSelector = (node) =>
  30. isPseudoClass(node) ||
  31. isAttribute(node) ||
  32. isClassName(node) ||
  33. isUniversal(node) ||
  34. isIdentifier(node) ||
  35. isTag(node);
  36. /**
  37. * @param {Node | undefined} node
  38. * @returns {node is Pseudo}
  39. */
  40. const isNot = (node) =>
  41. isPseudoClass(node) && node.value !== undefined && node.value.toLowerCase() === ':not';
  42. /**
  43. * @param {Selector[]} list
  44. * @returns {boolean}
  45. */
  46. const isSimple = (list) => {
  47. if (list.length > 1) return false;
  48. assert(list[0], 'list is never empty');
  49. const [first, second] = list[0].nodes;
  50. if (!first) return true;
  51. if (second) return false;
  52. return isSimpleSelector(first) && !isNot(first);
  53. };
  54. /** @type {import('stylelint').CoreRules[ruleName]} */
  55. const rule = (primary) => {
  56. return (root, result) => {
  57. const validOptions = validateOptions(result, ruleName, {
  58. actual: primary,
  59. possible: ['simple', 'complex'],
  60. });
  61. if (!validOptions) return;
  62. root.walkRules((ruleNode) => {
  63. if (!isStandardSyntaxRule(ruleNode)) return;
  64. const selector = ruleNode.selector;
  65. if (!selector.includes(':not(')) return;
  66. const selectorRoot = parseSelector(getRuleSelector(ruleNode), result, ruleNode);
  67. if (!selectorRoot) return;
  68. selectorRoot.walkPseudos((pseudo) => {
  69. if (!isNot(pseudo)) return;
  70. const index = pseudo.sourceIndex;
  71. const endIndex = index + pseudo.toString().trim().length;
  72. let fix;
  73. if (primary === 'complex') {
  74. const prev = pseudo.prev();
  75. const hasConsecutiveNot = prev && isNot(prev);
  76. if (!hasConsecutiveNot) return;
  77. fix = () => fixComplex(prev, ruleNode, selectorRoot);
  78. } else {
  79. const selectors = pseudo.nodes;
  80. if (isSimple(selectors)) return;
  81. const second = selectors.length > 1 && selectors[1];
  82. const isFixable =
  83. second && (!second.nodes.length || selectors.every(({ nodes }) => nodes.length === 1));
  84. if (isFixable) fix = () => fixSimple(pseudo, ruleNode, selectorRoot);
  85. }
  86. report({
  87. message: messages.expected,
  88. messageArgs: [primary],
  89. node: ruleNode,
  90. result,
  91. ruleName,
  92. index,
  93. endIndex,
  94. fix: {
  95. apply: fix,
  96. node: ruleNode,
  97. },
  98. });
  99. });
  100. });
  101. };
  102. };
  103. /**
  104. * @param {Pseudo} not
  105. * @returns {Node}
  106. */
  107. const getLastConsecutiveNot = ({ parent, sourceIndex }) => {
  108. assert(parent);
  109. const nodes = parent.nodes;
  110. const index = nodes.findIndex((node) => node.sourceIndex >= sourceIndex && !isNot(node));
  111. const node = index === -1 ? nodes[nodes.length - 1] : nodes[index - 1];
  112. assert(node);
  113. return node;
  114. };
  115. /**
  116. * @param {Pseudo} not
  117. * @param {Rule} ruleNode
  118. * @param {Root} selectorRoot
  119. */
  120. function fixSimple(not, ruleNode, selectorRoot) {
  121. const simpleSelectors = not.nodes
  122. .filter(({ nodes }) => nodes[0] && isSimpleSelector(nodes[0]))
  123. .map((s) => {
  124. assert(s.nodes[0]);
  125. s.nodes[0].rawSpaceBefore = '';
  126. s.nodes[0].rawSpaceAfter = '';
  127. return s;
  128. });
  129. const firstSelector = simpleSelectors.shift();
  130. assert(firstSelector);
  131. assert(not.parent);
  132. not.empty();
  133. not.nodes.push(firstSelector);
  134. for (const s of simpleSelectors) {
  135. const last = getLastConsecutiveNot(not);
  136. const clone = last.clone({ nodes: [s] });
  137. clone.rawSpaceBefore = '';
  138. not.parent.insertAfter(last, clone);
  139. }
  140. ruleNode.selector = selectorRoot.toString();
  141. }
  142. /**
  143. * @param {Pseudo} previousNot
  144. * @param {Rule} ruleNode
  145. * @param {Root} selectorRoot
  146. */
  147. function fixComplex(previousNot, ruleNode, selectorRoot) {
  148. const indentAndTrimRight = (/** @type {Selector[]} */ selectors) => {
  149. for (const s of selectors) {
  150. assert(s.nodes[0]);
  151. s.nodes[0].rawSpaceBefore = ' ';
  152. s.nodes[0].rawSpaceAfter = '';
  153. }
  154. };
  155. const [head, ...tail] = previousNot.nodes;
  156. let node = previousNot.next();
  157. if (head == null || head.nodes.length === 0) return;
  158. assert(head.nodes[0]);
  159. head.nodes[0].rawSpaceBefore = '';
  160. head.nodes[0].rawSpaceAfter = '';
  161. indentAndTrimRight(tail);
  162. while (isNot(node)) {
  163. const selectors = node.nodes;
  164. const prev = node;
  165. indentAndTrimRight(selectors);
  166. previousNot.nodes = previousNot.nodes.concat(selectors);
  167. node = node.next();
  168. prev.remove();
  169. }
  170. ruleNode.selector = selectorRoot.toString();
  171. }
  172. rule.ruleName = ruleName;
  173. rule.messages = messages;
  174. rule.meta = meta;
  175. export default rule;