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.

221 lines
7.3 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$/;
/**
* 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
}
];