|
|
/** * @fileoverview Operator linebreak - enforces operator linebreak style of two types: after and before * @author Benoît Zugmeyer */
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = { meta: { type: "layout",
docs: { description: "enforce consistent linebreak style for operators", category: "Stylistic Issues", recommended: false, url: "https://eslint.org/docs/rules/operator-linebreak" },
schema: [ { enum: ["after", "before", "none", null] }, { type: "object", properties: { overrides: { type: "object", additionalProperties: { enum: ["after", "before", "none", "ignore"] } } }, additionalProperties: false } ],
fixable: "code",
messages: { operatorAtBeginning: "'{{operator}}' should be placed at the beginning of the line.", operatorAtEnd: "'{{operator}}' should be placed at the end of the line.", badLinebreak: "Bad line breaking before and after '{{operator}}'.", noLinebreak: "There should be no line break before or after '{{operator}}'." } },
create(context) {
const usedDefaultGlobal = !context.options[0]; const globalStyle = context.options[0] || "after"; const options = context.options[1] || {}; const styleOverrides = options.overrides ? Object.assign({}, options.overrides) : {};
if (usedDefaultGlobal && !styleOverrides["?"]) { styleOverrides["?"] = "before"; }
if (usedDefaultGlobal && !styleOverrides[":"]) { styleOverrides[":"] = "before"; }
const sourceCode = context.getSourceCode();
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
/** * Gets a fixer function to fix rule issues * @param {Token} operatorToken The operator token of an expression * @param {string} desiredStyle The style for the rule. One of 'before', 'after', 'none' * @returns {Function} A fixer function */ function getFixer(operatorToken, desiredStyle) { return fixer => { const tokenBefore = sourceCode.getTokenBefore(operatorToken); const tokenAfter = sourceCode.getTokenAfter(operatorToken); const textBefore = sourceCode.text.slice(tokenBefore.range[1], operatorToken.range[0]); const textAfter = sourceCode.text.slice(operatorToken.range[1], tokenAfter.range[0]); const hasLinebreakBefore = !astUtils.isTokenOnSameLine(tokenBefore, operatorToken); const hasLinebreakAfter = !astUtils.isTokenOnSameLine(operatorToken, tokenAfter); let newTextBefore, newTextAfter;
if (hasLinebreakBefore !== hasLinebreakAfter && desiredStyle !== "none") {
// If there is a comment before and after the operator, don't do a fix.
if (sourceCode.getTokenBefore(operatorToken, { includeComments: true }) !== tokenBefore && sourceCode.getTokenAfter(operatorToken, { includeComments: true }) !== tokenAfter) {
return null; }
/* * If there is only one linebreak and it's on the wrong side of the operator, swap the text before and after the operator. * foo && * bar * would get fixed to * foo * && bar */ newTextBefore = textAfter; newTextAfter = textBefore; } else { const LINEBREAK_REGEX = astUtils.createGlobalLinebreakMatcher();
// Otherwise, if no linebreak is desired and no comments interfere, replace the linebreaks with empty strings.
newTextBefore = desiredStyle === "before" || textBefore.trim() ? textBefore : textBefore.replace(LINEBREAK_REGEX, ""); newTextAfter = desiredStyle === "after" || textAfter.trim() ? textAfter : textAfter.replace(LINEBREAK_REGEX, "");
// If there was no change (due to interfering comments), don't output a fix.
if (newTextBefore === textBefore && newTextAfter === textAfter) { return null; } }
if (newTextAfter === "" && tokenAfter.type === "Punctuator" && "+-".includes(operatorToken.value) && tokenAfter.value === operatorToken.value) {
// To avoid accidentally creating a ++ or -- operator, insert a space if the operator is a +/- and the following token is a unary +/-.
newTextAfter += " "; }
return fixer.replaceTextRange([tokenBefore.range[1], tokenAfter.range[0]], newTextBefore + operatorToken.value + newTextAfter); }; }
/** * Checks the operator placement * @param {ASTNode} node The node to check * @param {ASTNode} leftSide The node that comes before the operator in `node` * @private * @returns {void} */ function validateNode(node, leftSide) {
/* * When the left part of a binary expression is a single expression wrapped in * parentheses (ex: `(a) + b`), leftToken will be the last token of the expression * and operatorToken will be the closing parenthesis. * The leftToken should be the last closing parenthesis, and the operatorToken * should be the token right after that. */ const operatorToken = sourceCode.getTokenAfter(leftSide, astUtils.isNotClosingParenToken); const leftToken = sourceCode.getTokenBefore(operatorToken); const rightToken = sourceCode.getTokenAfter(operatorToken); const operator = operatorToken.value; const operatorStyleOverride = styleOverrides[operator]; const style = operatorStyleOverride || globalStyle; const fix = getFixer(operatorToken, style);
// if single line
if (astUtils.isTokenOnSameLine(leftToken, operatorToken) && astUtils.isTokenOnSameLine(operatorToken, rightToken)) {
// do nothing.
} else if (operatorStyleOverride !== "ignore" && !astUtils.isTokenOnSameLine(leftToken, operatorToken) && !astUtils.isTokenOnSameLine(operatorToken, rightToken)) {
// lone operator
context.report({ node, loc: operatorToken.loc, messageId: "badLinebreak", data: { operator }, fix });
} else if (style === "before" && astUtils.isTokenOnSameLine(leftToken, operatorToken)) {
context.report({ node, loc: operatorToken.loc, messageId: "operatorAtBeginning", data: { operator }, fix });
} else if (style === "after" && astUtils.isTokenOnSameLine(operatorToken, rightToken)) {
context.report({ node, loc: operatorToken.loc, messageId: "operatorAtEnd", data: { operator }, fix });
} else if (style === "none") {
context.report({ node, loc: operatorToken.loc, messageId: "noLinebreak", data: { operator }, fix });
} }
/** * Validates a binary expression using `validateNode` * @param {BinaryExpression|LogicalExpression|AssignmentExpression} node node to be validated * @returns {void} */ function validateBinaryExpression(node) { validateNode(node, node.left); }
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return { BinaryExpression: validateBinaryExpression, LogicalExpression: validateBinaryExpression, AssignmentExpression: validateBinaryExpression, VariableDeclarator(node) { if (node.init) { validateNode(node, node.id); } }, ConditionalExpression(node) { validateNode(node, node.test); validateNode(node, node.consequent); } }; } };
|