index.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. 'use strict';
  2. const isStandardSyntaxAtRule = require('../../utils/isStandardSyntaxAtRule');
  3. const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
  4. const report = require('../../utils/report');
  5. const ruleMessages = require('../../utils/ruleMessages');
  6. const styleSearch = require('style-search');
  7. const validateOptions = require('../../utils/validateOptions');
  8. const { isAtRule } = require('../../utils/typeGuards');
  9. const ruleName = 'no-extra-semicolons';
  10. const messages = ruleMessages(ruleName, {
  11. rejected: 'Unexpected extra semicolon',
  12. });
  13. const meta = {
  14. url: 'https://stylelint.io/user-guide/rules/no-extra-semicolons',
  15. fixable: true,
  16. };
  17. /**
  18. * @param {import('postcss').Node} node
  19. * @returns {number}
  20. */
  21. function getOffsetByNode(node) {
  22. // @ts-expect-error -- TS2339: Property 'document' does not exist on type 'Document | Container<ChildNode>'
  23. if (node.parent && node.parent.document) {
  24. return 0;
  25. }
  26. const root = node.root();
  27. if (!root.source) throw new Error('The root node must have a source');
  28. if (!node.source) throw new Error('The node must have a source');
  29. if (!node.source.start) throw new Error('The source must have a start position');
  30. const string = root.source.input.css;
  31. const nodeColumn = node.source.start.column;
  32. const nodeLine = node.source.start.line;
  33. let line = 1;
  34. let column = 1;
  35. let index = 0;
  36. for (let i = 0; i < string.length; i++) {
  37. if (column === nodeColumn && nodeLine === line) {
  38. index = i;
  39. break;
  40. }
  41. if (string[i] === '\n') {
  42. column = 1;
  43. line += 1;
  44. } else {
  45. column += 1;
  46. }
  47. }
  48. return index;
  49. }
  50. /** @type {import('stylelint').Rule} */
  51. const rule = (primary, _secondaryOptions, context) => {
  52. return (root, result) => {
  53. const validOptions = validateOptions(result, ruleName, { actual: primary });
  54. if (!validOptions) {
  55. return;
  56. }
  57. if (root.raws.after && root.raws.after.trim().length !== 0) {
  58. const rawAfterRoot = root.raws.after;
  59. /** @type {number[]} */
  60. const fixSemiIndices = [];
  61. styleSearch({ source: rawAfterRoot, target: ';' }, (match) => {
  62. if (context.fix) {
  63. fixSemiIndices.push(match.startIndex);
  64. return;
  65. }
  66. if (!root.source) throw new Error('The root node must have a source');
  67. complain(root.source.input.css.length - rawAfterRoot.length + match.startIndex);
  68. });
  69. // fix
  70. if (fixSemiIndices.length) {
  71. root.raws.after = removeIndices(rawAfterRoot, fixSemiIndices);
  72. }
  73. }
  74. root.walk((node) => {
  75. if (isAtRule(node) && !isStandardSyntaxAtRule(node)) {
  76. return;
  77. }
  78. if (node.type === 'rule' && !isStandardSyntaxRule(node)) {
  79. return;
  80. }
  81. if (node.raws.before && node.raws.before.trim().length !== 0) {
  82. const rawBeforeNode = node.raws.before;
  83. const allowedSemi = 0;
  84. const rawBeforeIndexStart = 0;
  85. /** @type {number[]} */
  86. const fixSemiIndices = [];
  87. styleSearch({ source: rawBeforeNode, target: ';' }, (match, count) => {
  88. if (count === allowedSemi) {
  89. return;
  90. }
  91. if (context.fix) {
  92. fixSemiIndices.push(match.startIndex - rawBeforeIndexStart);
  93. return;
  94. }
  95. complain(getOffsetByNode(node) - rawBeforeNode.length + match.startIndex);
  96. });
  97. // fix
  98. if (fixSemiIndices.length) {
  99. node.raws.before = removeIndices(rawBeforeNode, fixSemiIndices);
  100. }
  101. }
  102. if (typeof node.raws.after === 'string' && node.raws.after.trim().length !== 0) {
  103. const rawAfterNode = node.raws.after;
  104. /**
  105. * If the last child is a Less mixin followed by more than one semicolon,
  106. * node.raws.after will be populated with that semicolon.
  107. * Since we ignore Less mixins, exit here
  108. */
  109. if (
  110. 'last' in node &&
  111. node.last &&
  112. node.last.type === 'atrule' &&
  113. !isStandardSyntaxAtRule(node.last)
  114. ) {
  115. return;
  116. }
  117. /** @type {number[]} */
  118. const fixSemiIndices = [];
  119. styleSearch({ source: rawAfterNode, target: ';' }, (match) => {
  120. if (context.fix) {
  121. fixSemiIndices.push(match.startIndex);
  122. return;
  123. }
  124. const index =
  125. getOffsetByNode(node) +
  126. node.toString().length -
  127. 1 -
  128. rawAfterNode.length +
  129. match.startIndex;
  130. complain(index);
  131. });
  132. // fix
  133. if (fixSemiIndices.length) {
  134. node.raws.after = removeIndices(rawAfterNode, fixSemiIndices);
  135. }
  136. }
  137. if (typeof node.raws.ownSemicolon === 'string') {
  138. const rawOwnSemicolon = node.raws.ownSemicolon;
  139. const allowedSemi = 0;
  140. /** @type {number[]} */
  141. const fixSemiIndices = [];
  142. styleSearch({ source: rawOwnSemicolon, target: ';' }, (match, count) => {
  143. if (count === allowedSemi) {
  144. return;
  145. }
  146. if (context.fix) {
  147. fixSemiIndices.push(match.startIndex);
  148. return;
  149. }
  150. const index =
  151. getOffsetByNode(node) +
  152. node.toString().length -
  153. rawOwnSemicolon.length +
  154. match.startIndex;
  155. complain(index);
  156. });
  157. // fix
  158. if (fixSemiIndices.length) {
  159. node.raws.ownSemicolon = removeIndices(rawOwnSemicolon, fixSemiIndices);
  160. }
  161. }
  162. });
  163. /**
  164. * @param {number} index
  165. */
  166. function complain(index) {
  167. report({
  168. message: messages.rejected,
  169. node: root,
  170. index,
  171. result,
  172. ruleName,
  173. });
  174. }
  175. /**
  176. * @param {string} str
  177. * @param {number[]} indices
  178. * @returns {string}
  179. */
  180. function removeIndices(str, indices) {
  181. for (const index of indices.reverse()) {
  182. str = str.slice(0, index) + str.slice(index + 1);
  183. }
  184. return str;
  185. }
  186. };
  187. };
  188. rule.ruleName = ruleName;
  189. rule.messages = messages;
  190. rule.meta = meta;
  191. module.exports = rule;