/** * @fileoverview Rule to forbid or enforce dangling commas. * @author Ian Christian Myers * @deprecated in ESLint v8.53.0 */ 'use strict'; //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ const astUtils = require('./utils/ast-utils'); //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ const DEFAULT_OPTIONS = Object.freeze({ arrays: 'never', objects: 'never', imports: 'never', exports: 'never', functions: 'never', }); /** * Checks whether or not a trailing comma is allowed in a given node. * If the `lastItem` is `RestElement` or `RestProperty`, it disallows trailing commas. * @param {ASTNode} lastItem The node of the last element in the given node. * @returns {boolean} `true` if a trailing comma is allowed. */ function isTrailingCommaAllowed(lastItem) { return !( lastItem.type === 'RestElement' || lastItem.type === 'RestProperty' || lastItem.type === 'ExperimentalRestProperty' ); } /** * Normalize option value. * @param {string|Object|undefined} optionValue The 1st option value to normalize. * @param {number} ecmaVersion The normalized ECMAScript version. * @returns {Object} The normalized option value. */ function normalizeOptions(optionValue, ecmaVersion) { if (typeof optionValue === 'string') { return { arrays: optionValue, objects: optionValue, imports: optionValue, exports: optionValue, functions: ecmaVersion < 2017 ? 'ignore' : optionValue, }; } if (typeof optionValue === 'object' && optionValue !== null) { return { arrays: optionValue.arrays || DEFAULT_OPTIONS.arrays, objects: optionValue.objects || DEFAULT_OPTIONS.objects, imports: optionValue.imports || DEFAULT_OPTIONS.imports, exports: optionValue.exports || DEFAULT_OPTIONS.exports, functions: optionValue.functions || DEFAULT_OPTIONS.functions, }; } return DEFAULT_OPTIONS; } //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ /** @type {import('../types').Rule.RuleModule} */ module.exports = { meta: { deprecated: { message: 'Formatting rules are being moved out of ESLint core.', url: 'https://eslint.org/blog/2023/10/deprecating-formatting-rules/', deprecatedSince: '8.53.0', availableUntil: '10.0.0', replacedBy: [ { message: 'ESLint Stylistic now maintains deprecated stylistic core rules.', url: 'https://eslint.style/guide/migration', plugin: { name: '@stylistic/eslint-plugin-js', url: 'https://eslint.style/packages/js', }, rule: { name: 'comma-dangle', url: 'https://eslint.style/rules/js/comma-dangle', }, }, ], }, type: 'layout', docs: { description: 'Require or disallow trailing commas', recommended: false, url: 'https://eslint.org/docs/latest/rules/comma-dangle', }, fixable: 'code', schema: { definitions: { value: { enum: ['always-multiline', 'always', 'never', 'only-multiline'], }, valueWithIgnore: { enum: [ 'always-multiline', 'always', 'ignore', 'never', 'only-multiline', ], }, }, type: 'array', items: [ { oneOf: [ { $ref: '#/definitions/value', }, { type: 'object', properties: { arrays: { $ref: '#/definitions/valueWithIgnore', }, objects: { $ref: '#/definitions/valueWithIgnore', }, imports: { $ref: '#/definitions/valueWithIgnore', }, exports: { $ref: '#/definitions/valueWithIgnore', }, functions: { $ref: '#/definitions/valueWithIgnore', }, }, additionalProperties: false, }, ], }, ], additionalItems: false, }, messages: { unexpected: 'Unexpected trailing comma.', missing: 'Missing trailing comma.', }, }, create(context) { const options = normalizeOptions( context.options[0], context.languageOptions.ecmaVersion ); const sourceCode = context.sourceCode; /** * Gets the last item of the given node. * @param {ASTNode} node The node to get. * @returns {ASTNode|null} The last node or null. */ function getLastItem(node) { /** * Returns the last element of an array * @param {any[]} array The input array * @returns {any} The last element */ function last(array) { return array.at(-1); } switch (node.type) { case 'ObjectExpression': case 'ObjectPattern': return last(node.properties); case 'ArrayExpression': case 'ArrayPattern': return last(node.elements); case 'ImportDeclaration': case 'ExportNamedDeclaration': return last(node.specifiers); case 'FunctionDeclaration': case 'FunctionExpression': case 'ArrowFunctionExpression': return last(node.params); case 'CallExpression': case 'NewExpression': return last(node.arguments); default: return null; } } /** * Gets the trailing comma token of the given node. * If the trailing comma does not exist, this returns the token which is * the insertion point of the trailing comma token. * @param {ASTNode} node The node to get. * @param {ASTNode} lastItem The last item of the node. * @returns {Token} The trailing comma token or the insertion point. */ function getTrailingToken(node, lastItem) { switch (node.type) { case 'ObjectExpression': case 'ArrayExpression': case 'CallExpression': case 'NewExpression': return sourceCode.getLastToken(node, 1); default: { const nextToken = sourceCode.getTokenAfter(lastItem); if (astUtils.isCommaToken(nextToken)) { return nextToken; } return sourceCode.getLastToken(lastItem); } } } /** * Checks whether or not a given node is multiline. * This rule handles a given node as multiline when the closing parenthesis * and the last element are not on the same line. * @param {ASTNode} node A node to check. * @returns {boolean} `true` if the node is multiline. */ function isMultiline(node) { const lastItem = getLastItem(node); if (!lastItem) { return false; } const penultimateToken = getTrailingToken(node, lastItem); const lastToken = sourceCode.getTokenAfter(penultimateToken); return lastToken.loc.end.line !== penultimateToken.loc.end.line; } /** * Reports a trailing comma if it exists. * @param {ASTNode} node A node to check. Its type is one of * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern, * ImportDeclaration, and ExportNamedDeclaration. * @returns {void} */ function forbidTrailingComma(node) { const lastItem = getLastItem(node); if ( !lastItem || (node.type === 'ImportDeclaration' && lastItem.type !== 'ImportSpecifier') ) { return; } const trailingToken = getTrailingToken(node, lastItem); if (astUtils.isCommaToken(trailingToken)) { context.report({ node: lastItem, loc: trailingToken.loc, messageId: 'unexpected', *fix(fixer) { yield fixer.remove(trailingToken); /* * Extend the range of the fix to include surrounding tokens to ensure * that the element after which the comma is removed stays _last_. * This intentionally makes conflicts in fix ranges with rules that may be * adding or removing elements in the same autofix pass. * https://github.com/eslint/eslint/issues/15660 */ yield fixer.insertTextBefore( sourceCode.getTokenBefore(trailingToken), '' ); yield fixer.insertTextAfter( sourceCode.getTokenAfter(trailingToken), '' ); }, }); } } /** * Reports the last element of a given node if it does not have a trailing * comma. * * If a given node is `ArrayPattern` which has `RestElement`, the trailing * comma is disallowed, so report if it exists. * @param {ASTNode} node A node to check. Its type is one of * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern, * ImportDeclaration, and ExportNamedDeclaration. * @returns {void} */ function forceTrailingComma(node) { const lastItem = getLastItem(node); if ( !lastItem || (node.type === 'ImportDeclaration' && lastItem.type !== 'ImportSpecifier') ) { return; } if (!isTrailingCommaAllowed(lastItem)) { forbidTrailingComma(node); return; } const trailingToken = getTrailingToken(node, lastItem); if (trailingToken.value !== ',') { context.report({ node: lastItem, loc: { start: trailingToken.loc.end, end: astUtils.getNextLocation(sourceCode, trailingToken.loc.end), }, messageId: 'missing', *fix(fixer) { yield fixer.insertTextAfter(trailingToken, ','); /* * Extend the range of the fix to include surrounding tokens to ensure * that the element after which the comma is inserted stays _last_. * This intentionally makes conflicts in fix ranges with rules that may be * adding or removing elements in the same autofix pass. * https://github.com/eslint/eslint/issues/15660 */ yield fixer.insertTextBefore(trailingToken, ''); yield fixer.insertTextAfter( sourceCode.getTokenAfter(trailingToken), '' ); }, }); } } /** * If a given node is multiline, reports the last element of a given node * when it does not have a trailing comma. * Otherwise, reports a trailing comma if it exists. * @param {ASTNode} node A node to check. Its type is one of * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern, * ImportDeclaration, and ExportNamedDeclaration. * @returns {void} */ function forceTrailingCommaIfMultiline(node) { if (isMultiline(node)) { forceTrailingComma(node); } else { forbidTrailingComma(node); } } /** * Only if a given node is not multiline, reports the last element of a given node * when it does not have a trailing comma. * Otherwise, reports a trailing comma if it exists. * @param {ASTNode} node A node to check. Its type is one of * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern, * ImportDeclaration, and ExportNamedDeclaration. * @returns {void} */ function allowTrailingCommaIfMultiline(node) { if (!isMultiline(node)) { forbidTrailingComma(node); } } const predicate = { always: forceTrailingComma, 'always-multiline': forceTrailingCommaIfMultiline, 'only-multiline': allowTrailingCommaIfMultiline, never: forbidTrailingComma, ignore() {}, }; return { ObjectExpression: predicate[options.objects], ObjectPattern: predicate[options.objects], ArrayExpression: predicate[options.arrays], ArrayPattern: predicate[options.arrays], ImportDeclaration: predicate[options.imports], ExportNamedDeclaration: predicate[options.exports], FunctionDeclaration: predicate[options.functions], FunctionExpression: predicate[options.functions], ArrowFunctionExpression: predicate[options.functions], CallExpression: predicate[options.functions], NewExpression: predicate[options.functions], }; }, };