1 | /**
|
2 | * @fileoverview Rule to forbid or enforce dangling commas.
|
3 | * @author Ian Christian Myers
|
4 | */
|
5 |
|
6 | ;
|
7 |
|
8 | //------------------------------------------------------------------------------
|
9 | // Requirements
|
10 | //------------------------------------------------------------------------------
|
11 |
|
12 | const lodash = require("lodash");
|
13 | const astUtils = require("../ast-utils");
|
14 |
|
15 | //------------------------------------------------------------------------------
|
16 | // Helpers
|
17 | //------------------------------------------------------------------------------
|
18 |
|
19 | const DEFAULT_OPTIONS = Object.freeze({
|
20 | arrays: "never",
|
21 | objects: "never",
|
22 | imports: "never",
|
23 | exports: "never",
|
24 | functions: "ignore"
|
25 | });
|
26 |
|
27 | /**
|
28 | * Checks whether or not a trailing comma is allowed in a given node.
|
29 | * If the `lastItem` is `RestElement` or `RestProperty`, it disallows trailing commas.
|
30 | *
|
31 | * @param {ASTNode} lastItem - The node of the last element in the given node.
|
32 | * @returns {boolean} `true` if a trailing comma is allowed.
|
33 | */
|
34 | function isTrailingCommaAllowed(lastItem) {
|
35 | return !(
|
36 | lastItem.type === "RestElement" ||
|
37 | lastItem.type === "RestProperty" ||
|
38 | lastItem.type === "ExperimentalRestProperty"
|
39 | );
|
40 | }
|
41 |
|
42 | /**
|
43 | * Normalize option value.
|
44 | *
|
45 | * @param {string|Object|undefined} optionValue - The 1st option value to normalize.
|
46 | * @returns {Object} The normalized option value.
|
47 | */
|
48 | function normalizeOptions(optionValue) {
|
49 | if (typeof optionValue === "string") {
|
50 | return {
|
51 | arrays: optionValue,
|
52 | objects: optionValue,
|
53 | imports: optionValue,
|
54 | exports: optionValue,
|
55 |
|
56 | // For backward compatibility, always ignore functions.
|
57 | functions: "ignore"
|
58 | };
|
59 | }
|
60 | if (typeof optionValue === "object" && optionValue !== null) {
|
61 | return {
|
62 | arrays: optionValue.arrays || DEFAULT_OPTIONS.arrays,
|
63 | objects: optionValue.objects || DEFAULT_OPTIONS.objects,
|
64 | imports: optionValue.imports || DEFAULT_OPTIONS.imports,
|
65 | exports: optionValue.exports || DEFAULT_OPTIONS.exports,
|
66 | functions: optionValue.functions || DEFAULT_OPTIONS.functions
|
67 | };
|
68 | }
|
69 |
|
70 | return DEFAULT_OPTIONS;
|
71 | }
|
72 |
|
73 | //------------------------------------------------------------------------------
|
74 | // Rule Definition
|
75 | //------------------------------------------------------------------------------
|
76 |
|
77 | module.exports = {
|
78 | meta: {
|
79 | docs: {
|
80 | description: "require or disallow trailing commas",
|
81 | category: "Stylistic Issues",
|
82 | recommended: false
|
83 | },
|
84 | fixable: "code",
|
85 | schema: {
|
86 | definitions: {
|
87 | value: {
|
88 | enum: [
|
89 | "always-multiline",
|
90 | "always",
|
91 | "never",
|
92 | "only-multiline"
|
93 | ]
|
94 | },
|
95 | valueWithIgnore: {
|
96 | enum: [
|
97 | "always-multiline",
|
98 | "always",
|
99 | "ignore",
|
100 | "never",
|
101 | "only-multiline"
|
102 | ]
|
103 | }
|
104 | },
|
105 | type: "array",
|
106 | items: [
|
107 | {
|
108 | oneOf: [
|
109 | {
|
110 | $ref: "#/definitions/value"
|
111 | },
|
112 | {
|
113 | type: "object",
|
114 | properties: {
|
115 | arrays: { $ref: "#/definitions/valueWithIgnore" },
|
116 | objects: { $ref: "#/definitions/valueWithIgnore" },
|
117 | imports: { $ref: "#/definitions/valueWithIgnore" },
|
118 | exports: { $ref: "#/definitions/valueWithIgnore" },
|
119 | functions: { $ref: "#/definitions/valueWithIgnore" }
|
120 | },
|
121 | additionalProperties: false
|
122 | }
|
123 | ]
|
124 | }
|
125 | ]
|
126 | }
|
127 | },
|
128 |
|
129 | create(context) {
|
130 | const options = normalizeOptions(context.options[0]);
|
131 | const sourceCode = context.getSourceCode();
|
132 | const UNEXPECTED_MESSAGE = "Unexpected trailing comma.";
|
133 | const MISSING_MESSAGE = "Missing trailing comma.";
|
134 |
|
135 | /**
|
136 | * Gets the last item of the given node.
|
137 | * @param {ASTNode} node - The node to get.
|
138 | * @returns {ASTNode|null} The last node or null.
|
139 | */
|
140 | function getLastItem(node) {
|
141 | switch (node.type) {
|
142 | case "ObjectExpression":
|
143 | case "ObjectPattern":
|
144 | return lodash.last(node.properties);
|
145 | case "ArrayExpression":
|
146 | case "ArrayPattern":
|
147 | return lodash.last(node.elements);
|
148 | case "ImportDeclaration":
|
149 | case "ExportNamedDeclaration":
|
150 | return lodash.last(node.specifiers);
|
151 | case "FunctionDeclaration":
|
152 | case "FunctionExpression":
|
153 | case "ArrowFunctionExpression":
|
154 | return lodash.last(node.params);
|
155 | case "CallExpression":
|
156 | case "NewExpression":
|
157 | return lodash.last(node.arguments);
|
158 | default:
|
159 | return null;
|
160 | }
|
161 | }
|
162 |
|
163 | /**
|
164 | * Gets the trailing comma token of the given node.
|
165 | * If the trailing comma does not exist, this returns the token which is
|
166 | * the insertion point of the trailing comma token.
|
167 | *
|
168 | * @param {ASTNode} node - The node to get.
|
169 | * @param {ASTNode} lastItem - The last item of the node.
|
170 | * @returns {Token} The trailing comma token or the insertion point.
|
171 | */
|
172 | function getTrailingToken(node, lastItem) {
|
173 | switch (node.type) {
|
174 | case "ObjectExpression":
|
175 | case "ArrayExpression":
|
176 | case "CallExpression":
|
177 | case "NewExpression":
|
178 | return sourceCode.getLastToken(node, 1);
|
179 | default: {
|
180 | const nextToken = sourceCode.getTokenAfter(lastItem);
|
181 |
|
182 | if (astUtils.isCommaToken(nextToken)) {
|
183 | return nextToken;
|
184 | }
|
185 | return sourceCode.getLastToken(lastItem);
|
186 | }
|
187 | }
|
188 | }
|
189 |
|
190 | /**
|
191 | * Checks whether or not a given node is multiline.
|
192 | * This rule handles a given node as multiline when the closing parenthesis
|
193 | * and the last element are not on the same line.
|
194 | *
|
195 | * @param {ASTNode} node - A node to check.
|
196 | * @returns {boolean} `true` if the node is multiline.
|
197 | */
|
198 | function isMultiline(node) {
|
199 | const lastItem = getLastItem(node);
|
200 |
|
201 | if (!lastItem) {
|
202 | return false;
|
203 | }
|
204 |
|
205 | const penultimateToken = getTrailingToken(node, lastItem);
|
206 | const lastToken = sourceCode.getTokenAfter(penultimateToken);
|
207 |
|
208 | return lastToken.loc.end.line !== penultimateToken.loc.end.line;
|
209 | }
|
210 |
|
211 | /**
|
212 | * Reports a trailing comma if it exists.
|
213 | *
|
214 | * @param {ASTNode} node - A node to check. Its type is one of
|
215 | * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
|
216 | * ImportDeclaration, and ExportNamedDeclaration.
|
217 | * @returns {void}
|
218 | */
|
219 | function forbidTrailingComma(node) {
|
220 | const lastItem = getLastItem(node);
|
221 |
|
222 | if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
|
223 | return;
|
224 | }
|
225 |
|
226 | const trailingToken = getTrailingToken(node, lastItem);
|
227 |
|
228 | if (astUtils.isCommaToken(trailingToken)) {
|
229 | context.report({
|
230 | node: lastItem,
|
231 | loc: trailingToken.loc.start,
|
232 | message: UNEXPECTED_MESSAGE,
|
233 | fix(fixer) {
|
234 | return fixer.remove(trailingToken);
|
235 | }
|
236 | });
|
237 | }
|
238 | }
|
239 |
|
240 | /**
|
241 | * Reports the last element of a given node if it does not have a trailing
|
242 | * comma.
|
243 | *
|
244 | * If a given node is `ArrayPattern` which has `RestElement`, the trailing
|
245 | * comma is disallowed, so report if it exists.
|
246 | *
|
247 | * @param {ASTNode} node - A node to check. Its type is one of
|
248 | * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
|
249 | * ImportDeclaration, and ExportNamedDeclaration.
|
250 | * @returns {void}
|
251 | */
|
252 | function forceTrailingComma(node) {
|
253 | const lastItem = getLastItem(node);
|
254 |
|
255 | if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
|
256 | return;
|
257 | }
|
258 | if (!isTrailingCommaAllowed(lastItem)) {
|
259 | forbidTrailingComma(node);
|
260 | return;
|
261 | }
|
262 |
|
263 | const trailingToken = getTrailingToken(node, lastItem);
|
264 |
|
265 | if (trailingToken.value !== ",") {
|
266 | context.report({
|
267 | node: lastItem,
|
268 | loc: trailingToken.loc.end,
|
269 | message: MISSING_MESSAGE,
|
270 | fix(fixer) {
|
271 | return fixer.insertTextAfter(trailingToken, ",");
|
272 | }
|
273 | });
|
274 | }
|
275 | }
|
276 |
|
277 | /**
|
278 | * If a given node is multiline, reports the last element of a given node
|
279 | * when it does not have a trailing comma.
|
280 | * Otherwise, reports a trailing comma if it exists.
|
281 | *
|
282 | * @param {ASTNode} node - A node to check. Its type is one of
|
283 | * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
|
284 | * ImportDeclaration, and ExportNamedDeclaration.
|
285 | * @returns {void}
|
286 | */
|
287 | function forceTrailingCommaIfMultiline(node) {
|
288 | if (isMultiline(node)) {
|
289 | forceTrailingComma(node);
|
290 | } else {
|
291 | forbidTrailingComma(node);
|
292 | }
|
293 | }
|
294 |
|
295 | /**
|
296 | * Only if a given node is not multiline, reports the last element of a given node
|
297 | * when it does not have a trailing comma.
|
298 | * Otherwise, reports a trailing comma if it exists.
|
299 | *
|
300 | * @param {ASTNode} node - A node to check. Its type is one of
|
301 | * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
|
302 | * ImportDeclaration, and ExportNamedDeclaration.
|
303 | * @returns {void}
|
304 | */
|
305 | function allowTrailingCommaIfMultiline(node) {
|
306 | if (!isMultiline(node)) {
|
307 | forbidTrailingComma(node);
|
308 | }
|
309 | }
|
310 |
|
311 | const predicate = {
|
312 | always: forceTrailingComma,
|
313 | "always-multiline": forceTrailingCommaIfMultiline,
|
314 | "only-multiline": allowTrailingCommaIfMultiline,
|
315 | never: forbidTrailingComma,
|
316 | ignore: lodash.noop
|
317 | };
|
318 |
|
319 | return {
|
320 | ObjectExpression: predicate[options.objects],
|
321 | ObjectPattern: predicate[options.objects],
|
322 |
|
323 | ArrayExpression: predicate[options.arrays],
|
324 | ArrayPattern: predicate[options.arrays],
|
325 |
|
326 | ImportDeclaration: predicate[options.imports],
|
327 |
|
328 | ExportNamedDeclaration: predicate[options.exports],
|
329 |
|
330 | FunctionDeclaration: predicate[options.functions],
|
331 | FunctionExpression: predicate[options.functions],
|
332 | ArrowFunctionExpression: predicate[options.functions],
|
333 | CallExpression: predicate[options.functions],
|
334 | NewExpression: predicate[options.functions]
|
335 | };
|
336 | }
|
337 | };
|