summaryrefslogtreecommitdiff
path: root/tools/node_modules/eslint/lib/rules/newline-after-var.js
blob: 8f244149c5574ad4c071b53e676a30f02260be12 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
/**
 * @fileoverview Rule to check empty newline after "var" statement
 * @author Gopal Venkatesan
 * @deprecated
 */

"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const astUtils = require("./utils/ast-utils");

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

module.exports = {
    meta: {
        type: "layout",

        docs: {
            description: "require or disallow an empty line after variable declarations",
            category: "Stylistic Issues",
            recommended: false,
            url: "https://eslint.org/docs/rules/newline-after-var"
        },
        schema: [
            {
                enum: ["never", "always"]
            }
        ],
        fixable: "whitespace",
        messages: {
            expected: "Expected blank line after variable declarations.",
            unexpected: "Unexpected blank line after variable declarations."
        },

        deprecated: true,

        replacedBy: ["padding-line-between-statements"]
    },

    create(context) {
        const sourceCode = context.getSourceCode();

        // Default `mode` to "always".
        const mode = context.options[0] === "never" ? "never" : "always";

        // Cache starting and ending line numbers of comments for faster lookup
        const commentEndLine = sourceCode.getAllComments().reduce((result, token) => {
            result[token.loc.start.line] = token.loc.end.line;
            return result;
        }, {});


        //--------------------------------------------------------------------------
        // Helpers
        //--------------------------------------------------------------------------

        /**
         * Gets a token from the given node to compare line to the next statement.
         *
         * In general, the token is the last token of the node. However, the token is the second last token if the following conditions satisfy.
         *
         * - The last token is semicolon.
         * - The semicolon is on a different line from the previous token of the semicolon.
         *
         * This behavior would address semicolon-less style code. e.g.:
         *
         *     var foo = 1
         *
         *     ;(a || b).doSomething()
         *
         * @param {ASTNode} node - The node to get.
         * @returns {Token} The token to compare line to the next statement.
         */
        function getLastToken(node) {
            const lastToken = sourceCode.getLastToken(node);

            if (lastToken.type === "Punctuator" && lastToken.value === ";") {
                const prevToken = sourceCode.getTokenBefore(lastToken);

                if (prevToken.loc.end.line !== lastToken.loc.start.line) {
                    return prevToken;
                }
            }

            return lastToken;
        }

        /**
         * Determine if provided keyword is a variable declaration
         * @private
         * @param {string} keyword - keyword to test
         * @returns {boolean} True if `keyword` is a type of var
         */
        function isVar(keyword) {
            return keyword === "var" || keyword === "let" || keyword === "const";
        }

        /**
         * Determine if provided keyword is a variant of for specifiers
         * @private
         * @param {string} keyword - keyword to test
         * @returns {boolean} True if `keyword` is a variant of for specifier
         */
        function isForTypeSpecifier(keyword) {
            return keyword === "ForStatement" || keyword === "ForInStatement" || keyword === "ForOfStatement";
        }

        /**
         * Determine if provided keyword is an export specifiers
         * @private
         * @param {string} nodeType - nodeType to test
         * @returns {boolean} True if `nodeType` is an export specifier
         */
        function isExportSpecifier(nodeType) {
            return nodeType === "ExportNamedDeclaration" || nodeType === "ExportSpecifier" ||
                nodeType === "ExportDefaultDeclaration" || nodeType === "ExportAllDeclaration";
        }

        /**
         * Determine if provided node is the last of their parent block.
         * @private
         * @param {ASTNode} node - node to test
         * @returns {boolean} True if `node` is last of their parent block.
         */
        function isLastNode(node) {
            const token = sourceCode.getTokenAfter(node);

            return !token || (token.type === "Punctuator" && token.value === "}");
        }

        /**
         * Gets the last line of a group of consecutive comments
         * @param {number} commentStartLine The starting line of the group
         * @returns {number} The number of the last comment line of the group
         */
        function getLastCommentLineOfBlock(commentStartLine) {
            const currentCommentEnd = commentEndLine[commentStartLine];

            return commentEndLine[currentCommentEnd + 1] ? getLastCommentLineOfBlock(currentCommentEnd + 1) : currentCommentEnd;
        }

        /**
         * Determine if a token starts more than one line after a comment ends
         * @param  {token}   token            The token being checked
         * @param {integer}  commentStartLine The line number on which the comment starts
         * @returns {boolean}                 True if `token` does not start immediately after a comment
         */
        function hasBlankLineAfterComment(token, commentStartLine) {
            return token.loc.start.line > getLastCommentLineOfBlock(commentStartLine) + 1;
        }

        /**
         * Checks that a blank line exists after a variable declaration when mode is
         * set to "always", or checks that there is no blank line when mode is set
         * to "never"
         * @private
         * @param {ASTNode} node - `VariableDeclaration` node to test
         * @returns {void}
         */
        function checkForBlankLine(node) {

            /*
             * lastToken is the last token on the node's line. It will usually also be the last token of the node, but it will
             * sometimes be second-last if there is a semicolon on a different line.
             */
            const lastToken = getLastToken(node),

                /*
                 * If lastToken is the last token of the node, nextToken should be the token after the node. Otherwise, nextToken
                 * is the last token of the node.
                 */
                nextToken = lastToken === sourceCode.getLastToken(node) ? sourceCode.getTokenAfter(node) : sourceCode.getLastToken(node),
                nextLineNum = lastToken.loc.end.line + 1;

            // Ignore if there is no following statement
            if (!nextToken) {
                return;
            }

            // Ignore if parent of node is a for variant
            if (isForTypeSpecifier(node.parent.type)) {
                return;
            }

            // Ignore if parent of node is an export specifier
            if (isExportSpecifier(node.parent.type)) {
                return;
            }

            /*
             * Some coding styles use multiple `var` statements, so do nothing if
             * the next token is a `var` statement.
             */
            if (nextToken.type === "Keyword" && isVar(nextToken.value)) {
                return;
            }

            // Ignore if it is last statement in a block
            if (isLastNode(node)) {
                return;
            }

            // Next statement is not a `var`...
            const noNextLineToken = nextToken.loc.start.line > nextLineNum;
            const hasNextLineComment = (typeof commentEndLine[nextLineNum] !== "undefined");

            if (mode === "never" && noNextLineToken && !hasNextLineComment) {
                context.report({
                    node,
                    messageId: "unexpected",
                    data: { identifier: node.name },
                    fix(fixer) {
                        const linesBetween = sourceCode.getText().slice(lastToken.range[1], nextToken.range[0]).split(astUtils.LINEBREAK_MATCHER);

                        return fixer.replaceTextRange([lastToken.range[1], nextToken.range[0]], `${linesBetween.slice(0, -1).join("")}\n${linesBetween[linesBetween.length - 1]}`);
                    }
                });
            }

            // Token on the next line, or comment without blank line
            if (
                mode === "always" && (
                    !noNextLineToken ||
                    hasNextLineComment && !hasBlankLineAfterComment(nextToken, nextLineNum)
                )
            ) {
                context.report({
                    node,
                    messageId: "expected",
                    data: { identifier: node.name },
                    fix(fixer) {
                        if ((noNextLineToken ? getLastCommentLineOfBlock(nextLineNum) : lastToken.loc.end.line) === nextToken.loc.start.line) {
                            return fixer.insertTextBefore(nextToken, "\n\n");
                        }

                        return fixer.insertTextBeforeRange([nextToken.range[0] - nextToken.loc.start.column, nextToken.range[1]], "\n");
                    }
                });
            }
        }

        //--------------------------------------------------------------------------
        // Public
        //--------------------------------------------------------------------------

        return {
            VariableDeclaration: checkForBlankLine
        };

    }
};