UNPKG

6.94 kBJavaScriptView Raw
1/**
2 * @fileoverview Rule to disallow mixed binary operators.
3 * @author Toru Nagashima
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Requirements
10//------------------------------------------------------------------------------
11
12const astUtils = require("../ast-utils.js");
13
14//------------------------------------------------------------------------------
15// Helpers
16//------------------------------------------------------------------------------
17
18const ARITHMETIC_OPERATORS = ["+", "-", "*", "/", "%", "**"];
19const BITWISE_OPERATORS = ["&", "|", "^", "~", "<<", ">>", ">>>"];
20const COMPARISON_OPERATORS = ["==", "!=", "===", "!==", ">", ">=", "<", "<="];
21const LOGICAL_OPERATORS = ["&&", "||"];
22const RELATIONAL_OPERATORS = ["in", "instanceof"];
23const ALL_OPERATORS = [].concat(
24 ARITHMETIC_OPERATORS,
25 BITWISE_OPERATORS,
26 COMPARISON_OPERATORS,
27 LOGICAL_OPERATORS,
28 RELATIONAL_OPERATORS
29);
30const DEFAULT_GROUPS = [
31 ARITHMETIC_OPERATORS,
32 BITWISE_OPERATORS,
33 COMPARISON_OPERATORS,
34 LOGICAL_OPERATORS,
35 RELATIONAL_OPERATORS
36];
37const TARGET_NODE_TYPE = /^(?:Binary|Logical)Expression$/;
38
39/**
40 * Normalizes options.
41 *
42 * @param {Object|undefined} options - A options object to normalize.
43 * @returns {Object} Normalized option object.
44 */
45function normalizeOptions(options) {
46 const hasGroups = (options && options.groups && options.groups.length > 0);
47 const groups = hasGroups ? options.groups : DEFAULT_GROUPS;
48 const allowSamePrecedence = (options && options.allowSamePrecedence) !== false;
49
50 return {
51 groups,
52 allowSamePrecedence
53 };
54}
55
56/**
57 * Checks whether any group which includes both given operator exists or not.
58 *
59 * @param {Array.<string[]>} groups - A list of groups to check.
60 * @param {string} left - An operator.
61 * @param {string} right - Another operator.
62 * @returns {boolean} `true` if such group existed.
63 */
64function includesBothInAGroup(groups, left, right) {
65 return groups.some(function(group) {
66 return group.indexOf(left) !== -1 && group.indexOf(right) !== -1;
67 });
68}
69
70//------------------------------------------------------------------------------
71// Rule Definition
72//------------------------------------------------------------------------------
73
74module.exports = {
75 meta: {
76 docs: {
77 description: "disallow mixed binary operators",
78 category: "Stylistic Issues",
79 recommended: false
80 },
81 schema: [
82 {
83 type: "object",
84 properties: {
85 groups: {
86 type: "array",
87 items: {
88 type: "array",
89 items: {enum: ALL_OPERATORS},
90 minItems: 2,
91 uniqueItems: true
92 },
93 uniqueItems: true
94 },
95 allowSamePrecedence: {
96 type: "boolean"
97 }
98 },
99 additionalProperties: false
100 }
101 ]
102 },
103
104 create(context) {
105 const sourceCode = context.getSourceCode();
106 const options = normalizeOptions(context.options[0]);
107
108 /**
109 * Checks whether a given node should be ignored by options or not.
110 *
111 * @param {ASTNode} node - A node to check. This is a BinaryExpression
112 * node or a LogicalExpression node. This parent node is one of
113 * them, too.
114 * @returns {boolean} `true` if the node should be ignored.
115 */
116 function shouldIgnore(node) {
117 const a = node;
118 const b = node.parent;
119
120 return (
121 !includesBothInAGroup(options.groups, a.operator, b.operator) ||
122 (
123 options.allowSamePrecedence &&
124 astUtils.getPrecedence(a) === astUtils.getPrecedence(b)
125 )
126 );
127 }
128
129 /**
130 * Checks whether the operator of a given node is mixed with parent
131 * node's operator or not.
132 *
133 * @param {ASTNode} node - A node to check. This is a BinaryExpression
134 * node or a LogicalExpression node. This parent node is one of
135 * them, too.
136 * @returns {boolean} `true` if the node was mixed.
137 */
138 function isMixedWithParent(node) {
139 return (
140 node.operator !== node.parent.operator &&
141 !astUtils.isParenthesised(sourceCode, node)
142 );
143 }
144
145 /**
146 * Gets the operator token of a given node.
147 *
148 * @param {ASTNode} node - A node to check. This is a BinaryExpression
149 * node or a LogicalExpression node.
150 * @returns {Token} The operator token of the node.
151 */
152 function getOperatorToken(node) {
153 let token = sourceCode.getTokenAfter(node.left);
154
155 while (token.value === ")") {
156 token = sourceCode.getTokenAfter(token);
157 }
158
159 return token;
160 }
161
162 /**
163 * Reports both the operator of a given node and the operator of the
164 * parent node.
165 *
166 * @param {ASTNode} node - A node to check. This is a BinaryExpression
167 * node or a LogicalExpression node. This parent node is one of
168 * them, too.
169 * @returns {void}
170 */
171 function reportBothOperators(node) {
172 const parent = node.parent;
173 const left = (parent.left === node) ? node : parent;
174 const right = (parent.left !== node) ? node : parent;
175 const message =
176 "Unexpected mix of '{{leftOperator}}' and '{{rightOperator}}'.";
177 const data = {
178 leftOperator: left.operator,
179 rightOperator: right.operator
180 };
181
182 context.report({
183 node: left,
184 loc: getOperatorToken(left).loc.start,
185 message,
186 data
187 });
188 context.report({
189 node: right,
190 loc: getOperatorToken(right).loc.start,
191 message,
192 data
193 });
194 }
195
196 /**
197 * Checks between the operator of this node and the operator of the
198 * parent node.
199 *
200 * @param {ASTNode} node - A node to check.
201 * @returns {void}
202 */
203 function check(node) {
204 if (TARGET_NODE_TYPE.test(node.parent.type) &&
205 isMixedWithParent(node) &&
206 !shouldIgnore(node)
207 ) {
208 reportBothOperators(node);
209 }
210 }
211
212 return {
213 BinaryExpression: check,
214 LogicalExpression: check
215 };
216 }
217};