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