index.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. 'use strict';
  2. const atRuleParamIndex = require('../../utils/atRuleParamIndex');
  3. const isCustomSelector = require('../../utils/isCustomSelector');
  4. const isStandardSyntaxAtRule = require('../../utils/isStandardSyntaxAtRule');
  5. const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
  6. const isStandardSyntaxSelector = require('../../utils/isStandardSyntaxSelector');
  7. const {
  8. atRulePagePseudoClasses,
  9. pseudoClasses,
  10. pseudoElements,
  11. webkitScrollbarPseudoClasses,
  12. webkitScrollbarPseudoElements,
  13. } = require('../../reference/selectors');
  14. const optionsMatches = require('../../utils/optionsMatches');
  15. const parseSelector = require('../../utils/parseSelector');
  16. const report = require('../../utils/report');
  17. const ruleMessages = require('../../utils/ruleMessages');
  18. const validateOptions = require('../../utils/validateOptions');
  19. const vendor = require('../../utils/vendor');
  20. const { isString, isRegExp } = require('../../utils/validateTypes');
  21. const { isAtRule } = require('../../utils/typeGuards');
  22. const ruleName = 'selector-pseudo-class-no-unknown';
  23. const messages = ruleMessages(ruleName, {
  24. rejected: (selector) => `Unexpected unknown pseudo-class selector "${selector}"`,
  25. });
  26. const meta = {
  27. url: 'https://stylelint.io/user-guide/rules/list/selector-pseudo-class-no-unknown',
  28. };
  29. /** @type {import('stylelint').Rule} */
  30. const rule = (primary, secondaryOptions) => {
  31. return (root, result) => {
  32. const validOptions = validateOptions(
  33. result,
  34. ruleName,
  35. { actual: primary },
  36. {
  37. actual: secondaryOptions,
  38. possible: {
  39. ignorePseudoClasses: [isString, isRegExp],
  40. },
  41. optional: true,
  42. },
  43. );
  44. if (!validOptions) {
  45. return;
  46. }
  47. /**
  48. * @param {string} selector
  49. * @param {import('postcss').ChildNode} node
  50. */
  51. function check(selector, node) {
  52. parseSelector(selector, result, node, (selectorTree) => {
  53. selectorTree.walkPseudos((pseudoNode) => {
  54. const value = pseudoNode.value;
  55. if (!isStandardSyntaxSelector(value)) {
  56. return;
  57. }
  58. if (isCustomSelector(value)) {
  59. return;
  60. }
  61. // Ignore pseudo-elements
  62. if (value.slice(0, 2) === '::') {
  63. return;
  64. }
  65. if (optionsMatches(secondaryOptions, 'ignorePseudoClasses', pseudoNode.value.slice(1))) {
  66. return;
  67. }
  68. let index = null;
  69. const name = value.slice(1).toLowerCase();
  70. if (isAtRule(node) && node.name === 'page') {
  71. if (atRulePagePseudoClasses.has(name)) {
  72. return;
  73. }
  74. index = atRuleParamIndex(node) + pseudoNode.sourceIndex;
  75. } else {
  76. if (vendor.prefix(name) || pseudoClasses.has(name) || pseudoElements.has(name)) {
  77. return;
  78. }
  79. /** @type {import('postcss-selector-parser').Base} */
  80. let prevPseudoElement = pseudoNode;
  81. do {
  82. prevPseudoElement = /** @type {import('postcss-selector-parser').Base} */ (
  83. prevPseudoElement.prev()
  84. );
  85. if (prevPseudoElement && prevPseudoElement.value.slice(0, 2) === '::') {
  86. break;
  87. }
  88. } while (prevPseudoElement);
  89. if (prevPseudoElement) {
  90. const prevPseudoElementValue = prevPseudoElement.value.toLowerCase().slice(2);
  91. if (
  92. webkitScrollbarPseudoElements.has(prevPseudoElementValue) &&
  93. webkitScrollbarPseudoClasses.has(name)
  94. ) {
  95. return;
  96. }
  97. }
  98. index = pseudoNode.sourceIndex;
  99. }
  100. report({
  101. message: messages.rejected(value),
  102. node,
  103. index,
  104. ruleName,
  105. result,
  106. word: value,
  107. });
  108. });
  109. });
  110. }
  111. root.walk((node) => {
  112. let selector = null;
  113. if (node.type === 'rule') {
  114. if (!isStandardSyntaxRule(node)) {
  115. return;
  116. }
  117. selector = node.selector;
  118. } else if (isAtRule(node) && node.name === 'page' && node.params) {
  119. if (!isStandardSyntaxAtRule(node)) {
  120. return;
  121. }
  122. selector = node.params;
  123. }
  124. // Return if selector empty, it is meaning node type is not a rule or a at-rule
  125. if (!selector) {
  126. return;
  127. }
  128. // Return early before parse if no pseudos for performance
  129. if (!selector.includes(':')) {
  130. return;
  131. }
  132. check(selector, node);
  133. });
  134. };
  135. };
  136. rule.ruleName = ruleName;
  137. rule.messages = messages;
  138. rule.meta = meta;
  139. module.exports = rule;