index.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. 'use strict';
  2. const beforeBlockString = require('../../utils/beforeBlockString');
  3. const blockString = require('../../utils/blockString');
  4. const hasBlock = require('../../utils/hasBlock');
  5. const hasEmptyBlock = require('../../utils/hasEmptyBlock');
  6. const optionsMatches = require('../../utils/optionsMatches');
  7. const rawNodeString = require('../../utils/rawNodeString');
  8. const report = require('../../utils/report');
  9. const ruleMessages = require('../../utils/ruleMessages');
  10. const validateOptions = require('../../utils/validateOptions');
  11. const whitespaceChecker = require('../../utils/whitespaceChecker');
  12. const ruleName = 'block-opening-brace-newline-after';
  13. const messages = ruleMessages(ruleName, {
  14. expectedAfter: () => 'Expected newline after "{"',
  15. expectedAfterMultiLine: () => 'Expected newline after "{" of a multi-line block',
  16. rejectedAfterMultiLine: () => 'Unexpected whitespace after "{" of a multi-line block',
  17. });
  18. const meta = {
  19. url: 'https://stylelint.io/user-guide/rules/block-opening-brace-newline-after',
  20. fixable: true,
  21. };
  22. /** @type {import('stylelint').Rule} */
  23. const rule = (primary, secondaryOptions, context) => {
  24. const checker = whitespaceChecker('newline', primary, messages);
  25. return (root, result) => {
  26. const validOptions = validateOptions(
  27. result,
  28. ruleName,
  29. {
  30. actual: primary,
  31. possible: ['always', 'rules', 'always-multi-line', 'never-multi-line'],
  32. },
  33. {
  34. actual: secondaryOptions,
  35. possible: {
  36. ignore: ['rules'],
  37. },
  38. optional: true,
  39. },
  40. );
  41. if (!validOptions) {
  42. return;
  43. }
  44. // Check both kinds of statement: rules and at-rules
  45. if (!optionsMatches(secondaryOptions, 'ignore', 'rules')) {
  46. root.walkRules(check);
  47. }
  48. root.walkAtRules(check);
  49. /**
  50. * @param {import('postcss').Rule | import('postcss').AtRule} statement
  51. */
  52. function check(statement) {
  53. // Return early if blockless or has an empty block
  54. if (!hasBlock(statement) || hasEmptyBlock(statement)) {
  55. return;
  56. }
  57. const backupCommentNextBefores = new Map();
  58. /**
  59. * next node with checking newlines after comment
  60. *
  61. * @param {import('postcss').ChildNode | undefined} startNode
  62. * @returns {import('postcss').ChildNode | undefined}
  63. */
  64. function nextNode(startNode) {
  65. if (!startNode || !startNode.next) return;
  66. if (startNode.type === 'comment') {
  67. const reNewLine = /\r?\n/;
  68. const newLineMatch = reNewLine.test(startNode.raws.before || '');
  69. const next = startNode.next();
  70. if (next && newLineMatch && !reNewLine.test(next.raws.before || '')) {
  71. backupCommentNextBefores.set(next, next.raws.before);
  72. next.raws.before = startNode.raws.before;
  73. }
  74. return nextNode(next);
  75. }
  76. return startNode;
  77. }
  78. // Allow an end-of-line comment
  79. const nodeToCheck = nextNode(statement.first);
  80. if (!nodeToCheck) {
  81. return;
  82. }
  83. checker.afterOneOnly({
  84. source: rawNodeString(nodeToCheck),
  85. index: -1,
  86. lineCheckStr: blockString(statement),
  87. err: (m) => {
  88. if (context.fix) {
  89. const nodeToCheckRaws = nodeToCheck.raws;
  90. if (typeof nodeToCheckRaws.before !== 'string') return;
  91. if (primary.startsWith('always')) {
  92. const index = nodeToCheckRaws.before.search(/\r?\n/);
  93. nodeToCheckRaws.before =
  94. index >= 0
  95. ? nodeToCheckRaws.before.slice(index)
  96. : context.newline + nodeToCheckRaws.before;
  97. backupCommentNextBefores.delete(nodeToCheck);
  98. return;
  99. }
  100. if (primary === 'never-multi-line') {
  101. // Restore the `before` of the node next to the comment node.
  102. for (const [node, before] of backupCommentNextBefores.entries()) {
  103. node.raws.before = before;
  104. }
  105. backupCommentNextBefores.clear();
  106. // Fix
  107. const reNewLine = /\r?\n/;
  108. let fixTarget = statement.first;
  109. while (fixTarget) {
  110. const fixTargetRaws = fixTarget.raws;
  111. if (typeof fixTargetRaws.before !== 'string') continue;
  112. if (reNewLine.test(fixTargetRaws.before || '')) {
  113. fixTargetRaws.before = fixTargetRaws.before.replace(/\r?\n/g, '');
  114. }
  115. if (fixTarget.type !== 'comment') {
  116. break;
  117. }
  118. fixTarget = fixTarget.next();
  119. }
  120. nodeToCheckRaws.before = '';
  121. return;
  122. }
  123. }
  124. report({
  125. message: m,
  126. node: statement,
  127. index: beforeBlockString(statement, { noRawBefore: true }).length + 1,
  128. result,
  129. ruleName,
  130. });
  131. },
  132. });
  133. // Restore the `before` of the node next to the comment node.
  134. for (const [node, before] of backupCommentNextBefores.entries()) {
  135. node.raws.before = before;
  136. }
  137. }
  138. };
  139. };
  140. rule.ruleName = ruleName;
  141. rule.messages = messages;
  142. rule.meta = meta;
  143. module.exports = rule;