index.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. 'use strict';
  2. const addEmptyLineBefore = require('../../utils/addEmptyLineBefore');
  3. const getPreviousNonSharedLineCommentNode = require('../../utils/getPreviousNonSharedLineCommentNode');
  4. const hasEmptyLine = require('../../utils/hasEmptyLine');
  5. const isAfterSingleLineComment = require('../../utils/isAfterSingleLineComment');
  6. const isFirstNested = require('../../utils/isFirstNested');
  7. const isFirstNodeOfRoot = require('../../utils/isFirstNodeOfRoot');
  8. const isSingleLineString = require('../../utils/isSingleLineString');
  9. const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
  10. const optionsMatches = require('../../utils/optionsMatches');
  11. const removeEmptyLinesBefore = require('../../utils/removeEmptyLinesBefore');
  12. const report = require('../../utils/report');
  13. const ruleMessages = require('../../utils/ruleMessages');
  14. const validateOptions = require('../../utils/validateOptions');
  15. const ruleName = 'rule-empty-line-before';
  16. const messages = ruleMessages(ruleName, {
  17. expected: 'Expected empty line before rule',
  18. rejected: 'Unexpected empty line before rule',
  19. });
  20. const meta = {
  21. url: 'https://stylelint.io/user-guide/rules/list/rule-empty-line-before',
  22. };
  23. /** @type {import('stylelint').Rule} */
  24. const rule = (primary, secondaryOptions, context) => {
  25. return (root, result) => {
  26. const validOptions = validateOptions(
  27. result,
  28. ruleName,
  29. {
  30. actual: primary,
  31. possible: ['always', 'never', 'always-multi-line', 'never-multi-line'],
  32. },
  33. {
  34. actual: secondaryOptions,
  35. possible: {
  36. ignore: ['after-comment', 'first-nested', 'inside-block'],
  37. except: [
  38. 'after-rule',
  39. 'after-single-line-comment',
  40. 'first-nested',
  41. 'inside-block-and-after-rule',
  42. 'inside-block',
  43. ],
  44. },
  45. optional: true,
  46. },
  47. );
  48. if (!validOptions) {
  49. return;
  50. }
  51. const expectation = /** @type {string} */ (primary);
  52. root.walkRules((ruleNode) => {
  53. if (!isStandardSyntaxRule(ruleNode)) {
  54. return;
  55. }
  56. // Ignore the first node
  57. if (isFirstNodeOfRoot(ruleNode)) {
  58. return;
  59. }
  60. // Optionally ignore the expectation if a comment precedes this node
  61. if (optionsMatches(secondaryOptions, 'ignore', 'after-comment')) {
  62. const prevNode = ruleNode.prev();
  63. if (prevNode && prevNode.type === 'comment') {
  64. return;
  65. }
  66. }
  67. // Optionally ignore the node if it is the first nested
  68. if (optionsMatches(secondaryOptions, 'ignore', 'first-nested') && isFirstNested(ruleNode)) {
  69. return;
  70. }
  71. const isNested = ruleNode.parent && ruleNode.parent.type !== 'root';
  72. // Optionally ignore the expectation if inside a block
  73. if (optionsMatches(secondaryOptions, 'ignore', 'inside-block') && isNested) {
  74. return;
  75. }
  76. // Ignore if the expectation is for multiple and the rule is single-line
  77. if (expectation.includes('multi-line') && isSingleLineString(ruleNode.toString())) {
  78. return;
  79. }
  80. let expectEmptyLineBefore = expectation.includes('always');
  81. // Optionally reverse the expectation if any exceptions apply
  82. if (
  83. (optionsMatches(secondaryOptions, 'except', 'first-nested') && isFirstNested(ruleNode)) ||
  84. (optionsMatches(secondaryOptions, 'except', 'after-rule') && isAfterRule(ruleNode)) ||
  85. (optionsMatches(secondaryOptions, 'except', 'inside-block-and-after-rule') &&
  86. isNested &&
  87. isAfterRule(ruleNode)) ||
  88. (optionsMatches(secondaryOptions, 'except', 'after-single-line-comment') &&
  89. isAfterSingleLineComment(ruleNode)) ||
  90. (optionsMatches(secondaryOptions, 'except', 'inside-block') && isNested)
  91. ) {
  92. expectEmptyLineBefore = !expectEmptyLineBefore;
  93. }
  94. const hasEmptyLineBefore = hasEmptyLine(ruleNode.raws.before);
  95. // Return if the expectation is met
  96. if (expectEmptyLineBefore === hasEmptyLineBefore) {
  97. return;
  98. }
  99. // Fix
  100. if (context.fix) {
  101. const newline = context.newline;
  102. if (typeof newline !== 'string') {
  103. throw new Error(`The "newline" property must be a string: ${newline}`);
  104. }
  105. if (expectEmptyLineBefore) {
  106. addEmptyLineBefore(ruleNode, newline);
  107. } else {
  108. removeEmptyLinesBefore(ruleNode, newline);
  109. }
  110. return;
  111. }
  112. const message = expectEmptyLineBefore ? messages.expected : messages.rejected;
  113. report({
  114. message,
  115. node: ruleNode,
  116. result,
  117. ruleName,
  118. });
  119. });
  120. };
  121. };
  122. /**
  123. * @param {import('postcss').Rule} ruleNode
  124. * @returns {boolean}
  125. */
  126. function isAfterRule(ruleNode) {
  127. const prevNode = getPreviousNonSharedLineCommentNode(ruleNode);
  128. return prevNode != null && prevNode.type === 'rule';
  129. }
  130. rule.ruleName = ruleName;
  131. rule.messages = messages;
  132. rule.meta = meta;
  133. module.exports = rule;