index.js 4.6 KB

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