summaryrefslogtreecommitdiff
path: root/tools/eslint/lib/rules/no-implicit-coercion.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/eslint/lib/rules/no-implicit-coercion.js')
-rw-r--r--tools/eslint/lib/rules/no-implicit-coercion.js220
1 files changed, 220 insertions, 0 deletions
diff --git a/tools/eslint/lib/rules/no-implicit-coercion.js b/tools/eslint/lib/rules/no-implicit-coercion.js
new file mode 100644
index 0000000000..595412b184
--- /dev/null
+++ b/tools/eslint/lib/rules/no-implicit-coercion.js
@@ -0,0 +1,220 @@
+/**
+ * @fileoverview A rule to disallow the type conversions with shorter notations.
+ * @author Toru Nagashima
+ * @copyright 2015 Toru Nagashima. All rights reserved.
+ */
+
+"use strict";
+
+//------------------------------------------------------------------------------
+// Helpers
+//------------------------------------------------------------------------------
+
+var INDEX_OF_PATTERN = /^(?:i|lastI)ndexOf$/;
+
+/**
+ * Parses and normalizes an option object.
+ * @param {object} options - An option object to parse.
+ * @returns {object} The parsed and normalized option object.
+ */
+function parseOptions(options) {
+ options = options || {};
+ return {
+ boolean: "boolean" in options ? Boolean(options.boolean) : true,
+ number: "number" in options ? Boolean(options.number) : true,
+ string: "string" in options ? Boolean(options.string) : true
+ };
+}
+
+/**
+ * Checks whether or not a node is a double logical nigating.
+ * @param {ASTNode} node - An UnaryExpression node to check.
+ * @returns {boolean} Whether or not the node is a double logical nigating.
+ */
+function isDoubleLogicalNegating(node) {
+ return (
+ node.operator === "!" &&
+ node.argument.type === "UnaryExpression" &&
+ node.argument.operator === "!"
+ );
+}
+
+/**
+ * Checks whether or not a node is a binary negating of `.indexOf()` method calling.
+ * @param {ASTNode} node - An UnaryExpression node to check.
+ * @returns {boolean} Whether or not the node is a binary negating of `.indexOf()` method calling.
+ */
+function isBinaryNegatingOfIndexOf(node) {
+ return (
+ node.operator === "~" &&
+ node.argument.type === "CallExpression" &&
+ node.argument.callee.type === "MemberExpression" &&
+ node.argument.callee.property.type === "Identifier" &&
+ INDEX_OF_PATTERN.test(node.argument.callee.property.name)
+ );
+}
+
+/**
+ * Checks whether or not a node is a multiplying by one.
+ * @param {BinaryExpression} node - A BinaryExpression node to check.
+ * @returns {boolean} Whether or not the node is a multiplying by one.
+ */
+function isMultiplyByOne(node) {
+ return node.operator === "*" && (
+ node.left.type === "Literal" && node.left.value === 1 ||
+ node.right.type === "Literal" && node.right.value === 1
+ );
+}
+
+/**
+ * Checks whether the result of a node is numeric or not
+ * @param {ASTNode} node The node to test
+ * @returns {boolean} true if the node is a number literal or a `Number()`, `parseInt` or `parseFloat` call
+ */
+function isNumeric(node) {
+ return (
+ node.type === "Literal" && typeof node.value === "number" ||
+ node.type === "CallExpression" && (
+ node.callee.name === "Number" ||
+ node.callee.name === "parseInt" ||
+ node.callee.name === "parseFloat"
+ )
+ );
+}
+
+/**
+ * Returns the first non-numeric operand in a BinaryExpression. Designed to be
+ * used from bottom to up since it walks up the BinaryExpression trees using
+ * node.parent to find the result.
+ * @param {BinaryExpression} node The BinaryExpression node to be walked up on
+ * @returns {ASTNode|undefined} The first non-numeric item in the BinaryExpression tree or undefined
+ */
+function getNonNumericOperand(node) {
+ var left = node.left, right = node.right;
+
+ if (right.type !== "BinaryExpression" && !isNumeric(right)) {
+ return right;
+ }
+
+ if (left.type !== "BinaryExpression" && !isNumeric(left)) {
+ return left;
+ }
+}
+
+/**
+ * Checks whether or not a node is a concatenating with an empty string.
+ * @param {ASTNode} node - A BinaryExpression node to check.
+ * @returns {boolean} Whether or not the node is a concatenating with an empty string.
+ */
+function isConcatWithEmptyString(node) {
+ return node.operator === "+" && (
+ (node.left.type === "Literal" && node.left.value === "") ||
+ (node.right.type === "Literal" && node.right.value === "")
+ );
+}
+
+/**
+ * Checks whether or not a node is appended with an empty string.
+ * @param {ASTNode} node - An AssignmentExpression node to check.
+ * @returns {boolean} Whether or not the node is appended with an empty string.
+ */
+function isAppendEmptyString(node) {
+ return node.operator === "+=" && node.right.type === "Literal" && node.right.value === "";
+}
+
+/**
+ * Gets a node that is the left or right operand of a node, is not the specified literal.
+ * @param {ASTNode} node - A BinaryExpression node to get.
+ * @param {any} value - A literal value to check.
+ * @returns {ASTNode} A node that is the left or right operand of the node, is not the specified literal.
+ */
+function getOtherOperand(node, value) {
+ if (node.left.type === "Literal" && node.left.value === value) {
+ return node.right;
+ }
+ return node.left;
+}
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+module.exports = function(context) {
+ var options = parseOptions(context.options[0]);
+
+ return {
+ "UnaryExpression": function(node) {
+ // !!foo
+ if (options.boolean && isDoubleLogicalNegating(node)) {
+ context.report(
+ node,
+ "use `Boolean({{code}})` instead.",
+ {code: context.getSource(node.argument.argument)});
+ }
+
+ // ~foo.indexOf(bar)
+ if (options.boolean && isBinaryNegatingOfIndexOf(node)) {
+ context.report(
+ node,
+ "use `{{code}} !== -1` instead.",
+ {code: context.getSource(node.argument)});
+ }
+
+ // +foo
+ if (options.number && node.operator === "+" && !isNumeric(node.argument)) {
+ context.report(
+ node,
+ "use `Number({{code}})` instead.",
+ {code: context.getSource(node.argument)});
+ }
+ },
+
+ // Use `:exit` to prevent double reporting
+ "BinaryExpression:exit": function(node) {
+ // 1 * foo
+ var nonNumericOperand = options.number && isMultiplyByOne(node) && getNonNumericOperand(node);
+ if (nonNumericOperand) {
+ context.report(
+ node,
+ "use `Number({{code}})` instead.",
+ {code: context.getSource(nonNumericOperand)});
+ }
+
+ // "" + foo
+ if (options.string && isConcatWithEmptyString(node)) {
+ context.report(
+ node,
+ "use `String({{code}})` instead.",
+ {code: context.getSource(getOtherOperand(node, ""))});
+ }
+ },
+
+ "AssignmentExpression": function(node) {
+ // foo += ""
+ if (options.string && isAppendEmptyString(node)) {
+ context.report(
+ node,
+ "use `{{code}} = String({{code}})` instead.",
+ {code: context.getSource(getOtherOperand(node, ""))});
+ }
+ }
+ };
+};
+
+module.exports.schema = [
+ {
+ "type": "object",
+ "properties": {
+ "boolean": {
+ "type": "boolean"
+ },
+ "number": {
+ "type": "boolean"
+ },
+ "string": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false
+ }
+];