|
|
/** * @fileoverview Comma style - enforces comma styles of two types: last and first * @author Vignesh Anand aka vegetableman */
"use strict";
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = { meta: { type: "layout",
docs: { description: "enforce consistent comma style", category: "Stylistic Issues", recommended: false, url: "https://eslint.org/docs/rules/comma-style" },
fixable: "code",
schema: [ { enum: ["first", "last"] }, { type: "object", properties: { exceptions: { type: "object", additionalProperties: { type: "boolean" } } }, additionalProperties: false } ],
messages: { unexpectedLineBeforeAndAfterComma: "Bad line breaking before and after ','.", expectedCommaFirst: "',' should be placed first.", expectedCommaLast: "',' should be placed last." } },
create(context) { const style = context.options[0] || "last", sourceCode = context.getSourceCode(); const exceptions = { ArrayPattern: true, ArrowFunctionExpression: true, CallExpression: true, FunctionDeclaration: true, FunctionExpression: true, ImportDeclaration: true, ObjectPattern: true, NewExpression: true };
if (context.options.length === 2 && Object.prototype.hasOwnProperty.call(context.options[1], "exceptions")) { const keys = Object.keys(context.options[1].exceptions);
for (let i = 0; i < keys.length; i++) { exceptions[keys[i]] = context.options[1].exceptions[keys[i]]; } }
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/** * Modified text based on the style * @param {string} styleType Style type * @param {string} text Source code text * @returns {string} modified text * @private */ function getReplacedText(styleType, text) { switch (styleType) { case "between": return `,${text.replace(astUtils.LINEBREAK_MATCHER, "")}`;
case "first": return `${text},`;
case "last": return `,${text}`;
default: return ""; } }
/** * Determines the fixer function for a given style. * @param {string} styleType comma style * @param {ASTNode} previousItemToken The token to check. * @param {ASTNode} commaToken The token to check. * @param {ASTNode} currentItemToken The token to check. * @returns {Function} Fixer function * @private */ function getFixerFunction(styleType, previousItemToken, commaToken, currentItemToken) { const text = sourceCode.text.slice(previousItemToken.range[1], commaToken.range[0]) + sourceCode.text.slice(commaToken.range[1], currentItemToken.range[0]); const range = [previousItemToken.range[1], currentItemToken.range[0]];
return function(fixer) { return fixer.replaceTextRange(range, getReplacedText(styleType, text)); }; }
/** * Validates the spacing around single items in lists. * @param {Token} previousItemToken The last token from the previous item. * @param {Token} commaToken The token representing the comma. * @param {Token} currentItemToken The first token of the current item. * @param {Token} reportItem The item to use when reporting an error. * @returns {void} * @private */ function validateCommaItemSpacing(previousItemToken, commaToken, currentItemToken, reportItem) {
// if single line
if (astUtils.isTokenOnSameLine(commaToken, currentItemToken) && astUtils.isTokenOnSameLine(previousItemToken, commaToken)) {
// do nothing.
} else if (!astUtils.isTokenOnSameLine(commaToken, currentItemToken) && !astUtils.isTokenOnSameLine(previousItemToken, commaToken)) {
const comment = sourceCode.getCommentsAfter(commaToken)[0]; const styleType = comment && comment.type === "Block" && astUtils.isTokenOnSameLine(commaToken, comment) ? style : "between";
// lone comma
context.report({ node: reportItem, loc: commaToken.loc, messageId: "unexpectedLineBeforeAndAfterComma", fix: getFixerFunction(styleType, previousItemToken, commaToken, currentItemToken) });
} else if (style === "first" && !astUtils.isTokenOnSameLine(commaToken, currentItemToken)) {
context.report({ node: reportItem, loc: commaToken.loc, messageId: "expectedCommaFirst", fix: getFixerFunction(style, previousItemToken, commaToken, currentItemToken) });
} else if (style === "last" && astUtils.isTokenOnSameLine(commaToken, currentItemToken)) {
context.report({ node: reportItem, loc: commaToken.loc, messageId: "expectedCommaLast", fix: getFixerFunction(style, previousItemToken, commaToken, currentItemToken) }); } }
/** * Checks the comma placement with regards to a declaration/property/element * @param {ASTNode} node The binary expression node to check * @param {string} property The property of the node containing child nodes. * @private * @returns {void} */ function validateComma(node, property) { const items = node[property], arrayLiteral = (node.type === "ArrayExpression" || node.type === "ArrayPattern");
if (items.length > 1 || arrayLiteral) {
// seed as opening [
let previousItemToken = sourceCode.getFirstToken(node);
items.forEach(item => { const commaToken = item ? sourceCode.getTokenBefore(item) : previousItemToken, currentItemToken = item ? sourceCode.getFirstToken(item) : sourceCode.getTokenAfter(commaToken), reportItem = item || currentItemToken;
/* * This works by comparing three token locations: * - previousItemToken is the last token of the previous item * - commaToken is the location of the comma before the current item * - currentItemToken is the first token of the current item * * These values get switched around if item is undefined. * previousItemToken will refer to the last token not belonging * to the current item, which could be a comma or an opening * square bracket. currentItemToken could be a comma. * * All comparisons are done based on these tokens directly, so * they are always valid regardless of an undefined item. */ if (astUtils.isCommaToken(commaToken)) { validateCommaItemSpacing(previousItemToken, commaToken, currentItemToken, reportItem); }
if (item) { const tokenAfterItem = sourceCode.getTokenAfter(item, astUtils.isNotClosingParenToken);
previousItemToken = tokenAfterItem ? sourceCode.getTokenBefore(tokenAfterItem) : sourceCode.ast.tokens[sourceCode.ast.tokens.length - 1]; } });
/* * Special case for array literals that have empty last items, such * as [ 1, 2, ]. These arrays only have two items show up in the * AST, so we need to look at the token to verify that there's no * dangling comma. */ if (arrayLiteral) {
const lastToken = sourceCode.getLastToken(node), nextToLastToken = sourceCode.getTokenBefore(lastToken);
if (astUtils.isCommaToken(nextToLastToken)) { validateCommaItemSpacing( sourceCode.getTokenBefore(nextToLastToken), nextToLastToken, lastToken, lastToken ); } } } }
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
const nodes = {};
if (!exceptions.VariableDeclaration) { nodes.VariableDeclaration = function(node) { validateComma(node, "declarations"); }; } if (!exceptions.ObjectExpression) { nodes.ObjectExpression = function(node) { validateComma(node, "properties"); }; } if (!exceptions.ObjectPattern) { nodes.ObjectPattern = function(node) { validateComma(node, "properties"); }; } if (!exceptions.ArrayExpression) { nodes.ArrayExpression = function(node) { validateComma(node, "elements"); }; } if (!exceptions.ArrayPattern) { nodes.ArrayPattern = function(node) { validateComma(node, "elements"); }; } if (!exceptions.FunctionDeclaration) { nodes.FunctionDeclaration = function(node) { validateComma(node, "params"); }; } if (!exceptions.FunctionExpression) { nodes.FunctionExpression = function(node) { validateComma(node, "params"); }; } if (!exceptions.ArrowFunctionExpression) { nodes.ArrowFunctionExpression = function(node) { validateComma(node, "params"); }; } if (!exceptions.CallExpression) { nodes.CallExpression = function(node) { validateComma(node, "arguments"); }; } if (!exceptions.ImportDeclaration) { nodes.ImportDeclaration = function(node) { validateComma(node, "specifiers"); }; } if (!exceptions.NewExpression) { nodes.NewExpression = function(node) { validateComma(node, "arguments"); }; }
return nodes; } };
|