index.mjs 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import { parse, walk } from 'css-tree';
  2. import { isRegExp, isString } from '../../utils/validateTypes.mjs';
  3. import { atRuleParamIndex } from '../../utils/nodeFieldIndices.mjs';
  4. import { atRuleRegexes } from '../../utils/regexes.mjs';
  5. import getAtRuleParams from '../../utils/getAtRuleParams.mjs';
  6. import getRuleSelector from '../../utils/getRuleSelector.mjs';
  7. import isStandardSyntaxAtRule from '../../utils/isStandardSyntaxAtRule.mjs';
  8. import isStandardSyntaxRule from '../../utils/isStandardSyntaxRule.mjs';
  9. import optionsMatches from '../../utils/optionsMatches.mjs';
  10. import report from '../../utils/report.mjs';
  11. import ruleMessages from '../../utils/ruleMessages.mjs';
  12. import validateOptions from '../../utils/validateOptions.mjs';
  13. const ruleName = 'nesting-selector-no-missing-scoping-root';
  14. const messages = ruleMessages(ruleName, {
  15. rejected: 'Unexpected missing scoping root',
  16. });
  17. const meta = {
  18. url: 'https://stylelint.io/user-guide/rules/nesting-selector-no-missing-scoping-root',
  19. };
  20. /** @type {import('stylelint').CoreRules[ruleName]} */
  21. const rule = (primary, secondaryOptions) => {
  22. return (root, result) => {
  23. const validOptions = validateOptions(
  24. result,
  25. ruleName,
  26. { actual: primary, possible: [true] },
  27. {
  28. actual: secondaryOptions,
  29. possible: {
  30. ignoreAtRules: [isString, isRegExp],
  31. },
  32. optional: true,
  33. },
  34. );
  35. if (!validOptions) return;
  36. root.walkRules(/&/, (ruleNode) => {
  37. if (!isStandardSyntaxRule(ruleNode)) return;
  38. // Check if the rule is nested within a scoping root
  39. if (hasValidScopingRoot(ruleNode, secondaryOptions)) return;
  40. let ast;
  41. try {
  42. ast = parse(getRuleSelector(ruleNode), { context: 'selectorList', positions: true });
  43. } catch {
  44. // Cannot parse selector list, skip checking
  45. return;
  46. }
  47. check(ruleNode, ast);
  48. });
  49. // Check @scope at-rules for nesting selectors in parameters
  50. root.walkAtRules(atRuleRegexes.scopeName, (atRule) => {
  51. if (!isStandardSyntaxAtRule(atRule)) return;
  52. // Cheap check for nesting selector
  53. if (!atRule.params.includes('&')) return;
  54. // Only check @scope at-rules that don't have a parent scoping context
  55. if (hasValidScopingRoot(atRule, secondaryOptions)) return;
  56. let ast;
  57. try {
  58. ast = parse(getAtRuleParams(atRule), {
  59. atrule: 'scope',
  60. context: 'atrulePrelude',
  61. positions: true,
  62. });
  63. } catch {
  64. // Cannot parse @scope at-rule, skip checking
  65. return;
  66. }
  67. check(atRule, ast, atRuleParamIndex(atRule));
  68. });
  69. /**
  70. * @param {import('postcss').Rule | import('postcss').AtRule} node
  71. * @param {import('css-tree').CssNode} ast
  72. * @param {number} [offset=0]
  73. */
  74. function check(node, ast, offset = 0) {
  75. walk(ast, {
  76. /**
  77. * @param {import('css-tree').CssNode} cssNode
  78. */
  79. enter(cssNode) {
  80. if (cssNode.type !== 'NestingSelector') return;
  81. if (!cssNode.loc) return;
  82. const index = offset + cssNode.loc.start.offset;
  83. const endIndex = index + 1;
  84. report({
  85. message: messages.rejected,
  86. node,
  87. result,
  88. ruleName,
  89. index,
  90. endIndex,
  91. });
  92. },
  93. });
  94. }
  95. };
  96. };
  97. /**
  98. * Check if a node has a valid scoping root
  99. * @param {import('postcss').Rule | import('postcss').AtRule} node
  100. * @param {object} secondaryOptions
  101. * @returns {boolean}
  102. */
  103. function hasValidScopingRoot(node, secondaryOptions) {
  104. let current = node.parent;
  105. while (current) {
  106. // If we find a rule in the parent chain, it provides a scoping root
  107. if (current.type === 'rule') {
  108. return true;
  109. }
  110. // If we find an @scope at-rule, it provides a scoping root
  111. if (current.type === 'atrule' && current.name === 'scope') {
  112. return true;
  113. }
  114. // If we find an ignored at-rule, it provides a scoping root
  115. if (
  116. current.type === 'atrule' &&
  117. optionsMatches(secondaryOptions, 'ignoreAtRules', current.name)
  118. ) {
  119. return true;
  120. }
  121. // If we reach the root without finding a scoping root
  122. if (current.type === 'root') {
  123. return false;
  124. }
  125. current = current.parent;
  126. }
  127. return false;
  128. }
  129. rule.ruleName = ruleName;
  130. rule.messages = messages;
  131. rule.meta = meta;
  132. export default rule;