index.js 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. 'use strict';
  2. const addEmptyLineAfter = require('../../utils/addEmptyLineAfter');
  3. const blockString = require('../../utils/blockString');
  4. const hasBlock = require('../../utils/hasBlock');
  5. const hasEmptyBlock = require('../../utils/hasEmptyBlock');
  6. const hasEmptyLine = require('../../utils/hasEmptyLine');
  7. const isSingleLineString = require('../../utils/isSingleLineString');
  8. const optionsMatches = require('../../utils/optionsMatches');
  9. const removeEmptyLinesAfter = require('../../utils/removeEmptyLinesAfter');
  10. const report = require('../../utils/report');
  11. const ruleMessages = require('../../utils/ruleMessages');
  12. const validateOptions = require('../../utils/validateOptions');
  13. const ruleName = 'block-closing-brace-empty-line-before';
  14. const messages = ruleMessages(ruleName, {
  15. expected: 'Expected empty line before closing brace',
  16. rejected: 'Unexpected empty line before closing brace',
  17. });
  18. const meta = {
  19. url: 'https://stylelint.io/user-guide/rules/list/block-closing-brace-empty-line-before',
  20. fixable: true,
  21. };
  22. /** @type {import('stylelint').Rule} */
  23. const rule = (primary, secondaryOptions, context) => {
  24. return (root, result) => {
  25. const validOptions = validateOptions(
  26. result,
  27. ruleName,
  28. {
  29. actual: primary,
  30. possible: ['always-multi-line', 'never'],
  31. },
  32. {
  33. actual: secondaryOptions,
  34. possible: {
  35. except: ['after-closing-brace'],
  36. },
  37. optional: true,
  38. },
  39. );
  40. if (!validOptions) {
  41. return;
  42. }
  43. // Check both kinds of statements: rules and at-rules
  44. root.walkRules(check);
  45. root.walkAtRules(check);
  46. /**
  47. * @param {import('postcss').Rule | import('postcss').AtRule} statement
  48. */
  49. function check(statement) {
  50. // Return early if blockless or has empty block
  51. if (!hasBlock(statement) || hasEmptyBlock(statement)) {
  52. return;
  53. }
  54. // Get whitespace after ""}", ignoring extra semicolon
  55. const before = (statement.raws.after || '').replace(/;+/, '');
  56. // Calculate index
  57. const statementString = statement.toString();
  58. let index = statementString.length - 1;
  59. if (statementString[index - 1] === '\r') {
  60. index -= 1;
  61. }
  62. // Set expectation
  63. const expectEmptyLineBefore = (() => {
  64. const childNodeTypes = statement.nodes.map((item) => item.type);
  65. // Reverse the primary options if `after-closing-brace` is set
  66. if (
  67. optionsMatches(secondaryOptions, 'except', 'after-closing-brace') &&
  68. statement.type === 'atrule' &&
  69. !childNodeTypes.includes('decl')
  70. ) {
  71. return primary === 'never';
  72. }
  73. return primary === 'always-multi-line' && !isSingleLineString(blockString(statement));
  74. })();
  75. // Check for at least one empty line
  76. const hasEmptyLineBefore = hasEmptyLine(before);
  77. // Return if the expectation is met
  78. if (expectEmptyLineBefore === hasEmptyLineBefore) {
  79. return;
  80. }
  81. if (context.fix) {
  82. const { newline } = context;
  83. if (typeof newline !== 'string') return;
  84. if (expectEmptyLineBefore) {
  85. addEmptyLineAfter(statement, newline);
  86. } else {
  87. removeEmptyLinesAfter(statement, newline);
  88. }
  89. return;
  90. }
  91. const message = expectEmptyLineBefore ? messages.expected : messages.rejected;
  92. report({
  93. message,
  94. result,
  95. ruleName,
  96. node: statement,
  97. index,
  98. });
  99. }
  100. };
  101. };
  102. rule.ruleName = ruleName;
  103. rule.messages = messages;
  104. rule.meta = meta;
  105. module.exports = rule;