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