• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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