123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 |
- /**
- * @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);
- }
- };
- }
- };
|