UNPKG

9.54 kBJavaScriptView Raw
1/**
2 * @fileoverview Disallows or enforces spaces inside of parentheses.
3 * @author Jonathan Rajavuori
4 */
5"use strict";
6
7const astUtils = require("../ast-utils");
8
9//------------------------------------------------------------------------------
10// Rule Definition
11//------------------------------------------------------------------------------
12
13module.exports = {
14 meta: {
15 docs: {
16 description: "enforce consistent spacing inside parentheses",
17 category: "Stylistic Issues",
18 recommended: false
19 },
20
21 fixable: "whitespace",
22
23 schema: [
24 {
25 enum: ["always", "never"]
26 },
27 {
28 type: "object",
29 properties: {
30 exceptions: {
31 type: "array",
32 items: {
33 enum: ["{}", "[]", "()", "empty"]
34 },
35 uniqueItems: true
36 }
37 },
38 additionalProperties: false
39 }
40 ]
41 },
42
43 create(context) {
44
45 const MISSING_SPACE_MESSAGE = "There must be a space inside this paren.",
46 REJECTED_SPACE_MESSAGE = "There should be no spaces inside this paren.",
47 ALWAYS = context.options[0] === "always",
48
49 exceptionsArrayOptions = (context.options.length === 2) ? context.options[1].exceptions : [],
50 options = {};
51 let exceptions;
52
53 if (exceptionsArrayOptions.length) {
54 options.braceException = exceptionsArrayOptions.indexOf("{}") !== -1;
55 options.bracketException = exceptionsArrayOptions.indexOf("[]") !== -1;
56 options.parenException = exceptionsArrayOptions.indexOf("()") !== -1;
57 options.empty = exceptionsArrayOptions.indexOf("empty") !== -1;
58 }
59
60 /**
61 * Produces an object with the opener and closer exception values
62 * @param {Object} opts The exception options
63 * @returns {Object} `openers` and `closers` exception values
64 * @private
65 */
66 function getExceptions() {
67 const openers = [],
68 closers = [];
69
70 if (options.braceException) {
71 openers.push("{");
72 closers.push("}");
73 }
74
75 if (options.bracketException) {
76 openers.push("[");
77 closers.push("]");
78 }
79
80 if (options.parenException) {
81 openers.push("(");
82 closers.push(")");
83 }
84
85 if (options.empty) {
86 openers.push(")");
87 closers.push("(");
88 }
89
90 return {
91 openers,
92 closers
93 };
94 }
95
96 //--------------------------------------------------------------------------
97 // Helpers
98 //--------------------------------------------------------------------------
99 const sourceCode = context.getSourceCode();
100
101 /**
102 * Determines if a token is one of the exceptions for the opener paren
103 * @param {Object} token The token to check
104 * @returns {boolean} True if the token is one of the exceptions for the opener paren
105 */
106 function isOpenerException(token) {
107 return token.type === "Punctuator" && exceptions.openers.indexOf(token.value) >= 0;
108 }
109
110 /**
111 * Determines if a token is one of the exceptions for the closer paren
112 * @param {Object} token The token to check
113 * @returns {boolean} True if the token is one of the exceptions for the closer paren
114 */
115 function isCloserException(token) {
116 return token.type === "Punctuator" && exceptions.closers.indexOf(token.value) >= 0;
117 }
118
119 /**
120 * Determines if an opener paren should have a missing space after it
121 * @param {Object} left The paren token
122 * @param {Object} right The token after it
123 * @returns {boolean} True if the paren should have a space
124 */
125 function shouldOpenerHaveSpace(left, right) {
126 if (sourceCode.isSpaceBetweenTokens(left, right)) {
127 return false;
128 }
129
130 if (ALWAYS) {
131 if (astUtils.isClosingParenToken(right)) {
132 return false;
133 }
134 return !isOpenerException(right);
135 }
136 return isOpenerException(right);
137
138 }
139
140 /**
141 * Determines if an closer paren should have a missing space after it
142 * @param {Object} left The token before the paren
143 * @param {Object} right The paren token
144 * @returns {boolean} True if the paren should have a space
145 */
146 function shouldCloserHaveSpace(left, right) {
147 if (astUtils.isOpeningParenToken(left)) {
148 return false;
149 }
150
151 if (sourceCode.isSpaceBetweenTokens(left, right)) {
152 return false;
153 }
154
155 if (ALWAYS) {
156 return !isCloserException(left);
157 }
158 return isCloserException(left);
159
160 }
161
162 /**
163 * Determines if an opener paren should not have an existing space after it
164 * @param {Object} left The paren token
165 * @param {Object} right The token after it
166 * @returns {boolean} True if the paren should reject the space
167 */
168 function shouldOpenerRejectSpace(left, right) {
169 if (right.type === "Line") {
170 return false;
171 }
172
173 if (!astUtils.isTokenOnSameLine(left, right)) {
174 return false;
175 }
176
177 if (!sourceCode.isSpaceBetweenTokens(left, right)) {
178 return false;
179 }
180
181 if (ALWAYS) {
182 return isOpenerException(right);
183 }
184 return !isOpenerException(right);
185
186 }
187
188 /**
189 * Determines if an closer paren should not have an existing space after it
190 * @param {Object} left The token before the paren
191 * @param {Object} right The paren token
192 * @returns {boolean} True if the paren should reject the space
193 */
194 function shouldCloserRejectSpace(left, right) {
195 if (astUtils.isOpeningParenToken(left)) {
196 return false;
197 }
198
199 if (!astUtils.isTokenOnSameLine(left, right)) {
200 return false;
201 }
202
203 if (!sourceCode.isSpaceBetweenTokens(left, right)) {
204 return false;
205 }
206
207 if (ALWAYS) {
208 return isCloserException(left);
209 }
210 return !isCloserException(left);
211
212 }
213
214 //--------------------------------------------------------------------------
215 // Public
216 //--------------------------------------------------------------------------
217
218 return {
219 Program: function checkParenSpaces(node) {
220 exceptions = getExceptions();
221 const tokens = sourceCode.tokensAndComments;
222
223 tokens.forEach((token, i) => {
224 const prevToken = tokens[i - 1];
225 const nextToken = tokens[i + 1];
226
227 if (!astUtils.isOpeningParenToken(token) && !astUtils.isClosingParenToken(token)) {
228 return;
229 }
230
231 if (token.value === "(" && shouldOpenerHaveSpace(token, nextToken)) {
232 context.report({
233 node,
234 loc: token.loc.start,
235 message: MISSING_SPACE_MESSAGE,
236 fix(fixer) {
237 return fixer.insertTextAfter(token, " ");
238 }
239 });
240 } else if (token.value === "(" && shouldOpenerRejectSpace(token, nextToken)) {
241 context.report({
242 node,
243 loc: token.loc.start,
244 message: REJECTED_SPACE_MESSAGE,
245 fix(fixer) {
246 return fixer.removeRange([token.range[1], nextToken.range[0]]);
247 }
248 });
249 } else if (token.value === ")" && shouldCloserHaveSpace(prevToken, token)) {
250
251 // context.report(node, token.loc.start, MISSING_SPACE_MESSAGE);
252 context.report({
253 node,
254 loc: token.loc.start,
255 message: MISSING_SPACE_MESSAGE,
256 fix(fixer) {
257 return fixer.insertTextBefore(token, " ");
258 }
259 });
260 } else if (token.value === ")" && shouldCloserRejectSpace(prevToken, token)) {
261 context.report({
262 node,
263 loc: token.loc.start,
264 message: REJECTED_SPACE_MESSAGE,
265 fix(fixer) {
266 return fixer.removeRange([prevToken.range[1], token.range[0]]);
267 }
268 });
269 }
270 });
271 }
272 };
273
274 }
275};