📄 prefer-includes¶
📊 Analysis Summary¶
| Metric | Count |
|---|---|
| 🔧 Functions | 8 |
| 📦 Imports | 9 |
📚 Table of Contents¶
🛠️ File Location:¶
📂 packages/eslint-plugin/src/rules/prefer-includes.ts
📤 Default Export¶
| Property | Value |
|---|---|
name |
'prefer-includes' |
meta.type |
'suggestion' |
meta.docs.description |
'Enforce includes method over indexOf method' |
meta.docs.recommended |
'stylistic' |
meta.docs.requiresTypeChecking |
true |
meta.fixable |
'code' |
meta.messages.preferIncludes |
"Use 'includes()' method instead." |
meta.messages.preferStringIncludes |
'Use String#includes() method with a string instead.' |
meta.schema |
[] |
defaultOptions |
[] |
Entry point: create — documented under Functions.
📦 Imports¶
| Name | Source |
|---|---|
TSESLint |
@typescript-eslint/utils |
TSESTree |
@typescript-eslint/utils |
parseRegExpLiteral |
@eslint-community/regexpp |
AST_NODE_TYPES |
@typescript-eslint/utils |
createRule |
../util |
getConstrainedTypeAtLocation |
../util |
getParserServices |
../util |
getStaticValue |
../util |
isStaticMemberAccessOfValue |
../util |
Functions¶
create(context: any): { 'BinaryExpression > CallExpression.left > MemberExpressio…¶
Parameters:
contextany
Returns: { 'BinaryExpression > CallExpression.left > MemberExpression'(node: TSESTree.MemberExpression): void; 'BinaryExpression > ChainExpression.left > CallExpression > MemberExpression'(node: TSESTree.MemberExpression): void; 'CallExpression[arguments.length=1] > MemberExpression.callee[property.name="test"][computed=false]'(node: { parent: TSESTree.CallExpression; } & TSESTree.MemberExpression): void; }
Calls:
context.sourceCode.getScopegetParserServices (from ../util)services.program.getTypeCheckergetStaticValue (from ../util)isNumberts.isFunctionLikeparamA.getTextparamB.getTextparseRegExpLiteral (from @eslint-community/regexpp)chars.everyString.fromCodePointchars.mapObject.values(EscapeMap).joinstr.replaceAllisStaticMemberAccessOfValue (from ../util)isNegativeCheckisPositiveCheckservices .getSymbolAtLocation(node.property) ?.getDeclarationschecker.getTypeAtLocationtype .getProperty('includes') ?.getDeclarationsincludesMethodDecl?.somehasSameParameterscontext.reportfixer.insertTextBeforefixer.replaceTextfixer.removeRangecheckArrayIndexOfparseRegExpgetConstrainedTypeAtLocation (from ../util)fixer.insertTextAfterescapeString
Internal Comments:
// Check name, type, and question token once.
/**
* Parse a given node if it's a `RegExp` instance.
* @param node The node to parse.
*/
// Check if it can determine a unique string. (x2)
// To string.
// Check if the comparison is equivalent to `includes()`. (x2)
// Get the symbol of `indexOf` method. (x2)
// Check if every declaration of `indexOf` method has `includes` method
// and the two methods have the same parameters.
// Report it. (x4)
// a.indexOf(b) !== 1 (x2)
// a?.indexOf(b) !== 1 (x2)
// /bar/.test(foo) (x2)
//check the argument type of test methods (x2)
Code
create(context) {
const globalScope = context.sourceCode.getScope(context.sourceCode.ast);
const services = getParserServices(context);
const checker = services.program.getTypeChecker();
function isNumber(node: TSESTree.Node, value: number): boolean {
const evaluated = getStaticValue(node, globalScope);
return evaluated?.value === value;
}
function isPositiveCheck(node: TSESTree.BinaryExpression): boolean {
switch (node.operator) {
case '!==':
case '!=':
case '>':
return isNumber(node.right, -1);
case '>=':
return isNumber(node.right, 0);
default:
return false;
}
}
function isNegativeCheck(node: TSESTree.BinaryExpression): boolean {
switch (node.operator) {
case '===':
case '==':
case '<=':
return isNumber(node.right, -1);
case '<':
return isNumber(node.right, 0);
default:
return false;
}
}
function hasSameParameters(
nodeA: ts.Declaration,
nodeB: ts.Declaration,
): boolean {
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: TSESTree.Node): string | null {
const evaluated = getStaticValue(node, globalScope);
if (evaluated == null || !(evaluated.value instanceof RegExp)) {
return null;
}
const { flags, pattern } = 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: string): string {
const EscapeMap = {
'\0': '\\0',
'\t': '\\t',
'\n': '\\n',
'\v': '\\v',
'\f': '\\f',
'\r': '\\r',
"'": "\\'",
'\\': '\\\\',
// "\b" cause unexpected replacements
// '\b': '\\b',
};
const replaceRegex = new RegExp(Object.values(EscapeMap).join('|'), 'g');
return str.replaceAll(
replaceRegex,
char => EscapeMap[char as keyof typeof EscapeMap],
);
}
function checkArrayIndexOf(
node: TSESTree.MemberExpression,
allowFixing: boolean,
): void {
if (!isStaticMemberAccessOfValue(node, context, 'indexOf')) {
return;
}
// Check if the comparison is equivalent to `includes()`.
const callNode = node.parent as TSESTree.CallExpression;
const compareNode = (
callNode.parent.type === AST_NODE_TYPES.ChainExpression
? callNode.parent.parent
: callNode.parent
) as TSESTree.BinaryExpression;
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): Generator<TSESLint.RuleFix> {
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'(
node: TSESTree.MemberExpression,
): void {
checkArrayIndexOf(node, /* allowFixing */ true);
},
// a?.indexOf(b) !== 1
'BinaryExpression > ChainExpression.left > CallExpression > MemberExpression'(
node: TSESTree.MemberExpression,
): void {
checkArrayIndexOf(node, /* allowFixing */ false);
},
// /bar/.test(foo)
'CallExpression[arguments.length=1] > MemberExpression.callee[property.name="test"][computed=false]'(
node: { parent: TSESTree.CallExpression } & TSESTree.MemberExpression,
): void {
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 = 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 !== AST_NODE_TYPES.Literal &&
argNode.type !== AST_NODE_TYPES.TemplateLiteral &&
argNode.type !== AST_NODE_TYPES.Identifier &&
argNode.type !== AST_NODE_TYPES.MemberExpression &&
argNode.type !== 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)}')`,
);
},
});
},
};
}
Internal helpers¶
Declared inside another function in this file.
isNumber(node: TSESTree.Node, value: number): boolean¶
Parameters:
nodeTSESTree.Nodevaluenumber
Returns: boolean
Calls:
getStaticValue (from ../util)
Code
isPositiveCheck(node: TSESTree.BinaryExpression): boolean¶
Parameters:
nodeTSESTree.BinaryExpression
Returns: boolean
Calls:
isNumber
Code
isNegativeCheck(node: TSESTree.BinaryExpression): boolean¶
Parameters:
nodeTSESTree.BinaryExpression
Returns: boolean
Calls:
isNumber
Code
hasSameParameters(nodeA: ts.Declaration, nodeB: ts.Declaration): boolean¶
Parameters:
nodeAts.DeclarationnodeBts.Declaration
Returns: boolean
Calls:
ts.isFunctionLikeparamA.getTextparamB.getText
Internal Comments:
Code
function hasSameParameters(
nodeA: ts.Declaration,
nodeB: ts.Declaration,
): boolean {
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;
}
parseRegExp(node: TSESTree.Node): string | null¶
Parse a given node if it's a RegExp instance.
Parameters:
nodeany: The node to parse.
Calls:
getStaticValue (from ../util)parseRegExpLiteral (from @eslint-community/regexpp)chars.everyString.fromCodePointchars.map
Internal Comments:
Code
function parseRegExp(node: TSESTree.Node): string | null {
const evaluated = getStaticValue(node, globalScope);
if (evaluated == null || !(evaluated.value instanceof RegExp)) {
return null;
}
const { flags, pattern } = 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));
}
escapeString(str: string): string¶
Parameters:
strstring
Returns: string
Calls:
Object.values(EscapeMap).joinstr.replaceAll
Code
function escapeString(str: string): string {
const EscapeMap = {
'\0': '\\0',
'\t': '\\t',
'\n': '\\n',
'\v': '\\v',
'\f': '\\f',
'\r': '\\r',
"'": "\\'",
'\\': '\\\\',
// "\b" cause unexpected replacements
// '\b': '\\b',
};
const replaceRegex = new RegExp(Object.values(EscapeMap).join('|'), 'g');
return str.replaceAll(
replaceRegex,
char => EscapeMap[char as keyof typeof EscapeMap],
);
}
checkArrayIndexOf(node: TSESTree.MemberExpression, allowFixing: boolean): void¶
Parameters:
nodeTSESTree.MemberExpressionallowFixingboolean
Returns: void
Calls:
isStaticMemberAccessOfValue (from ../util)isNegativeCheckisPositiveCheckservices .getSymbolAtLocation(node.property) ?.getDeclarationschecker.getTypeAtLocationtype .getProperty('includes') ?.getDeclarationsincludesMethodDecl?.somehasSameParameterscontext.reportfixer.insertTextBeforefixer.replaceTextfixer.removeRange
Internal Comments:
// Check if the comparison is equivalent to `includes()`. (x2)
// Get the symbol of `indexOf` method. (x2)
// Check if every declaration of `indexOf` method has `includes` method
// and the two methods have the same parameters.
// Report it. (x4)
Code
function checkArrayIndexOf(
node: TSESTree.MemberExpression,
allowFixing: boolean,
): void {
if (!isStaticMemberAccessOfValue(node, context, 'indexOf')) {
return;
}
// Check if the comparison is equivalent to `includes()`.
const callNode = node.parent as TSESTree.CallExpression;
const compareNode = (
callNode.parent.type === AST_NODE_TYPES.ChainExpression
? callNode.parent.parent
: callNode.parent
) as TSESTree.BinaryExpression;
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): Generator<TSESLint.RuleFix> {
if (negative) {
yield fixer.insertTextBefore(callNode, '!');
}
yield fixer.replaceText(node.property, 'includes');
yield fixer.removeRange([callNode.range[1], compareNode.range[1]]);
},
}),
});
}
Generated by Syntax Scribe