summaryrefslogtreecommitdiff
path: root/tools/eslint/lib/rules/semi-style.js
blob: 41fb39246c7514080f2eb835033a38b259539d5d (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
/**
 * @fileoverview Rule to enforce location of semicolons.
 * @author Toru Nagashima
 */

"use strict";

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

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

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

const SELECTOR = `:matches(${
    [
        "BreakStatement", "ContinueStatement", "DebuggerStatement",
        "DoWhileStatement", "ExportAllDeclaration",
        "ExportDefaultDeclaration", "ExportNamedDeclaration",
        "ExpressionStatement", "ImportDeclaration", "ReturnStatement",
        "ThrowStatement", "VariableDeclaration"
    ].join(",")
})`;

/**
 * Get the child node list of a given node.
 * This returns `Program#body`, `BlockStatement#body`, or `SwitchCase#consequent`.
 * This is used to check whether a node is the first/last child.
 * @param {Node} node A node to get child node list.
 * @returns {Node[]|null} The child node list.
 */
function getChildren(node) {
    const t = node.type;

    if (t === "BlockStatement" || t === "Program") {
        return node.body;
    }
    if (t === "SwitchCase") {
        return node.consequent;
    }
    return null;
}

/**
 * Check whether a given node is the last statement in the parent block.
 * @param {Node} node A node to check.
 * @returns {boolean} `true` if the node is the last statement in the parent block.
 */
function isLastChild(node) {
    const t = node.parent.type;

    if (t === "IfStatement" && node.parent.consequent === node && node.parent.alternate) { // before `else` keyword.
        return true;
    }
    if (t === "DoWhileStatement") { // before `while` keyword.
        return true;
    }
    const nodeList = getChildren(node.parent);

    return nodeList !== null && nodeList[nodeList.length - 1] === node; // before `}` or etc.
}

module.exports = {
    meta: {
        docs: {
            description: "enforce location of semicolons",
            category: "Stylistic Issues",
            recommended: false
        },
        schema: [{ enum: ["last", "first"] }],
        fixable: "whitespace"
    },

    create(context) {
        const sourceCode = context.getSourceCode();
        const option = context.options[0] || "last";

        /**
         * Check the given semicolon token.
         * @param {Token} semiToken The semicolon token to check.
         * @param {"first"|"last"} expected The expected location to check.
         * @returns {void}
         */
        function check(semiToken, expected) {
            const prevToken = sourceCode.getTokenBefore(semiToken);
            const nextToken = sourceCode.getTokenAfter(semiToken);
            const prevIsSameLine = !prevToken || astUtils.isTokenOnSameLine(prevToken, semiToken);
            const nextIsSameLine = !nextToken || astUtils.isTokenOnSameLine(semiToken, nextToken);

            if ((expected === "last" && !prevIsSameLine) || (expected === "first" && !nextIsSameLine)) {
                context.report({
                    loc: semiToken.loc,
                    message: "Expected this semicolon to be at {{pos}}.",
                    data: {
                        pos: (expected === "last")
                            ? "the end of the previous line"
                            : "the beginning of the next line"
                    },
                    fix(fixer) {
                        if (prevToken && nextToken && sourceCode.commentsExistBetween(prevToken, nextToken)) {
                            return null;
                        }

                        const start = prevToken ? prevToken.range[1] : semiToken.range[0];
                        const end = nextToken ? nextToken.range[0] : semiToken.range[1];
                        const text = (expected === "last") ? ";\n" : "\n;";

                        return fixer.replaceTextRange([start, end], text);
                    }
                });
            }
        }

        return {
            [SELECTOR](node) {
                if (option === "first" && isLastChild(node)) {
                    return;
                }

                const lastToken = sourceCode.getLastToken(node);

                if (astUtils.isSemicolonToken(lastToken)) {
                    check(lastToken, option);
                }
            },

            ForStatement(node) {
                const firstSemi = node.init && sourceCode.getTokenAfter(node.init, astUtils.isSemicolonToken);
                const secondSemi = node.test && sourceCode.getTokenAfter(node.test, astUtils.isSemicolonToken);

                if (firstSemi) {
                    check(firstSemi, "last");
                }
                if (secondSemi) {
                    check(secondSemi, "last");
                }
            }
        };
    }
};