boolean: "boolean" in options ? options.boolean : true,
number: "number" in options ? options.number : true,
string: "string" in options ? options.string : true,
+ disallowTemplateShorthand: "disallowTemplateShorthand" in options ? options.disallowTemplateShorthand : false,
allow: options.allow || []
};
}
* @returns {boolean} Whether or not the node is a binary negating of `.indexOf()` method calling.
*/
function isBinaryNegatingOfIndexOf(node) {
+ if (node.operator !== "~") {
+ return false;
+ }
+ const callNode = astUtils.skipChainExpression(node.argument);
+
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)
+ callNode.type === "CallExpression" &&
+ astUtils.isSpecificMemberAccess(callNode.callee, null, INDEX_OF_PATTERN)
);
}
return null;
}
+/**
+ * Checks whether an expression evaluates to a string.
+ * @param {ASTNode} node node that represents the expression to check.
+ * @returns {boolean} Whether or not the expression evaluates to a string.
+ */
+function isStringType(node) {
+ return astUtils.isStringLiteral(node) ||
+ (
+ node.type === "CallExpression" &&
+ node.callee.type === "Identifier" &&
+ node.callee.name === "String"
+ );
+}
+
/**
* Checks whether a node is an empty string literal or not.
* @param {ASTNode} node The node to check.
*/
function isConcatWithEmptyString(node) {
return node.operator === "+" && (
- (isEmptyString(node.left) && !astUtils.isStringLiteral(node.right)) ||
- (isEmptyString(node.right) && !astUtils.isStringLiteral(node.left))
+ (isEmptyString(node.left) && !isStringType(node.right)) ||
+ (isEmptyString(node.right) && !isStringType(node.left))
);
}
type: "boolean",
default: true
},
+ disallowTemplateShorthand: {
+ type: "boolean",
+ default: false
+ },
allow: {
type: "array",
items: {
}
},
additionalProperties: false
- }]
+ }],
+
+ messages: {
+ useRecommendation: "use `{{recommendation}}` instead."
+ }
},
create(context) {
function report(node, recommendation, shouldFix) {
context.report({
node,
- message: "use `{{recommendation}}` instead.",
+ messageId: "useRecommendation",
data: {
recommendation
},
// ~foo.indexOf(bar)
operatorAllowed = options.allow.indexOf("~") >= 0;
if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) {
- const recommendation = `${sourceCode.getText(node.argument)} !== -1`;
+
+ // `foo?.indexOf(bar) !== -1` will be true (== found) if the `foo` is nullish. So use `>= 0` in that case.
+ const comparison = node.argument.type === "ChainExpression" ? ">= 0" : "!== -1";
+ const recommendation = `${sourceCode.getText(node.argument)} ${comparison}`;
report(node, recommendation, false);
}
report(node, recommendation, true);
}
+ },
+
+ TemplateLiteral(node) {
+ if (!options.disallowTemplateShorthand) {
+ return;
+ }
+
+ // tag`${foo}`
+ if (node.parent.type === "TaggedTemplateExpression") {
+ return;
+ }
+
+ // `` or `${foo}${bar}`
+ if (node.expressions.length !== 1) {
+ return;
+ }
+
+
+ // `prefix${foo}`
+ if (node.quasis[0].value.cooked !== "") {
+ return;
+ }
+
+ // `${foo}postfix`
+ if (node.quasis[1].value.cooked !== "") {
+ return;
+ }
+
+ // if the expression is already a string, then this isn't a coercion
+ if (isStringType(node.expressions[0])) {
+ return;
+ }
+
+ const code = sourceCode.getText(node.expressions[0]);
+ const recommendation = `String(${code})`;
+
+ report(node, recommendation, true);
}
};
}