• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/**
2 * @fileoverview Disallow use of multiple spaces.
3 * @author Nicholas C. Zakas
4 */
5
6"use strict";
7
8const astUtils = require("./utils/ast-utils");
9
10//------------------------------------------------------------------------------
11// Rule Definition
12//------------------------------------------------------------------------------
13
14module.exports = {
15    meta: {
16        type: "layout",
17
18        docs: {
19            description: "disallow multiple spaces",
20            category: "Best Practices",
21            recommended: false,
22            url: "https://eslint.org/docs/rules/no-multi-spaces"
23        },
24
25        fixable: "whitespace",
26
27        schema: [
28            {
29                type: "object",
30                properties: {
31                    exceptions: {
32                        type: "object",
33                        patternProperties: {
34                            "^([A-Z][a-z]*)+$": {
35                                type: "boolean"
36                            }
37                        },
38                        additionalProperties: false
39                    },
40                    ignoreEOLComments: {
41                        type: "boolean",
42                        default: false
43                    }
44                },
45                additionalProperties: false
46            }
47        ],
48
49        messages: {
50            multipleSpaces: "Multiple spaces found before '{{displayValue}}'."
51        }
52    },
53
54    create(context) {
55        const sourceCode = context.getSourceCode();
56        const options = context.options[0] || {};
57        const ignoreEOLComments = options.ignoreEOLComments;
58        const exceptions = Object.assign({ Property: true }, options.exceptions);
59        const hasExceptions = Object.keys(exceptions).filter(key => exceptions[key]).length > 0;
60
61        /**
62         * Formats value of given comment token for error message by truncating its length.
63         * @param {Token} token comment token
64         * @returns {string} formatted value
65         * @private
66         */
67        function formatReportedCommentValue(token) {
68            const valueLines = token.value.split("\n");
69            const value = valueLines[0];
70            const formattedValue = `${value.slice(0, 12)}...`;
71
72            return valueLines.length === 1 && value.length <= 12 ? value : formattedValue;
73        }
74
75        //--------------------------------------------------------------------------
76        // Public
77        //--------------------------------------------------------------------------
78
79        return {
80            Program() {
81                sourceCode.tokensAndComments.forEach((leftToken, leftIndex, tokensAndComments) => {
82                    if (leftIndex === tokensAndComments.length - 1) {
83                        return;
84                    }
85                    const rightToken = tokensAndComments[leftIndex + 1];
86
87                    // Ignore tokens that don't have 2 spaces between them or are on different lines
88                    if (
89                        !sourceCode.text.slice(leftToken.range[1], rightToken.range[0]).includes("  ") ||
90                        leftToken.loc.end.line < rightToken.loc.start.line
91                    ) {
92                        return;
93                    }
94
95                    // Ignore comments that are the last token on their line if `ignoreEOLComments` is active.
96                    if (
97                        ignoreEOLComments &&
98                        astUtils.isCommentToken(rightToken) &&
99                        (
100                            leftIndex === tokensAndComments.length - 2 ||
101                            rightToken.loc.end.line < tokensAndComments[leftIndex + 2].loc.start.line
102                        )
103                    ) {
104                        return;
105                    }
106
107                    // Ignore tokens that are in a node in the "exceptions" object
108                    if (hasExceptions) {
109                        const parentNode = sourceCode.getNodeByRangeIndex(rightToken.range[0] - 1);
110
111                        if (parentNode && exceptions[parentNode.type]) {
112                            return;
113                        }
114                    }
115
116                    let displayValue;
117
118                    if (rightToken.type === "Block") {
119                        displayValue = `/*${formatReportedCommentValue(rightToken)}*/`;
120                    } else if (rightToken.type === "Line") {
121                        displayValue = `//${formatReportedCommentValue(rightToken)}`;
122                    } else {
123                        displayValue = rightToken.value;
124                    }
125
126                    context.report({
127                        node: rightToken,
128                        loc: { start: leftToken.loc.end, end: rightToken.loc.start },
129                        messageId: "multipleSpaces",
130                        data: { displayValue },
131                        fix: fixer => fixer.replaceTextRange([leftToken.range[1], rightToken.range[0]], " ")
132                    });
133                });
134            }
135        };
136
137    }
138};
139