230 lines
9.7 KiB
Plaintext
230 lines
9.7 KiB
Plaintext
|
"use strict";
|
||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||
|
if (k2 === undefined) k2 = k;
|
||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||
|
}
|
||
|
Object.defineProperty(o, k2, desc);
|
||
|
}) : (function(o, m, k, k2) {
|
||
|
if (k2 === undefined) k2 = k;
|
||
|
o[k2] = m[k];
|
||
|
}));
|
||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||
|
}) : function(o, v) {
|
||
|
o["default"] = v;
|
||
|
});
|
||
|
var __importStar = (this && this.__importStar) || function (mod) {
|
||
|
if (mod && mod.__esModule) return mod;
|
||
|
var result = {};
|
||
|
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||
|
__setModuleDefault(result, mod);
|
||
|
return result;
|
||
|
};
|
||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||
|
const regexpp_1 = require("@eslint-community/regexpp");
|
||
|
const utils_1 = require("@typescript-eslint/utils");
|
||
|
const ts = __importStar(require("typescript"));
|
||
|
const util_1 = require("../util");
|
||
|
exports.default = (0, util_1.createRule)({
|
||
|
name: 'prefer-includes',
|
||
|
defaultOptions: [],
|
||
|
meta: {
|
||
|
type: 'suggestion',
|
||
|
docs: {
|
||
|
description: 'Enforce `includes` method over `indexOf` method',
|
||
|
recommended: 'strict',
|
||
|
requiresTypeChecking: true,
|
||
|
},
|
||
|
fixable: 'code',
|
||
|
messages: {
|
||
|
preferIncludes: "Use 'includes()' method instead.",
|
||
|
preferStringIncludes: 'Use `String#includes()` method with a string instead.',
|
||
|
},
|
||
|
schema: [],
|
||
|
},
|
||
|
create(context) {
|
||
|
const globalScope = context.sourceCode.getScope(context.sourceCode.ast);
|
||
|
const services = (0, util_1.getParserServices)(context);
|
||
|
const checker = services.program.getTypeChecker();
|
||
|
function isNumber(node, value) {
|
||
|
const evaluated = (0, util_1.getStaticValue)(node, globalScope);
|
||
|
return evaluated != null && evaluated.value === value;
|
||
|
}
|
||
|
function isPositiveCheck(node) {
|
||
|
switch (node.operator) {
|
||
|
case '!==':
|
||
|
case '!=':
|
||
|
case '>':
|
||
|
return isNumber(node.right, -1);
|
||
|
case '>=':
|
||
|
return isNumber(node.right, 0);
|
||
|
default:
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
function isNegativeCheck(node) {
|
||
|
switch (node.operator) {
|
||
|
case '===':
|
||
|
case '==':
|
||
|
case '<=':
|
||
|
return isNumber(node.right, -1);
|
||
|
case '<':
|
||
|
return isNumber(node.right, 0);
|
||
|
default:
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
function hasSameParameters(nodeA, nodeB) {
|
||
|
if (!ts.isFunctionLike(nodeA) || !ts.isFunctionLike(nodeB)) {
|
||
|
return false;
|
||
|
}
|
||
|
const paramsA = nodeA.parameters;
|
||
|
const paramsB = nodeB.parameters;
|
||
|
if (paramsA.length !== paramsB.length) {
|
||
|
return false;
|
||
|
}
|
||
|
for (let i = 0; i < paramsA.length; ++i) {
|
||
|
const paramA = paramsA[i];
|
||
|
const paramB = paramsB[i];
|
||
|
// Check name, type, and question token once.
|
||
|
if (paramA.getText() !== paramB.getText()) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
/**
|
||
|
* Parse a given node if it's a `RegExp` instance.
|
||
|
* @param node The node to parse.
|
||
|
*/
|
||
|
function parseRegExp(node) {
|
||
|
const evaluated = (0, util_1.getStaticValue)(node, globalScope);
|
||
|
if (evaluated == null || !(evaluated.value instanceof RegExp)) {
|
||
|
return null;
|
||
|
}
|
||
|
const { pattern, flags } = (0, regexpp_1.parseRegExpLiteral)(evaluated.value);
|
||
|
if (pattern.alternatives.length !== 1 ||
|
||
|
flags.ignoreCase ||
|
||
|
flags.global) {
|
||
|
return null;
|
||
|
}
|
||
|
// Check if it can determine a unique string.
|
||
|
const chars = pattern.alternatives[0].elements;
|
||
|
if (!chars.every(c => c.type === 'Character')) {
|
||
|
return null;
|
||
|
}
|
||
|
// To string.
|
||
|
return String.fromCodePoint(...chars.map(c => c.value));
|
||
|
}
|
||
|
function escapeString(str) {
|
||
|
const EscapeMap = {
|
||
|
'\0': '\\0',
|
||
|
"'": "\\'",
|
||
|
'\\': '\\\\',
|
||
|
'\n': '\\n',
|
||
|
'\r': '\\r',
|
||
|
'\v': '\\v',
|
||
|
'\t': '\\t',
|
||
|
'\f': '\\f',
|
||
|
// "\b" cause unexpected replacements
|
||
|
// '\b': '\\b',
|
||
|
};
|
||
|
const replaceRegex = new RegExp(Object.values(EscapeMap).join('|'), 'g');
|
||
|
return str.replace(replaceRegex, char => EscapeMap[char]);
|
||
|
}
|
||
|
function checkArrayIndexOf(node, allowFixing) {
|
||
|
// Check if the comparison is equivalent to `includes()`.
|
||
|
const callNode = node.parent;
|
||
|
const compareNode = (callNode.parent.type === utils_1.AST_NODE_TYPES.ChainExpression
|
||
|
? callNode.parent.parent
|
||
|
: callNode.parent);
|
||
|
const negative = isNegativeCheck(compareNode);
|
||
|
if (!negative && !isPositiveCheck(compareNode)) {
|
||
|
return;
|
||
|
}
|
||
|
// Get the symbol of `indexOf` method.
|
||
|
const indexofMethodDeclarations = services
|
||
|
.getSymbolAtLocation(node.property)
|
||
|
?.getDeclarations();
|
||
|
if (indexofMethodDeclarations == null ||
|
||
|
indexofMethodDeclarations.length === 0) {
|
||
|
return;
|
||
|
}
|
||
|
// Check if every declaration of `indexOf` method has `includes` method
|
||
|
// and the two methods have the same parameters.
|
||
|
for (const instanceofMethodDecl of indexofMethodDeclarations) {
|
||
|
const typeDecl = instanceofMethodDecl.parent;
|
||
|
const type = checker.getTypeAtLocation(typeDecl);
|
||
|
const includesMethodDecl = type
|
||
|
.getProperty('includes')
|
||
|
?.getDeclarations();
|
||
|
if (!includesMethodDecl?.some(includesMethodDecl => hasSameParameters(includesMethodDecl, instanceofMethodDecl))) {
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
// Report it.
|
||
|
context.report({
|
||
|
node: compareNode,
|
||
|
messageId: 'preferIncludes',
|
||
|
...(allowFixing && {
|
||
|
*fix(fixer) {
|
||
|
if (negative) {
|
||
|
yield fixer.insertTextBefore(callNode, '!');
|
||
|
}
|
||
|
yield fixer.replaceText(node.property, 'includes');
|
||
|
yield fixer.removeRange([callNode.range[1], compareNode.range[1]]);
|
||
|
},
|
||
|
}),
|
||
|
});
|
||
|
}
|
||
|
return {
|
||
|
// a.indexOf(b) !== 1
|
||
|
"BinaryExpression > CallExpression.left > MemberExpression.callee[property.name='indexOf'][computed=false]"(node) {
|
||
|
checkArrayIndexOf(node, /* allowFixing */ true);
|
||
|
},
|
||
|
// a?.indexOf(b) !== 1
|
||
|
"BinaryExpression > ChainExpression.left > CallExpression > MemberExpression.callee[property.name='indexOf'][computed=false]"(node) {
|
||
|
checkArrayIndexOf(node, /* allowFixing */ false);
|
||
|
},
|
||
|
// /bar/.test(foo)
|
||
|
'CallExpression[arguments.length=1] > MemberExpression.callee[property.name="test"][computed=false]'(node) {
|
||
|
const callNode = node.parent;
|
||
|
const text = parseRegExp(node.object);
|
||
|
if (text == null) {
|
||
|
return;
|
||
|
}
|
||
|
//check the argument type of test methods
|
||
|
const argument = callNode.arguments[0];
|
||
|
const type = (0, util_1.getConstrainedTypeAtLocation)(services, argument);
|
||
|
const includesMethodDecl = type
|
||
|
.getProperty('includes')
|
||
|
?.getDeclarations();
|
||
|
if (includesMethodDecl == null) {
|
||
|
return;
|
||
|
}
|
||
|
context.report({
|
||
|
node: callNode,
|
||
|
messageId: 'preferStringIncludes',
|
||
|
*fix(fixer) {
|
||
|
const argNode = callNode.arguments[0];
|
||
|
const needsParen = argNode.type !== utils_1.AST_NODE_TYPES.Literal &&
|
||
|
argNode.type !== utils_1.AST_NODE_TYPES.TemplateLiteral &&
|
||
|
argNode.type !== utils_1.AST_NODE_TYPES.Identifier &&
|
||
|
argNode.type !== utils_1.AST_NODE_TYPES.MemberExpression &&
|
||
|
argNode.type !== utils_1.AST_NODE_TYPES.CallExpression;
|
||
|
yield fixer.removeRange([callNode.range[0], argNode.range[0]]);
|
||
|
yield fixer.removeRange([argNode.range[1], callNode.range[1]]);
|
||
|
if (needsParen) {
|
||
|
yield fixer.insertTextBefore(argNode, '(');
|
||
|
yield fixer.insertTextAfter(argNode, ')');
|
||
|
}
|
||
|
yield fixer.insertTextAfter(argNode, `${node.optional ? '?.' : '.'}includes('${escapeString(text)}')`);
|
||
|
},
|
||
|
});
|
||
|
},
|
||
|
};
|
||
|
},
|
||
|
});
|
||
|
//# sourceMappingURL=prefer-includes.js.map
|