417 lines
13 KiB
JavaScript
417 lines
13 KiB
JavaScript
/**
|
|
* @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],
|
|
};
|
|
},
|
|
};
|