Skip to content
Permalink
ed9506bbaf
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
Latest commit c96f843 Sep 14, 2020 History
0 contributors

Users who have contributed to this file

247 lines (206 sloc) 9.63 KB
/**
* @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);
}
};
}
};