Skip to content

feat(no-in-array): Disallow using in operator for arrays #5851

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 12 commits into from
Closed
43 changes: 43 additions & 0 deletions packages/eslint-plugin/docs/rules/call-super-on-override.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
description: 'Require overridden methods to call super.method in their body.'
---

> 🛑 This file is source code, not the primary documentation location! 🛑
>
> See **https://typescript-eslint.io/rules/call-super-on-override** for documentation.

This rule enforces that overridden methods are calling exact super method to avoid missing super class method implementations.

## Rule Details

Examples of code for this rule:

### ❌ Incorrect

```ts
class Foo1 {
bar(param: any): void {}
}

class Foo2 extends Foo1 {
override bar(param: any): void {}
}
```

### ✅ Correct

```ts
class Foo1 {
bar(param: any): void {}
}

class Foo2 extends Foo1 {
override bar(param: any): void {
super.bar(param);
}
}
```

## When Not To Use It

When you are using TypeScript < 4.3 or you did not set `noImplicitOverride: true` in `CompilerOptions`
37 changes: 37 additions & 0 deletions packages/eslint-plugin/docs/rules/no-in-array.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
description: 'Disallow using in operator for arrays.'
---

> 🛑 This file is source code, not the primary documentation location! 🛑
>
> See **https://typescript-eslint.io/rules/no-in-array** for documentation.

This rule bans using `in` operator for checking array members existence.

## Rule Details

Examples of code for this rule:

### ❌ Incorrect

```ts
const arr = ['a', 'b', 'c'];

if ('c' in arr) {
// ...
}
```

### ✅ Correct

```ts
const arr = ['a', 'b', 'c'];

if (arr.includes('a')) {
// ...
}
```

## When Not To Use It

When you want exactly iterate over array indexes.
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/configs/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export = {
'@typescript-eslint/ban-types': 'error',
'brace-style': 'off',
'@typescript-eslint/brace-style': 'error',
'@typescript-eslint/call-super-on-override': 'error',
'@typescript-eslint/class-literal-property-style': 'error',
'comma-dangle': 'off',
'@typescript-eslint/comma-dangle': 'error',
Expand Down Expand Up @@ -69,6 +70,7 @@ export = {
'@typescript-eslint/no-for-in-array': 'error',
'no-implied-eval': 'off',
'@typescript-eslint/no-implied-eval': 'error',
'@typescript-eslint/no-in-array': 'error',
'@typescript-eslint/no-inferrable-types': 'error',
'no-invalid-this': 'off',
'@typescript-eslint/no-invalid-this': 'error',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export = {
'@typescript-eslint/no-for-in-array': 'error',
'no-implied-eval': 'off',
'@typescript-eslint/no-implied-eval': 'error',
'@typescript-eslint/no-in-array': 'error',
'@typescript-eslint/no-misused-promises': 'error',
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
'@typescript-eslint/no-unsafe-argument': 'error',
Expand Down
119 changes: 119 additions & 0 deletions packages/eslint-plugin/src/rules/call-super-on-override.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import type { TSESTree } from '@typescript-eslint/utils';
import { AST_NODE_TYPES } from '@typescript-eslint/utils';

import * as utils from '../util';

type MessageIds = 'missingSuperMethodCall';

export default utils.createRule<[], MessageIds>({
name: 'call-super-on-override',
meta: {
type: 'suggestion',
docs: {
description:
'Require overridden methods to call super.method in their body',
recommended: false,
requiresTypeChecking: false,
},
messages: {
missingSuperMethodCall:
"Use 'super{{property}}{{parameterTuple}}' to avoid missing super class method implementations",
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
topLevel: {
type: 'boolean',
},
},
additionalProperties: false,
},
],
},
defaultOptions: [],
create(context) {
return {
'MethodDefinition[override=true][kind="method"]'(
node: TSESTree.MethodDefinition,
): void {
let methodName = '',
methodNameIsLiteral = false,
methodNameIsNull = false; // don't add quotes for error message on [null] case

if (node.key.type === AST_NODE_TYPES.Identifier) {
methodName = node.key.name;
} else {
methodNameIsLiteral = true;
// null & undefined can be used as property names, undefined counted as Identifier & null as Literal
methodName =
(node.key as TSESTree.Literal).value?.toString() ?? 'null';
methodNameIsNull = (node.key as TSESTree.Literal).value == null;
}

const { computed: isComputed } = node,
bodyStatements = node.value.body!.body;

// Search for super method call
for (const statement of bodyStatements) {
if (
isSuperMethodCall(
statement,
methodName,
!methodNameIsLiteral && isComputed,
)
) {
return; // We are done here, no missingSuperMethodCall error
}
}

// Raise if not found
context.report({
messageId: 'missingSuperMethodCall',
node: node,
data: {
property: isComputed
? `[${
methodNameIsLiteral && !methodNameIsNull
? `'${methodName}'`
: methodName
}]`
: `.${methodName}`,
parameterTuple: `(${node.value.params
.map(p => (p as TSESTree.Identifier).name)
.join(', ')})`,
},
});
},
};
},
});

const isSuperMethodCall = (
statement: TSESTree.Statement | undefined,
methodName: string,
methodIsComputedIdentifier: boolean,
): boolean => {
// for edge cases like this -> override [X]() { super.X() }
// we make sure that computed identifier should have computed callback
let calleeIsComputedIdentifier = false;

const calleeName =
statement?.type === AST_NODE_TYPES.ExpressionStatement &&
statement.expression.type === AST_NODE_TYPES.CallExpression &&
statement.expression.callee.type === AST_NODE_TYPES.MemberExpression &&
statement.expression.callee.object.type === AST_NODE_TYPES.Super &&
(statement.expression.callee.property.type === AST_NODE_TYPES.Identifier
? ((calleeIsComputedIdentifier = statement.expression.callee.computed),
statement.expression.callee.property.name)
: statement.expression.callee.property.type === AST_NODE_TYPES.Literal
? statement.expression.callee.property.value?.toString() ?? 'null'
: undefined);

return methodIsComputedIdentifier
? calleeIsComputedIdentifier
? methodName === calleeName
: false
: methodName === calleeName;
};
4 changes: 4 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import banTsComment from './ban-ts-comment';
import banTslintComment from './ban-tslint-comment';
import banTypes from './ban-types';
import braceStyle from './brace-style';
import callSuperOnOverride from './call-super-on-override';
import classLiteralPropertyStyle from './class-literal-property-style';
import commaDangle from './comma-dangle';
import commaSpacing from './comma-spacing';
Expand Down Expand Up @@ -47,6 +48,7 @@ import noFloatingPromises from './no-floating-promises';
import noForInArray from './no-for-in-array';
import noImplicitAnyCatch from './no-implicit-any-catch';
import noImpliedEval from './no-implied-eval';
import noInArray from './no-in-array';
import noInferrableTypes from './no-inferrable-types';
import noInvalidThis from './no-invalid-this';
import noInvalidVoidType from './no-invalid-void-type';
Expand Down Expand Up @@ -135,6 +137,7 @@ export default {
'ban-tslint-comment': banTslintComment,
'ban-types': banTypes,
'brace-style': braceStyle,
'call-super-on-override': callSuperOnOverride,
'class-literal-property-style': classLiteralPropertyStyle,
'comma-dangle': commaDangle,
'comma-spacing': commaSpacing,
Expand Down Expand Up @@ -177,6 +180,7 @@ export default {
'no-for-in-array': noForInArray,
'no-implicit-any-catch': noImplicitAnyCatch,
'no-implied-eval': noImpliedEval,
'no-in-array': noInArray,
'no-inferrable-types': noInferrableTypes,
'no-invalid-this': noInvalidThis,
'no-invalid-void-type': noInvalidVoidType,
Expand Down
46 changes: 46 additions & 0 deletions packages/eslint-plugin/src/rules/no-in-array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { TSESTree } from '@typescript-eslint/utils';
import * as ts from 'typescript';

import * as util from '../util';

export default util.createRule<[], 'inArrayViolation'>({
name: 'no-in-array',
meta: {
docs: {
description: 'Disallow using in operator for arrays',
recommended: 'error',
requiresTypeChecking: true,
},
messages: {
inArrayViolation:
"'in' operator for arrays is forbidden. Use array.indexOf or array.includes instead.",
},
schema: [],
type: 'problem',
},
defaultOptions: [],
create(context) {
return {
"BinaryExpression[operator='in']"(node: TSESTree.BinaryExpression): void {
const parserServices = util.getParserServices(context);
const checker = parserServices.program.getTypeChecker();
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);

const type = util.getConstrainedTypeAtLocation(
checker,
originalNode.right,
);

if (
util.isTypeArrayTypeOrUnionOfArrayTypes(type, checker) ||
(type.flags & ts.TypeFlags.StringLike) !== 0
) {
context.report({
node,
messageId: 'inArrayViolation',
});
}
},
};
},
});
Loading