index.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. 'use strict';
  2. const addEmptyLineBefore = require('../../utils/addEmptyLineBefore');
  3. const getPreviousNonSharedLineCommentNode = require('../../utils/getPreviousNonSharedLineCommentNode');
  4. const hasEmptyLine = require('../../utils/hasEmptyLine');
  5. const isAfterComment = require('../../utils/isAfterComment');
  6. const isBlocklessAtRuleAfterBlocklessAtRule = require('../../utils/isBlocklessAtRuleAfterBlocklessAtRule');
  7. const isBlocklessAtRuleAfterSameNameBlocklessAtRule = require('../../utils/isBlocklessAtRuleAfterSameNameBlocklessAtRule');
  8. const isFirstNested = require('../../utils/isFirstNested');
  9. const isFirstNodeOfRoot = require('../../utils/isFirstNodeOfRoot');
  10. const isStandardSyntaxAtRule = require('../../utils/isStandardSyntaxAtRule');
  11. const optionsMatches = require('../../utils/optionsMatches');
  12. const removeEmptyLinesBefore = require('../../utils/removeEmptyLinesBefore');
  13. const report = require('../../utils/report');
  14. const ruleMessages = require('../../utils/ruleMessages');
  15. const validateOptions = require('../../utils/validateOptions');
  16. const { isString } = require('../../utils/validateTypes');
  17. const ruleName = 'at-rule-empty-line-before';
  18. const messages = ruleMessages(ruleName, {
  19. expected: 'Expected empty line before at-rule',
  20. rejected: 'Unexpected empty line before at-rule',
  21. });
  22. const meta = {
  23. url: 'https://stylelint.io/user-guide/rules/list/at-rule-empty-line-before',
  24. };
  25. /** @type {import('stylelint').Rule} */
  26. const rule = (primary, secondaryOptions, context) => {
  27. return (root, result) => {
  28. const validOptions = validateOptions(
  29. result,
  30. ruleName,
  31. {
  32. actual: primary,
  33. possible: ['always', 'never'],
  34. },
  35. {
  36. actual: secondaryOptions,
  37. possible: {
  38. except: [
  39. 'after-same-name',
  40. 'inside-block',
  41. 'blockless-after-same-name-blockless',
  42. 'blockless-after-blockless',
  43. 'first-nested',
  44. ],
  45. ignore: [
  46. 'after-comment',
  47. 'first-nested',
  48. 'inside-block',
  49. 'blockless-after-same-name-blockless',
  50. 'blockless-after-blockless',
  51. ],
  52. ignoreAtRules: [isString],
  53. },
  54. optional: true,
  55. },
  56. );
  57. if (!validOptions) {
  58. return;
  59. }
  60. /** @type {'always' | 'never'} */
  61. const expectation = primary;
  62. root.walkAtRules((atRule) => {
  63. const isNested = atRule.parent && atRule.parent.type !== 'root';
  64. // Ignore the first node
  65. if (isFirstNodeOfRoot(atRule)) {
  66. return;
  67. }
  68. if (!isStandardSyntaxAtRule(atRule)) {
  69. return;
  70. }
  71. // Return early if at-rule is to be ignored
  72. if (optionsMatches(secondaryOptions, 'ignoreAtRules', atRule.name)) {
  73. return;
  74. }
  75. // Optionally ignore the expectation if the node is blockless
  76. if (
  77. optionsMatches(secondaryOptions, 'ignore', 'blockless-after-blockless') &&
  78. isBlocklessAtRuleAfterBlocklessAtRule(atRule)
  79. ) {
  80. return;
  81. }
  82. // Optionally ignore the node if it is the first nested
  83. if (optionsMatches(secondaryOptions, 'ignore', 'first-nested') && isFirstNested(atRule)) {
  84. return;
  85. }
  86. // Optionally ignore the expectation if the node is blockless
  87. // and following another blockless at-rule with the same name
  88. if (
  89. optionsMatches(secondaryOptions, 'ignore', 'blockless-after-same-name-blockless') &&
  90. isBlocklessAtRuleAfterSameNameBlocklessAtRule(atRule)
  91. ) {
  92. return;
  93. }
  94. // Optionally ignore the expectation if the node is inside a block
  95. if (optionsMatches(secondaryOptions, 'ignore', 'inside-block') && isNested) {
  96. return;
  97. }
  98. // Optionally ignore the expectation if a comment precedes this node
  99. if (optionsMatches(secondaryOptions, 'ignore', 'after-comment') && isAfterComment(atRule)) {
  100. return;
  101. }
  102. const hasEmptyLineBefore = hasEmptyLine(atRule.raws.before);
  103. let expectEmptyLineBefore = expectation === 'always';
  104. // Optionally reverse the expectation if any exceptions apply
  105. if (
  106. (optionsMatches(secondaryOptions, 'except', 'after-same-name') &&
  107. isAtRuleAfterSameNameAtRule(atRule)) ||
  108. (optionsMatches(secondaryOptions, 'except', 'inside-block') && isNested) ||
  109. (optionsMatches(secondaryOptions, 'except', 'first-nested') && isFirstNested(atRule)) ||
  110. (optionsMatches(secondaryOptions, 'except', 'blockless-after-blockless') &&
  111. isBlocklessAtRuleAfterBlocklessAtRule(atRule)) ||
  112. (optionsMatches(secondaryOptions, 'except', 'blockless-after-same-name-blockless') &&
  113. isBlocklessAtRuleAfterSameNameBlocklessAtRule(atRule))
  114. ) {
  115. expectEmptyLineBefore = !expectEmptyLineBefore;
  116. }
  117. // Return if the expectation is met
  118. if (expectEmptyLineBefore === hasEmptyLineBefore) {
  119. return;
  120. }
  121. // Fix
  122. if (context.fix && context.newline) {
  123. if (expectEmptyLineBefore) {
  124. addEmptyLineBefore(atRule, context.newline);
  125. } else {
  126. removeEmptyLinesBefore(atRule, context.newline);
  127. }
  128. return;
  129. }
  130. const message = expectEmptyLineBefore ? messages.expected : messages.rejected;
  131. report({ message, node: atRule, result, ruleName });
  132. });
  133. };
  134. };
  135. /**
  136. * @param {import('postcss').AtRule} atRule
  137. */
  138. function isAtRuleAfterSameNameAtRule(atRule) {
  139. const previousNode = getPreviousNonSharedLineCommentNode(atRule);
  140. // @ts-expect-error -- TS2339: Property 'name' does not exist on type 'Node'.
  141. return previousNode && previousNode.type === 'atrule' && previousNode.name === atRule.name;
  142. }
  143. rule.ruleName = ruleName;
  144. rule.messages = messages;
  145. rule.meta = meta;
  146. module.exports = rule;