[d565449] | 1 | /**
|
---|
| 2 | * @fileoverview Rule to forbid or enforce dangling commas.
|
---|
| 3 | * @author Ian Christian Myers
|
---|
| 4 | * @deprecated in ESLint v8.53.0
|
---|
| 5 | */
|
---|
| 6 |
|
---|
| 7 | "use strict";
|
---|
| 8 |
|
---|
| 9 | //------------------------------------------------------------------------------
|
---|
| 10 | // Requirements
|
---|
| 11 | //------------------------------------------------------------------------------
|
---|
| 12 |
|
---|
| 13 | const astUtils = require("./utils/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: "never"
|
---|
| 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 | * @param {ASTNode} lastItem The node of the last element in the given node.
|
---|
| 31 | * @returns {boolean} `true` if a trailing comma is allowed.
|
---|
| 32 | */
|
---|
| 33 | function isTrailingCommaAllowed(lastItem) {
|
---|
| 34 | return !(
|
---|
| 35 | lastItem.type === "RestElement" ||
|
---|
| 36 | lastItem.type === "RestProperty" ||
|
---|
| 37 | lastItem.type === "ExperimentalRestProperty"
|
---|
| 38 | );
|
---|
| 39 | }
|
---|
| 40 |
|
---|
| 41 | /**
|
---|
| 42 | * Normalize option value.
|
---|
| 43 | * @param {string|Object|undefined} optionValue The 1st option value to normalize.
|
---|
| 44 | * @param {number} ecmaVersion The normalized ECMAScript version.
|
---|
| 45 | * @returns {Object} The normalized option value.
|
---|
| 46 | */
|
---|
| 47 | function normalizeOptions(optionValue, ecmaVersion) {
|
---|
| 48 | if (typeof optionValue === "string") {
|
---|
| 49 | return {
|
---|
| 50 | arrays: optionValue,
|
---|
| 51 | objects: optionValue,
|
---|
| 52 | imports: optionValue,
|
---|
| 53 | exports: optionValue,
|
---|
| 54 | functions: ecmaVersion < 2017 ? "ignore" : optionValue
|
---|
| 55 | };
|
---|
| 56 | }
|
---|
| 57 | if (typeof optionValue === "object" && optionValue !== null) {
|
---|
| 58 | return {
|
---|
| 59 | arrays: optionValue.arrays || DEFAULT_OPTIONS.arrays,
|
---|
| 60 | objects: optionValue.objects || DEFAULT_OPTIONS.objects,
|
---|
| 61 | imports: optionValue.imports || DEFAULT_OPTIONS.imports,
|
---|
| 62 | exports: optionValue.exports || DEFAULT_OPTIONS.exports,
|
---|
| 63 | functions: optionValue.functions || DEFAULT_OPTIONS.functions
|
---|
| 64 | };
|
---|
| 65 | }
|
---|
| 66 |
|
---|
| 67 | return DEFAULT_OPTIONS;
|
---|
| 68 | }
|
---|
| 69 |
|
---|
| 70 | //------------------------------------------------------------------------------
|
---|
| 71 | // Rule Definition
|
---|
| 72 | //------------------------------------------------------------------------------
|
---|
| 73 |
|
---|
| 74 | /** @type {import('../shared/types').Rule} */
|
---|
| 75 | module.exports = {
|
---|
| 76 | meta: {
|
---|
| 77 | deprecated: true,
|
---|
| 78 | replacedBy: [],
|
---|
| 79 | type: "layout",
|
---|
| 80 |
|
---|
| 81 | docs: {
|
---|
| 82 | description: "Require or disallow trailing commas",
|
---|
| 83 | recommended: false,
|
---|
| 84 | url: "https://eslint.org/docs/latest/rules/comma-dangle"
|
---|
| 85 | },
|
---|
| 86 |
|
---|
| 87 | fixable: "code",
|
---|
| 88 |
|
---|
| 89 | schema: {
|
---|
| 90 | definitions: {
|
---|
| 91 | value: {
|
---|
| 92 | enum: [
|
---|
| 93 | "always-multiline",
|
---|
| 94 | "always",
|
---|
| 95 | "never",
|
---|
| 96 | "only-multiline"
|
---|
| 97 | ]
|
---|
| 98 | },
|
---|
| 99 | valueWithIgnore: {
|
---|
| 100 | enum: [
|
---|
| 101 | "always-multiline",
|
---|
| 102 | "always",
|
---|
| 103 | "ignore",
|
---|
| 104 | "never",
|
---|
| 105 | "only-multiline"
|
---|
| 106 | ]
|
---|
| 107 | }
|
---|
| 108 | },
|
---|
| 109 | type: "array",
|
---|
| 110 | items: [
|
---|
| 111 | {
|
---|
| 112 | oneOf: [
|
---|
| 113 | {
|
---|
| 114 | $ref: "#/definitions/value"
|
---|
| 115 | },
|
---|
| 116 | {
|
---|
| 117 | type: "object",
|
---|
| 118 | properties: {
|
---|
| 119 | arrays: { $ref: "#/definitions/valueWithIgnore" },
|
---|
| 120 | objects: { $ref: "#/definitions/valueWithIgnore" },
|
---|
| 121 | imports: { $ref: "#/definitions/valueWithIgnore" },
|
---|
| 122 | exports: { $ref: "#/definitions/valueWithIgnore" },
|
---|
| 123 | functions: { $ref: "#/definitions/valueWithIgnore" }
|
---|
| 124 | },
|
---|
| 125 | additionalProperties: false
|
---|
| 126 | }
|
---|
| 127 | ]
|
---|
| 128 | }
|
---|
| 129 | ],
|
---|
| 130 | additionalItems: false
|
---|
| 131 | },
|
---|
| 132 |
|
---|
| 133 | messages: {
|
---|
| 134 | unexpected: "Unexpected trailing comma.",
|
---|
| 135 | missing: "Missing trailing comma."
|
---|
| 136 | }
|
---|
| 137 | },
|
---|
| 138 |
|
---|
| 139 | create(context) {
|
---|
| 140 | const options = normalizeOptions(context.options[0], context.languageOptions.ecmaVersion);
|
---|
| 141 |
|
---|
| 142 | const sourceCode = context.sourceCode;
|
---|
| 143 |
|
---|
| 144 | /**
|
---|
| 145 | * Gets the last item of the given node.
|
---|
| 146 | * @param {ASTNode} node The node to get.
|
---|
| 147 | * @returns {ASTNode|null} The last node or null.
|
---|
| 148 | */
|
---|
| 149 | function getLastItem(node) {
|
---|
| 150 |
|
---|
| 151 | /**
|
---|
| 152 | * Returns the last element of an array
|
---|
| 153 | * @param {any[]} array The input array
|
---|
| 154 | * @returns {any} The last element
|
---|
| 155 | */
|
---|
| 156 | function last(array) {
|
---|
| 157 | return array[array.length - 1];
|
---|
| 158 | }
|
---|
| 159 |
|
---|
| 160 | switch (node.type) {
|
---|
| 161 | case "ObjectExpression":
|
---|
| 162 | case "ObjectPattern":
|
---|
| 163 | return last(node.properties);
|
---|
| 164 | case "ArrayExpression":
|
---|
| 165 | case "ArrayPattern":
|
---|
| 166 | return last(node.elements);
|
---|
| 167 | case "ImportDeclaration":
|
---|
| 168 | case "ExportNamedDeclaration":
|
---|
| 169 | return last(node.specifiers);
|
---|
| 170 | case "FunctionDeclaration":
|
---|
| 171 | case "FunctionExpression":
|
---|
| 172 | case "ArrowFunctionExpression":
|
---|
| 173 | return last(node.params);
|
---|
| 174 | case "CallExpression":
|
---|
| 175 | case "NewExpression":
|
---|
| 176 | return last(node.arguments);
|
---|
| 177 | default:
|
---|
| 178 | return null;
|
---|
| 179 | }
|
---|
| 180 | }
|
---|
| 181 |
|
---|
| 182 | /**
|
---|
| 183 | * Gets the trailing comma token of the given node.
|
---|
| 184 | * If the trailing comma does not exist, this returns the token which is
|
---|
| 185 | * the insertion point of the trailing comma token.
|
---|
| 186 | * @param {ASTNode} node The node to get.
|
---|
| 187 | * @param {ASTNode} lastItem The last item of the node.
|
---|
| 188 | * @returns {Token} The trailing comma token or the insertion point.
|
---|
| 189 | */
|
---|
| 190 | function getTrailingToken(node, lastItem) {
|
---|
| 191 | switch (node.type) {
|
---|
| 192 | case "ObjectExpression":
|
---|
| 193 | case "ArrayExpression":
|
---|
| 194 | case "CallExpression":
|
---|
| 195 | case "NewExpression":
|
---|
| 196 | return sourceCode.getLastToken(node, 1);
|
---|
| 197 | default: {
|
---|
| 198 | const nextToken = sourceCode.getTokenAfter(lastItem);
|
---|
| 199 |
|
---|
| 200 | if (astUtils.isCommaToken(nextToken)) {
|
---|
| 201 | return nextToken;
|
---|
| 202 | }
|
---|
| 203 | return sourceCode.getLastToken(lastItem);
|
---|
| 204 | }
|
---|
| 205 | }
|
---|
| 206 | }
|
---|
| 207 |
|
---|
| 208 | /**
|
---|
| 209 | * Checks whether or not a given node is multiline.
|
---|
| 210 | * This rule handles a given node as multiline when the closing parenthesis
|
---|
| 211 | * and the last element are not on the same line.
|
---|
| 212 | * @param {ASTNode} node A node to check.
|
---|
| 213 | * @returns {boolean} `true` if the node is multiline.
|
---|
| 214 | */
|
---|
| 215 | function isMultiline(node) {
|
---|
| 216 | const lastItem = getLastItem(node);
|
---|
| 217 |
|
---|
| 218 | if (!lastItem) {
|
---|
| 219 | return false;
|
---|
| 220 | }
|
---|
| 221 |
|
---|
| 222 | const penultimateToken = getTrailingToken(node, lastItem);
|
---|
| 223 | const lastToken = sourceCode.getTokenAfter(penultimateToken);
|
---|
| 224 |
|
---|
| 225 | return lastToken.loc.end.line !== penultimateToken.loc.end.line;
|
---|
| 226 | }
|
---|
| 227 |
|
---|
| 228 | /**
|
---|
| 229 | * Reports a trailing comma if it exists.
|
---|
| 230 | * @param {ASTNode} node A node to check. Its type is one of
|
---|
| 231 | * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
|
---|
| 232 | * ImportDeclaration, and ExportNamedDeclaration.
|
---|
| 233 | * @returns {void}
|
---|
| 234 | */
|
---|
| 235 | function forbidTrailingComma(node) {
|
---|
| 236 | const lastItem = getLastItem(node);
|
---|
| 237 |
|
---|
| 238 | if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
|
---|
| 239 | return;
|
---|
| 240 | }
|
---|
| 241 |
|
---|
| 242 | const trailingToken = getTrailingToken(node, lastItem);
|
---|
| 243 |
|
---|
| 244 | if (astUtils.isCommaToken(trailingToken)) {
|
---|
| 245 | context.report({
|
---|
| 246 | node: lastItem,
|
---|
| 247 | loc: trailingToken.loc,
|
---|
| 248 | messageId: "unexpected",
|
---|
| 249 | *fix(fixer) {
|
---|
| 250 | yield fixer.remove(trailingToken);
|
---|
| 251 |
|
---|
| 252 | /*
|
---|
| 253 | * Extend the range of the fix to include surrounding tokens to ensure
|
---|
| 254 | * that the element after which the comma is removed stays _last_.
|
---|
| 255 | * This intentionally makes conflicts in fix ranges with rules that may be
|
---|
| 256 | * adding or removing elements in the same autofix pass.
|
---|
| 257 | * https://github.com/eslint/eslint/issues/15660
|
---|
| 258 | */
|
---|
| 259 | yield fixer.insertTextBefore(sourceCode.getTokenBefore(trailingToken), "");
|
---|
| 260 | yield fixer.insertTextAfter(sourceCode.getTokenAfter(trailingToken), "");
|
---|
| 261 | }
|
---|
| 262 | });
|
---|
| 263 | }
|
---|
| 264 | }
|
---|
| 265 |
|
---|
| 266 | /**
|
---|
| 267 | * Reports the last element of a given node if it does not have a trailing
|
---|
| 268 | * comma.
|
---|
| 269 | *
|
---|
| 270 | * If a given node is `ArrayPattern` which has `RestElement`, the trailing
|
---|
| 271 | * comma is disallowed, so report if it exists.
|
---|
| 272 | * @param {ASTNode} node A node to check. Its type is one of
|
---|
| 273 | * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
|
---|
| 274 | * ImportDeclaration, and ExportNamedDeclaration.
|
---|
| 275 | * @returns {void}
|
---|
| 276 | */
|
---|
| 277 | function forceTrailingComma(node) {
|
---|
| 278 | const lastItem = getLastItem(node);
|
---|
| 279 |
|
---|
| 280 | if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
|
---|
| 281 | return;
|
---|
| 282 | }
|
---|
| 283 | if (!isTrailingCommaAllowed(lastItem)) {
|
---|
| 284 | forbidTrailingComma(node);
|
---|
| 285 | return;
|
---|
| 286 | }
|
---|
| 287 |
|
---|
| 288 | const trailingToken = getTrailingToken(node, lastItem);
|
---|
| 289 |
|
---|
| 290 | if (trailingToken.value !== ",") {
|
---|
| 291 | context.report({
|
---|
| 292 | node: lastItem,
|
---|
| 293 | loc: {
|
---|
| 294 | start: trailingToken.loc.end,
|
---|
| 295 | end: astUtils.getNextLocation(sourceCode, trailingToken.loc.end)
|
---|
| 296 | },
|
---|
| 297 | messageId: "missing",
|
---|
| 298 | *fix(fixer) {
|
---|
| 299 | yield fixer.insertTextAfter(trailingToken, ",");
|
---|
| 300 |
|
---|
| 301 | /*
|
---|
| 302 | * Extend the range of the fix to include surrounding tokens to ensure
|
---|
| 303 | * that the element after which the comma is inserted stays _last_.
|
---|
| 304 | * This intentionally makes conflicts in fix ranges with rules that may be
|
---|
| 305 | * adding or removing elements in the same autofix pass.
|
---|
| 306 | * https://github.com/eslint/eslint/issues/15660
|
---|
| 307 | */
|
---|
| 308 | yield fixer.insertTextBefore(trailingToken, "");
|
---|
| 309 | yield fixer.insertTextAfter(sourceCode.getTokenAfter(trailingToken), "");
|
---|
| 310 | }
|
---|
| 311 | });
|
---|
| 312 | }
|
---|
| 313 | }
|
---|
| 314 |
|
---|
| 315 | /**
|
---|
| 316 | * If a given node is multiline, reports the last element of a given node
|
---|
| 317 | * when it does not have a trailing comma.
|
---|
| 318 | * Otherwise, reports a trailing comma if it exists.
|
---|
| 319 | * @param {ASTNode} node A node to check. Its type is one of
|
---|
| 320 | * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
|
---|
| 321 | * ImportDeclaration, and ExportNamedDeclaration.
|
---|
| 322 | * @returns {void}
|
---|
| 323 | */
|
---|
| 324 | function forceTrailingCommaIfMultiline(node) {
|
---|
| 325 | if (isMultiline(node)) {
|
---|
| 326 | forceTrailingComma(node);
|
---|
| 327 | } else {
|
---|
| 328 | forbidTrailingComma(node);
|
---|
| 329 | }
|
---|
| 330 | }
|
---|
| 331 |
|
---|
| 332 | /**
|
---|
| 333 | * Only if a given node is not multiline, reports the last element of a given node
|
---|
| 334 | * when it does not have a trailing comma.
|
---|
| 335 | * Otherwise, reports a trailing comma if it exists.
|
---|
| 336 | * @param {ASTNode} node A node to check. Its type is one of
|
---|
| 337 | * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
|
---|
| 338 | * ImportDeclaration, and ExportNamedDeclaration.
|
---|
| 339 | * @returns {void}
|
---|
| 340 | */
|
---|
| 341 | function allowTrailingCommaIfMultiline(node) {
|
---|
| 342 | if (!isMultiline(node)) {
|
---|
| 343 | forbidTrailingComma(node);
|
---|
| 344 | }
|
---|
| 345 | }
|
---|
| 346 |
|
---|
| 347 | const predicate = {
|
---|
| 348 | always: forceTrailingComma,
|
---|
| 349 | "always-multiline": forceTrailingCommaIfMultiline,
|
---|
| 350 | "only-multiline": allowTrailingCommaIfMultiline,
|
---|
| 351 | never: forbidTrailingComma,
|
---|
| 352 | ignore() {}
|
---|
| 353 | };
|
---|
| 354 |
|
---|
| 355 | return {
|
---|
| 356 | ObjectExpression: predicate[options.objects],
|
---|
| 357 | ObjectPattern: predicate[options.objects],
|
---|
| 358 |
|
---|
| 359 | ArrayExpression: predicate[options.arrays],
|
---|
| 360 | ArrayPattern: predicate[options.arrays],
|
---|
| 361 |
|
---|
| 362 | ImportDeclaration: predicate[options.imports],
|
---|
| 363 |
|
---|
| 364 | ExportNamedDeclaration: predicate[options.exports],
|
---|
| 365 |
|
---|
| 366 | FunctionDeclaration: predicate[options.functions],
|
---|
| 367 | FunctionExpression: predicate[options.functions],
|
---|
| 368 | ArrowFunctionExpression: predicate[options.functions],
|
---|
| 369 | CallExpression: predicate[options.functions],
|
---|
| 370 | NewExpression: predicate[options.functions]
|
---|
| 371 | };
|
---|
| 372 | }
|
---|
| 373 | };
|
---|