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