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