You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

241 lines
8.1 KiB

/**
* @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$/;
var ALLOWABLE_OPERATORS = ["~", "!!", "+", "*"];
/**
* 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,
allow: options.allow || []
};
}
/**
* 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|null} The first non-numeric item in the BinaryExpression tree or null
*/
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;
}
return null;
}
/**
* 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]),
operatorAllowed = false;
return {
"UnaryExpression": function(node) {
// !!foo
operatorAllowed = options.allow.indexOf("!!") >= 0;
if (!operatorAllowed && options.boolean && isDoubleLogicalNegating(node)) {
context.report(
node,
"use `Boolean({{code}})` instead.", {
code: context.getSource(node.argument.argument)
});
}
// ~foo.indexOf(bar)
operatorAllowed = options.allow.indexOf("~") >= 0;
if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) {
context.report(
node,
"use `{{code}} !== -1` instead.", {
code: context.getSource(node.argument)
});
}
// +foo
operatorAllowed = options.allow.indexOf("+") >= 0;
if (!operatorAllowed && 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
operatorAllowed = options.allow.indexOf("*") >= 0;
var nonNumericOperand = !operatorAllowed && options.number && isMultiplyByOne(node) && getNonNumericOperand(node);
if (nonNumericOperand) {
context.report(
node,
"use `Number({{code}})` instead.", {
code: context.getSource(nonNumericOperand)
});
}
// "" + foo
operatorAllowed = options.allow.indexOf("+") >= 0;
if (!operatorAllowed && options.string && isConcatWithEmptyString(node)) {
context.report(
node,
"use `String({{code}})` instead.", {
code: context.getSource(getOtherOperand(node, ""))
});
}
},
"AssignmentExpression": function(node) {
// foo += ""
operatorAllowed = options.allow.indexOf("+") >= 0;
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"
},
"allow": {
"type": "array",
"items": {
"enum": ALLOWABLE_OPERATORS
},
"uniqueItems": true
}
},
"additionalProperties": false
}];