no-trailing-spaces.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. /**
  2. * @fileoverview Disallow trailing spaces at the end of lines.
  3. * @author Nodeca Team <https://github.com/nodeca>
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Rule Definition
  12. //------------------------------------------------------------------------------
  13. /** @type {import('../shared/types').Rule} */
  14. module.exports = {
  15. meta: {
  16. type: "layout",
  17. docs: {
  18. description: "Disallow trailing whitespace at the end of lines",
  19. recommended: false,
  20. url: "https://eslint.org/docs/rules/no-trailing-spaces"
  21. },
  22. fixable: "whitespace",
  23. schema: [
  24. {
  25. type: "object",
  26. properties: {
  27. skipBlankLines: {
  28. type: "boolean",
  29. default: false
  30. },
  31. ignoreComments: {
  32. type: "boolean",
  33. default: false
  34. }
  35. },
  36. additionalProperties: false
  37. }
  38. ],
  39. messages: {
  40. trailingSpace: "Trailing spaces not allowed."
  41. }
  42. },
  43. create(context) {
  44. const sourceCode = context.getSourceCode();
  45. const BLANK_CLASS = "[ \t\u00a0\u2000-\u200b\u3000]",
  46. SKIP_BLANK = `^${BLANK_CLASS}*$`,
  47. NONBLANK = `${BLANK_CLASS}+$`;
  48. const options = context.options[0] || {},
  49. skipBlankLines = options.skipBlankLines || false,
  50. ignoreComments = options.ignoreComments || false;
  51. /**
  52. * Report the error message
  53. * @param {ASTNode} node node to report
  54. * @param {int[]} location range information
  55. * @param {int[]} fixRange Range based on the whole program
  56. * @returns {void}
  57. */
  58. function report(node, location, fixRange) {
  59. /*
  60. * Passing node is a bit dirty, because message data will contain big
  61. * text in `source`. But... who cares :) ?
  62. * One more kludge will not make worse the bloody wizardry of this
  63. * plugin.
  64. */
  65. context.report({
  66. node,
  67. loc: location,
  68. messageId: "trailingSpace",
  69. fix(fixer) {
  70. return fixer.removeRange(fixRange);
  71. }
  72. });
  73. }
  74. /**
  75. * Given a list of comment nodes, return the line numbers for those comments.
  76. * @param {Array} comments An array of comment nodes.
  77. * @returns {number[]} An array of line numbers containing comments.
  78. */
  79. function getCommentLineNumbers(comments) {
  80. const lines = new Set();
  81. comments.forEach(comment => {
  82. const endLine = comment.type === "Block"
  83. ? comment.loc.end.line - 1
  84. : comment.loc.end.line;
  85. for (let i = comment.loc.start.line; i <= endLine; i++) {
  86. lines.add(i);
  87. }
  88. });
  89. return lines;
  90. }
  91. //--------------------------------------------------------------------------
  92. // Public
  93. //--------------------------------------------------------------------------
  94. return {
  95. Program: function checkTrailingSpaces(node) {
  96. /*
  97. * Let's hack. Since Espree does not return whitespace nodes,
  98. * fetch the source code and do matching via regexps.
  99. */
  100. const re = new RegExp(NONBLANK, "u"),
  101. skipMatch = new RegExp(SKIP_BLANK, "u"),
  102. lines = sourceCode.lines,
  103. linebreaks = sourceCode.getText().match(astUtils.createGlobalLinebreakMatcher()),
  104. comments = sourceCode.getAllComments(),
  105. commentLineNumbers = getCommentLineNumbers(comments);
  106. let totalLength = 0,
  107. fixRange = [];
  108. for (let i = 0, ii = lines.length; i < ii; i++) {
  109. const lineNumber = i + 1;
  110. /*
  111. * Always add linebreak length to line length to accommodate for line break (\n or \r\n)
  112. * Because during the fix time they also reserve one spot in the array.
  113. * Usually linebreak length is 2 for \r\n (CRLF) and 1 for \n (LF)
  114. */
  115. const linebreakLength = linebreaks && linebreaks[i] ? linebreaks[i].length : 1;
  116. const lineLength = lines[i].length + linebreakLength;
  117. const matches = re.exec(lines[i]);
  118. if (matches) {
  119. const location = {
  120. start: {
  121. line: lineNumber,
  122. column: matches.index
  123. },
  124. end: {
  125. line: lineNumber,
  126. column: lineLength - linebreakLength
  127. }
  128. };
  129. const rangeStart = totalLength + location.start.column;
  130. const rangeEnd = totalLength + location.end.column;
  131. const containingNode = sourceCode.getNodeByRangeIndex(rangeStart);
  132. if (containingNode && containingNode.type === "TemplateElement" &&
  133. rangeStart > containingNode.parent.range[0] &&
  134. rangeEnd < containingNode.parent.range[1]) {
  135. totalLength += lineLength;
  136. continue;
  137. }
  138. /*
  139. * If the line has only whitespace, and skipBlankLines
  140. * is true, don't report it
  141. */
  142. if (skipBlankLines && skipMatch.test(lines[i])) {
  143. totalLength += lineLength;
  144. continue;
  145. }
  146. fixRange = [rangeStart, rangeEnd];
  147. if (!ignoreComments || !commentLineNumbers.has(lineNumber)) {
  148. report(node, location, fixRange);
  149. }
  150. }
  151. totalLength += lineLength;
  152. }
  153. }
  154. };
  155. }
  156. };