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 | url: "https://eslint.org/docs/rules/comma-dangle"
|
84 | },
|
85 | fixable: "code",
|
86 | schema: {
|
87 | definitions: {
|
88 | value: {
|
89 | enum: [
|
90 | "always-multiline",
|
91 | "always",
|
92 | "never",
|
93 | "only-multiline"
|
94 | ]
|
95 | },
|
96 | valueWithIgnore: {
|
97 | enum: [
|
98 | "always-multiline",
|
99 | "always",
|
100 | "ignore",
|
101 | "never",
|
102 | "only-multiline"
|
103 | ]
|
104 | }
|
105 | },
|
106 | type: "array",
|
107 | items: [
|
108 | {
|
109 | oneOf: [
|
110 | {
|
111 | $ref: "#/definitions/value"
|
112 | },
|
113 | {
|
114 | type: "object",
|
115 | properties: {
|
116 | arrays: { $ref: "#/definitions/valueWithIgnore" },
|
117 | objects: { $ref: "#/definitions/valueWithIgnore" },
|
118 | imports: { $ref: "#/definitions/valueWithIgnore" },
|
119 | exports: { $ref: "#/definitions/valueWithIgnore" },
|
120 | functions: { $ref: "#/definitions/valueWithIgnore" }
|
121 | },
|
122 | additionalProperties: false
|
123 | }
|
124 | ]
|
125 | }
|
126 | ]
|
127 | },
|
128 |
|
129 | messages: {
|
130 | unexpected: "Unexpected trailing comma.",
|
131 | missing: "Missing trailing comma."
|
132 | }
|
133 | },
|
134 |
|
135 | create(context) {
|
136 | const options = normalizeOptions(context.options[0]);
|
137 | const sourceCode = context.getSourceCode();
|
138 |
|
139 | /**
|
140 | * Gets the last item of the given node.
|
141 | * @param {ASTNode} node - The node to get.
|
142 | * @returns {ASTNode|null} The last node or null.
|
143 | */
|
144 | function getLastItem(node) {
|
145 | switch (node.type) {
|
146 | case "ObjectExpression":
|
147 | case "ObjectPattern":
|
148 | return lodash.last(node.properties);
|
149 | case "ArrayExpression":
|
150 | case "ArrayPattern":
|
151 | return lodash.last(node.elements);
|
152 | case "ImportDeclaration":
|
153 | case "ExportNamedDeclaration":
|
154 | return lodash.last(node.specifiers);
|
155 | case "FunctionDeclaration":
|
156 | case "FunctionExpression":
|
157 | case "ArrowFunctionExpression":
|
158 | return lodash.last(node.params);
|
159 | case "CallExpression":
|
160 | case "NewExpression":
|
161 | return lodash.last(node.arguments);
|
162 | default:
|
163 | return null;
|
164 | }
|
165 | }
|
166 |
|
167 | /**
|
168 | * Gets the trailing comma token of the given node.
|
169 | * If the trailing comma does not exist, this returns the token which is
|
170 | * the insertion point of the trailing comma token.
|
171 | *
|
172 | * @param {ASTNode} node - The node to get.
|
173 | * @param {ASTNode} lastItem - The last item of the node.
|
174 | * @returns {Token} The trailing comma token or the insertion point.
|
175 | */
|
176 | function getTrailingToken(node, lastItem) {
|
177 | switch (node.type) {
|
178 | case "ObjectExpression":
|
179 | case "ArrayExpression":
|
180 | case "CallExpression":
|
181 | case "NewExpression":
|
182 | return sourceCode.getLastToken(node, 1);
|
183 | default: {
|
184 | const nextToken = sourceCode.getTokenAfter(lastItem);
|
185 |
|
186 | if (astUtils.isCommaToken(nextToken)) {
|
187 | return nextToken;
|
188 | }
|
189 | return sourceCode.getLastToken(lastItem);
|
190 | }
|
191 | }
|
192 | }
|
193 |
|
194 | /**
|
195 | * Checks whether or not a given node is multiline.
|
196 | * This rule handles a given node as multiline when the closing parenthesis
|
197 | * and the last element are not on the same line.
|
198 | *
|
199 | * @param {ASTNode} node - A node to check.
|
200 | * @returns {boolean} `true` if the node is multiline.
|
201 | */
|
202 | function isMultiline(node) {
|
203 | const lastItem = getLastItem(node);
|
204 |
|
205 | if (!lastItem) {
|
206 | return false;
|
207 | }
|
208 |
|
209 | const penultimateToken = getTrailingToken(node, lastItem);
|
210 | const lastToken = sourceCode.getTokenAfter(penultimateToken);
|
211 |
|
212 | return lastToken.loc.end.line !== penultimateToken.loc.end.line;
|
213 | }
|
214 |
|
215 | /**
|
216 | * Reports a trailing comma if it exists.
|
217 | *
|
218 | * @param {ASTNode} node - A node to check. Its type is one of
|
219 | * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
|
220 | * ImportDeclaration, and ExportNamedDeclaration.
|
221 | * @returns {void}
|
222 | */
|
223 | function forbidTrailingComma(node) {
|
224 | const lastItem = getLastItem(node);
|
225 |
|
226 | if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
|
227 | return;
|
228 | }
|
229 |
|
230 | const trailingToken = getTrailingToken(node, lastItem);
|
231 |
|
232 | if (astUtils.isCommaToken(trailingToken)) {
|
233 | context.report({
|
234 | node: lastItem,
|
235 | loc: trailingToken.loc.start,
|
236 | messageId: "unexpected",
|
237 | fix(fixer) {
|
238 | return fixer.remove(trailingToken);
|
239 | }
|
240 | });
|
241 | }
|
242 | }
|
243 |
|
244 | /**
|
245 | * Reports the last element of a given node if it does not have a trailing
|
246 | * comma.
|
247 | *
|
248 | * If a given node is `ArrayPattern` which has `RestElement`, the trailing
|
249 | * comma is disallowed, so report if it exists.
|
250 | *
|
251 | * @param {ASTNode} node - A node to check. Its type is one of
|
252 | * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
|
253 | * ImportDeclaration, and ExportNamedDeclaration.
|
254 | * @returns {void}
|
255 | */
|
256 | function forceTrailingComma(node) {
|
257 | const lastItem = getLastItem(node);
|
258 |
|
259 | if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
|
260 | return;
|
261 | }
|
262 | if (!isTrailingCommaAllowed(lastItem)) {
|
263 | forbidTrailingComma(node);
|
264 | return;
|
265 | }
|
266 |
|
267 | const trailingToken = getTrailingToken(node, lastItem);
|
268 |
|
269 | if (trailingToken.value !== ",") {
|
270 | context.report({
|
271 | node: lastItem,
|
272 | loc: trailingToken.loc.end,
|
273 | messageId: "missing",
|
274 | fix(fixer) {
|
275 | return fixer.insertTextAfter(trailingToken, ",");
|
276 | }
|
277 | });
|
278 | }
|
279 | }
|
280 |
|
281 | /**
|
282 | * If a given node is multiline, reports the last element of a given node
|
283 | * when it does not have a trailing comma.
|
284 | * Otherwise, reports a trailing comma if it exists.
|
285 | *
|
286 | * @param {ASTNode} node - A node to check. Its type is one of
|
287 | * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
|
288 | * ImportDeclaration, and ExportNamedDeclaration.
|
289 | * @returns {void}
|
290 | */
|
291 | function forceTrailingCommaIfMultiline(node) {
|
292 | if (isMultiline(node)) {
|
293 | forceTrailingComma(node);
|
294 | } else {
|
295 | forbidTrailingComma(node);
|
296 | }
|
297 | }
|
298 |
|
299 | /**
|
300 | * Only if a given node is not multiline, reports the last element of a given node
|
301 | * when it does not have a trailing comma.
|
302 | * Otherwise, reports a trailing comma if it exists.
|
303 | *
|
304 | * @param {ASTNode} node - A node to check. Its type is one of
|
305 | * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
|
306 | * ImportDeclaration, and ExportNamedDeclaration.
|
307 | * @returns {void}
|
308 | */
|
309 | function allowTrailingCommaIfMultiline(node) {
|
310 | if (!isMultiline(node)) {
|
311 | forbidTrailingComma(node);
|
312 | }
|
313 | }
|
314 |
|
315 | const predicate = {
|
316 | always: forceTrailingComma,
|
317 | "always-multiline": forceTrailingCommaIfMultiline,
|
318 | "only-multiline": allowTrailingCommaIfMultiline,
|
319 | never: forbidTrailingComma,
|
320 | ignore: lodash.noop
|
321 | };
|
322 |
|
323 | return {
|
324 | ObjectExpression: predicate[options.objects],
|
325 | ObjectPattern: predicate[options.objects],
|
326 |
|
327 | ArrayExpression: predicate[options.arrays],
|
328 | ArrayPattern: predicate[options.arrays],
|
329 |
|
330 | ImportDeclaration: predicate[options.imports],
|
331 |
|
332 | ExportNamedDeclaration: predicate[options.exports],
|
333 |
|
334 | FunctionDeclaration: predicate[options.functions],
|
335 | FunctionExpression: predicate[options.functions],
|
336 | ArrowFunctionExpression: predicate[options.functions],
|
337 | CallExpression: predicate[options.functions],
|
338 | NewExpression: predicate[options.functions]
|
339 | };
|
340 | }
|
341 | };
|