UNPKG

9.76 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 (right.type === "Punctuator" && right.value === ")") {
132 return false;
133 }
134 return !isOpenerException(right);
135 } else {
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 (left.type === "Punctuator" && left.value === "(") {
148 return false;
149 }
150
151 if (sourceCode.isSpaceBetweenTokens(left, right)) {
152 return false;
153 }
154
155 if (ALWAYS) {
156 return !isCloserException(left);
157 } else {
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 } else {
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 (left.type === "Punctuator" && left.value === "(") {
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 } else {
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(function(token, i) {
224 const prevToken = tokens[i - 1];
225 const nextToken = tokens[i + 1];
226
227 if (token.type !== "Punctuator") {
228 return;
229 }
230
231 if (token.value !== "(" && token.value !== ")") {
232 return;
233 }
234
235 if (token.value === "(" && shouldOpenerHaveSpace(token, nextToken)) {
236 context.report({
237 node,
238 loc: token.loc.start,
239 message: MISSING_SPACE_MESSAGE,
240 fix(fixer) {
241 return fixer.insertTextAfter(token, " ");
242 }
243 });
244 } else if (token.value === "(" && shouldOpenerRejectSpace(token, nextToken)) {
245 context.report({
246 node,
247 loc: token.loc.start,
248 message: REJECTED_SPACE_MESSAGE,
249 fix(fixer) {
250 return fixer.removeRange([token.range[1], nextToken.range[0]]);
251 }
252 });
253 } else if (token.value === ")" && shouldCloserHaveSpace(prevToken, token)) {
254
255 // context.report(node, token.loc.start, MISSING_SPACE_MESSAGE);
256 context.report({
257 node,
258 loc: token.loc.start,
259 message: MISSING_SPACE_MESSAGE,
260 fix(fixer) {
261 return fixer.insertTextBefore(token, " ");
262 }
263 });
264 } else if (token.value === ")" && shouldCloserRejectSpace(prevToken, token)) {
265 context.report({
266 node,
267 loc: token.loc.start,
268 message: REJECTED_SPACE_MESSAGE,
269 fix(fixer) {
270 return fixer.removeRange([prevToken.range[1], token.range[0]]);
271 }
272 });
273 }
274 });
275 }
276 };
277
278 }
279};