index.js 3.3 KB

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