index.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. 'use strict';
  2. const styleSearch = require('style-search');
  3. const isOnlyWhitespace = require('../../utils/isOnlyWhitespace');
  4. const isStandardSyntaxComment = require('../../utils/isStandardSyntaxComment');
  5. const optionsMatches = require('../../utils/optionsMatches');
  6. const report = require('../../utils/report');
  7. const ruleMessages = require('../../utils/ruleMessages');
  8. const { isAtRule, isComment, isDeclaration, isRule } = require('../../utils/typeGuards');
  9. const validateOptions = require('../../utils/validateOptions');
  10. const ruleName = 'no-eol-whitespace';
  11. const messages = ruleMessages(ruleName, {
  12. rejected: 'Unexpected whitespace at end of line',
  13. });
  14. const meta = {
  15. url: 'https://stylelint.io/user-guide/rules/no-eol-whitespace',
  16. fixable: true,
  17. deprecated: true,
  18. };
  19. const whitespacesToReject = new Set([' ', '\t']);
  20. /**
  21. * @param {string} str
  22. * @returns {string}
  23. */
  24. function fixString(str) {
  25. return str.replace(/[ \t]+$/, '');
  26. }
  27. /**
  28. * @param {number} lastEOLIndex
  29. * @param {string} string
  30. * @param {{ ignoreEmptyLines: boolean, isRootFirst: boolean }} options
  31. * @returns {number}
  32. */
  33. function findErrorStartIndex(lastEOLIndex, string, { ignoreEmptyLines, isRootFirst }) {
  34. const eolWhitespaceIndex = lastEOLIndex - 1;
  35. // If the character before newline is not whitespace, ignore
  36. if (!whitespacesToReject.has(string.charAt(eolWhitespaceIndex))) {
  37. return -1;
  38. }
  39. if (ignoreEmptyLines) {
  40. // If there is only whitespace between the previous newline and
  41. // this newline, ignore
  42. const beforeNewlineIndex = string.lastIndexOf('\n', eolWhitespaceIndex);
  43. if (beforeNewlineIndex >= 0 || isRootFirst) {
  44. const line = string.substring(beforeNewlineIndex, eolWhitespaceIndex);
  45. if (isOnlyWhitespace(line)) {
  46. return -1;
  47. }
  48. }
  49. }
  50. return eolWhitespaceIndex;
  51. }
  52. /** @type {import('stylelint').Rule} */
  53. const rule = (primary, secondaryOptions, context) => {
  54. return (root, result) => {
  55. const validOptions = validateOptions(
  56. result,
  57. ruleName,
  58. {
  59. actual: primary,
  60. },
  61. {
  62. optional: true,
  63. actual: secondaryOptions,
  64. possible: {
  65. ignore: ['empty-lines'],
  66. },
  67. },
  68. );
  69. if (!validOptions) {
  70. return;
  71. }
  72. const ignoreEmptyLines = optionsMatches(secondaryOptions, 'ignore', 'empty-lines');
  73. if (context.fix) {
  74. fix(root);
  75. }
  76. const rootString = context.fix ? root.toString() : (root.source && root.source.input.css) || '';
  77. /**
  78. * @param {number} index
  79. */
  80. const reportFromIndex = (index) => {
  81. report({
  82. message: messages.rejected,
  83. node: root,
  84. index,
  85. result,
  86. ruleName,
  87. });
  88. };
  89. eachEolWhitespace(rootString, reportFromIndex, true);
  90. const errorIndex = findErrorStartIndex(rootString.length, rootString, {
  91. ignoreEmptyLines,
  92. isRootFirst: true,
  93. });
  94. if (errorIndex > -1) {
  95. reportFromIndex(errorIndex);
  96. }
  97. /**
  98. * Iterate each whitespace at the end of each line of the given string.
  99. * @param {string} string - the source code string
  100. * @param {(index: number) => void} callback - callback the whitespace index at the end of each line.
  101. * @param {boolean} isRootFirst - set `true` if the given string is the first token of the root.
  102. * @returns {void}
  103. */
  104. function eachEolWhitespace(string, callback, isRootFirst) {
  105. styleSearch(
  106. {
  107. source: string,
  108. target: ['\n', '\r'],
  109. comments: 'check',
  110. },
  111. (match) => {
  112. const index = findErrorStartIndex(match.startIndex, string, {
  113. ignoreEmptyLines,
  114. isRootFirst,
  115. });
  116. if (index > -1) {
  117. callback(index);
  118. }
  119. },
  120. );
  121. }
  122. /**
  123. * @param {import('postcss').Root} rootNode
  124. */
  125. function fix(rootNode) {
  126. let isRootFirst = true;
  127. rootNode.walk((node) => {
  128. fixText(
  129. node.raws.before,
  130. (fixed) => {
  131. node.raws.before = fixed;
  132. },
  133. isRootFirst,
  134. );
  135. isRootFirst = false;
  136. if (isAtRule(node)) {
  137. fixText(node.raws.afterName, (fixed) => {
  138. node.raws.afterName = fixed;
  139. });
  140. const rawsParams = node.raws.params;
  141. if (rawsParams) {
  142. fixText(rawsParams.raw, (fixed) => {
  143. rawsParams.raw = fixed;
  144. });
  145. } else {
  146. fixText(node.params, (fixed) => {
  147. node.params = fixed;
  148. });
  149. }
  150. }
  151. if (isRule(node)) {
  152. const rawsSelector = node.raws.selector;
  153. if (rawsSelector) {
  154. fixText(rawsSelector.raw, (fixed) => {
  155. rawsSelector.raw = fixed;
  156. });
  157. } else {
  158. fixText(node.selector, (fixed) => {
  159. node.selector = fixed;
  160. });
  161. }
  162. }
  163. if (isAtRule(node) || isRule(node) || isDeclaration(node)) {
  164. fixText(node.raws.between, (fixed) => {
  165. node.raws.between = fixed;
  166. });
  167. }
  168. if (isDeclaration(node)) {
  169. const rawsValue = node.raws.value;
  170. if (rawsValue) {
  171. fixText(rawsValue.raw, (fixed) => {
  172. rawsValue.raw = fixed;
  173. });
  174. } else {
  175. fixText(node.value, (fixed) => {
  176. node.value = fixed;
  177. });
  178. }
  179. }
  180. if (isComment(node)) {
  181. fixText(node.raws.left, (fixed) => {
  182. node.raws.left = fixed;
  183. });
  184. if (!isStandardSyntaxComment(node)) {
  185. node.raws.right = node.raws.right && fixString(node.raws.right);
  186. } else {
  187. fixText(node.raws.right, (fixed) => {
  188. node.raws.right = fixed;
  189. });
  190. }
  191. fixText(node.text, (fixed) => {
  192. node.text = fixed;
  193. });
  194. }
  195. if (isAtRule(node) || isRule(node)) {
  196. fixText(node.raws.after, (fixed) => {
  197. node.raws.after = fixed;
  198. });
  199. }
  200. });
  201. fixText(
  202. rootNode.raws.after,
  203. (fixed) => {
  204. rootNode.raws.after = fixed;
  205. },
  206. isRootFirst,
  207. );
  208. if (typeof rootNode.raws.after === 'string') {
  209. const lastEOL = Math.max(
  210. rootNode.raws.after.lastIndexOf('\n'),
  211. rootNode.raws.after.lastIndexOf('\r'),
  212. );
  213. if (lastEOL !== rootNode.raws.after.length - 1) {
  214. rootNode.raws.after =
  215. rootNode.raws.after.slice(0, lastEOL + 1) +
  216. fixString(rootNode.raws.after.slice(lastEOL + 1));
  217. }
  218. }
  219. }
  220. /**
  221. * @param {string | undefined} value
  222. * @param {(text: string) => void} fixFn
  223. * @param {boolean} isRootFirst
  224. */
  225. function fixText(value, fixFn, isRootFirst = false) {
  226. if (!value) {
  227. return;
  228. }
  229. let fixed = '';
  230. let lastIndex = 0;
  231. eachEolWhitespace(
  232. value,
  233. (index) => {
  234. const newlineIndex = index + 1;
  235. fixed += fixString(value.slice(lastIndex, newlineIndex));
  236. lastIndex = newlineIndex;
  237. },
  238. isRootFirst,
  239. );
  240. if (lastIndex) {
  241. fixed += value.slice(lastIndex);
  242. fixFn(fixed);
  243. }
  244. }
  245. };
  246. };
  247. rule.ruleName = ruleName;
  248. rule.messages = messages;
  249. rule.meta = meta;
  250. module.exports = rule;