index.js 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. 'use strict';
  2. const hasBlock = require('../../utils/hasBlock');
  3. const optionsMatches = require('../../utils/optionsMatches');
  4. const report = require('../../utils/report');
  5. const ruleMessages = require('../../utils/ruleMessages');
  6. const validateOptions = require('../../utils/validateOptions');
  7. const ruleName = 'declaration-block-trailing-semicolon';
  8. const messages = ruleMessages(ruleName, {
  9. expected: 'Expected a trailing semicolon',
  10. rejected: 'Unexpected trailing semicolon',
  11. });
  12. const meta = {
  13. url: 'https://stylelint.io/user-guide/rules/list/declaration-block-trailing-semicolon',
  14. };
  15. /** @type {import('stylelint').Rule} */
  16. const rule = (primary, secondaryOptions, context) => {
  17. return (root, result) => {
  18. const validOptions = validateOptions(
  19. result,
  20. ruleName,
  21. {
  22. actual: primary,
  23. possible: ['always', 'never'],
  24. },
  25. {
  26. actual: secondaryOptions,
  27. possible: {
  28. ignore: ['single-declaration'],
  29. },
  30. optional: true,
  31. },
  32. );
  33. if (!validOptions) {
  34. return;
  35. }
  36. root.walkAtRules((atRule) => {
  37. if (!atRule.parent) throw new Error('A parent node must be present');
  38. if (atRule.parent === root) {
  39. return;
  40. }
  41. if (atRule !== atRule.parent.last) {
  42. return;
  43. }
  44. if (hasBlock(atRule)) {
  45. return;
  46. }
  47. checkLastNode(atRule);
  48. });
  49. root.walkDecls((decl) => {
  50. if (!decl.parent) throw new Error('A parent node must be present');
  51. if (decl.parent.type === 'object') {
  52. return;
  53. }
  54. if (decl !== decl.parent.last) {
  55. return;
  56. }
  57. checkLastNode(decl);
  58. });
  59. /**
  60. * @param {import('postcss').Node} node
  61. */
  62. function checkLastNode(node) {
  63. if (!node.parent) throw new Error('A parent node must be present');
  64. const hasSemicolon = node.parent.raws.semicolon;
  65. const ignoreSingleDeclaration = optionsMatches(
  66. secondaryOptions,
  67. 'ignore',
  68. 'single-declaration',
  69. );
  70. if (ignoreSingleDeclaration && node.parent.first === node) {
  71. return;
  72. }
  73. let message;
  74. if (primary === 'always') {
  75. if (hasSemicolon) {
  76. return;
  77. }
  78. // auto-fix
  79. if (context.fix) {
  80. node.parent.raws.semicolon = true;
  81. if (node.type === 'atrule') {
  82. node.raws.between = '';
  83. node.parent.raws.after = ' ';
  84. }
  85. return;
  86. }
  87. message = messages.expected;
  88. } else if (primary === 'never') {
  89. if (!hasSemicolon) {
  90. return;
  91. }
  92. // auto-fix
  93. if (context.fix) {
  94. node.parent.raws.semicolon = false;
  95. return;
  96. }
  97. message = messages.rejected;
  98. } else {
  99. throw new Error(`Unexpected primary option: "${primary}"`);
  100. }
  101. report({
  102. message,
  103. node,
  104. index: node.toString().trim().length - 1,
  105. result,
  106. ruleName,
  107. });
  108. }
  109. };
  110. };
  111. rule.ruleName = ruleName;
  112. rule.messages = messages;
  113. rule.meta = meta;
  114. module.exports = rule;