1 | /**
|
2 | * @fileoverview Disallow trailing spaces at the end of lines.
|
3 | * @author Nodeca Team <https://github.com/nodeca>
|
4 | */
|
5 | ;
|
6 |
|
7 | //------------------------------------------------------------------------------
|
8 | // Requirements
|
9 | //------------------------------------------------------------------------------
|
10 |
|
11 | const astUtils = require("./utils/ast-utils");
|
12 |
|
13 | //------------------------------------------------------------------------------
|
14 | // Rule Definition
|
15 | //------------------------------------------------------------------------------
|
16 |
|
17 | module.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 | };
|