diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index b9a1ebb9a4bf..a08097906968 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -70,6 +70,30 @@ function toStaticValue( return undefined; } +function typeContainsAnyOrUnknown(type: ts.Type): boolean { + return tsutils + .unionConstituents(type) + .some(part => isTypeAnyType(part) || isTypeUnknownType(part)); +} + +function isArrayIsArrayCall(node: TSESTree.CallExpression): boolean { + if (node.callee.type !== AST_NODE_TYPES.MemberExpression) { + return false; + } + + const memberExpr = node.callee; + if (memberExpr.computed || memberExpr.optional) { + return false; + } + + return ( + memberExpr.object.type === AST_NODE_TYPES.Identifier && + memberExpr.object.name === 'Array' && + memberExpr.property.type === AST_NODE_TYPES.Identifier && + memberExpr.property.name === 'isArray' + ); +} + const BOOL_OPERATORS = new Set([ '<', '>', @@ -609,6 +633,21 @@ export default createRule({ : 'type guard', }, }); + } else if ( + isArrayIsArrayCall(node) && + !typeContainsAnyOrUnknown(typeOfArgument) && + checker.isTypeAssignableTo( + typeOfArgument, + typeGuardAssertedArgument.type, + ) + ) { + context.report({ + node: typeGuardAssertedArgument.argument, + messageId: 'typeGuardAlreadyIsType', + data: { + typeGuardOrAssertionFunction: 'type guard', + }, + }); } } } diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts index 22bb2f46c152..65f6b6d87810 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -1138,6 +1138,71 @@ isString('falafel'); `, options: [{ checkTypePredicates: true }], }, + { + code: ` +declare const items: number[] | null; +if (Array.isArray(items)) { + console.log(items.length); +} + `, + options: [{ checkTypePredicates: true }], + }, + { + code: ` +declare const items: number[] | string; +if (Array.isArray(items)) { + console.log(items.length); +} + `, + options: [{ checkTypePredicates: true }], + }, + { + code: ` +declare const items: unknown; +if (Array.isArray(items)) { + console.log(items.length); +} + `, + options: [{ checkTypePredicates: true }], + }, + { + code: ` +declare const items: any; +if (Array.isArray(items)) { + console.log(items.length); +} + `, + options: [{ checkTypePredicates: true }], + }, + { + code: ` +declare const MaybeArray: typeof Array | undefined; +declare const items: number[]; +if (MaybeArray?.isArray(items)) { + console.log(items.length); +} + `, + options: [{ checkTypePredicates: true }], + }, + { + code: ` +declare const items: number[]; +if (Array['isArray'](items)) { + console.log(items.length); +} + `, + options: [{ checkTypePredicates: true }], + }, + { + code: ` +function process(value: T) { + if (Array.isArray(value)) { + console.log(value.length); + } +} + `, + options: [{ checkTypePredicates: true }], + }, ` type A = { [name in Lowercase]?: A }; declare const a: A; @@ -3518,6 +3583,64 @@ isString('fa' + 'lafel'); ], options: [{ checkTypePredicates: true }], }, + { + code: ` +declare const items: number[]; +if (Array.isArray(items)) { + console.log(items.length); +} + `, + errors: [ + { + line: 3, + messageId: 'typeGuardAlreadyIsType', + }, + ], + options: [{ checkTypePredicates: true }], + }, + { + code: ` +declare const items: string[]; +Array.isArray(items); + `, + errors: [ + { + line: 3, + messageId: 'typeGuardAlreadyIsType', + }, + ], + options: [{ checkTypePredicates: true }], + }, + { + code: ` +const tuple: [string, number] = ['a', 1]; +if (Array.isArray(tuple)) { + console.log(tuple[0]); +} + `, + errors: [ + { + line: 3, + messageId: 'typeGuardAlreadyIsType', + }, + ], + options: [{ checkTypePredicates: true }], + }, + { + code: ` +declare const items: string[] | number[]; +if (Array.isArray(items)) { + console.log(items.length); +} + `, + errors: [ + { + line: 3, + messageId: 'typeGuardAlreadyIsType', + }, + ], + options: [{ checkTypePredicates: true }], + }, // "branded" types unnecessaryConditionTest('"" & {}', 'alwaysFalsy'), diff --git a/packages/rule-tester/src/utils/flat-config-schema.ts b/packages/rule-tester/src/utils/flat-config-schema.ts index 1b631070a3e0..9e6e9342d94e 100644 --- a/packages/rule-tester/src/utils/flat-config-schema.ts +++ b/packages/rule-tester/src/utils/flat-config-schema.ts @@ -423,7 +423,7 @@ const processorSchema: ObjectPropertySchema = { }, }; -type ConfigRules = Record; +type ConfigRules = Record; const rulesSchema = { merge(first: ConfigRules = {}, second: ConfigRules = {}): ConfigRules {