index.js 4.6 KB

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