diff --git a/packages/eslint-plugin/src/rules/await-thenable.ts b/packages/eslint-plugin/src/rules/await-thenable.ts index 6fb49227470a..5f63a8dc0f31 100644 --- a/packages/eslint-plugin/src/rules/await-thenable.ts +++ b/packages/eslint-plugin/src/rules/await-thenable.ts @@ -51,10 +51,17 @@ export default createRule<[], MessageId>({ return { AwaitExpression(node): void { - const type = services.getTypeAtLocation(node.argument); - - const originalNode = services.esTreeNodeToTSNodeMap.get(node); - const certainty = needsToBeAwaited(checker, originalNode, type); + const awaitArgumentEsNode = node.argument; + const awaitArgumentType = + services.getTypeAtLocation(awaitArgumentEsNode); + const awaitArgumentTsNode = + services.esTreeNodeToTSNodeMap.get(awaitArgumentEsNode); + + const certainty = needsToBeAwaited( + checker, + awaitArgumentTsNode, + awaitArgumentType, + ); if (certainty === Awaitable.Never) { context.report({ diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-boolean-literal-compare.ts b/packages/eslint-plugin/src/rules/no-unnecessary-boolean-literal-compare.ts index 069055d67add..fa8a49a03f85 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-boolean-literal-compare.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-boolean-literal-compare.ts @@ -6,7 +6,7 @@ import * as ts from 'typescript'; import { createRule, - getConstrainedTypeAtLocation, + getConstraintInfo, getParserServices, isStrongPrecedenceNode, } from '../util'; @@ -85,6 +85,7 @@ export default createRule({ ], create(context, [options]) { const services = getParserServices(context); + const checker = services.program.getTypeChecker(); function getBooleanComparison( node: TSESTree.BinaryExpression, @@ -94,19 +95,23 @@ export default createRule({ return undefined; } - const expressionType = getConstrainedTypeAtLocation( - services, - comparison.expression, + const { constraintType, isTypeParameter } = getConstraintInfo( + checker, + services.getTypeAtLocation(comparison.expression), ); - if (isBooleanType(expressionType)) { + if (isTypeParameter && constraintType == null) { + return undefined; + } + + if (isBooleanType(constraintType)) { return { ...comparison, expressionIsNullableBoolean: false, }; } - if (isNullableBoolean(expressionType)) { + if (isNullableBoolean(constraintType)) { return { ...comparison, expressionIsNullableBoolean: true, diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index 1d821efbb6f3..08249fe19e19 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -7,6 +7,7 @@ import * as ts from 'typescript'; import { createRule, getConstrainedTypeAtLocation, + getConstraintInfo, getParserServices, getTypeName, getTypeOfPropertyOfName, @@ -653,16 +654,7 @@ export default createRule({ .getCallSignaturesOfType( getConstrainedTypeAtLocation(services, callback), ) - .map(sig => sig.getReturnType()) - .map(t => { - // TODO: use `getConstraintTypeInfoAtLocation` once it's merged - // https://github.com/typescript-eslint/typescript-eslint/pull/10496 - if (tsutils.isTypeParameter(t)) { - return checker.getBaseConstraintOfType(t); - } - - return t; - }); + .map(sig => sig.getReturnType()); if (returnTypes.length === 0) { // Not a callable function, e.g. `any` @@ -673,16 +665,21 @@ export default createRule({ let hasTruthyReturnTypes = false; for (const type of returnTypes) { + const { constraintType } = getConstraintInfo(checker, type); // Predicate is always necessary if it involves `any` or `unknown` - if (!type || isTypeAnyType(type) || isTypeUnknownType(type)) { + if ( + !constraintType || + isTypeAnyType(constraintType) || + isTypeUnknownType(constraintType) + ) { return; } - if (isPossiblyFalsy(type)) { + if (isPossiblyFalsy(constraintType)) { hasFalsyReturnTypes = true; } - if (isPossiblyTruthy(type)) { + if (isPossiblyTruthy(constraintType)) { hasTruthyReturnTypes = true; } diff --git a/packages/eslint-plugin/src/util/getConstraintInfo.ts b/packages/eslint-plugin/src/util/getConstraintInfo.ts new file mode 100644 index 000000000000..80eaeb72e24c --- /dev/null +++ b/packages/eslint-plugin/src/util/getConstraintInfo.ts @@ -0,0 +1,60 @@ +import type * as ts from 'typescript'; + +import * as tsutils from 'ts-api-utils'; + +export interface ConstraintTypeInfoUnconstrained { + constraintType: undefined; + isTypeParameter: true; +} + +export interface ConstraintTypeInfoConstrained { + constraintType: ts.Type; + isTypeParameter: true; +} + +export interface ConstraintTypeInfoNonGeneric { + constraintType: ts.Type; + isTypeParameter: false; +} + +export type ConstraintTypeInfo = + | ConstraintTypeInfoConstrained + | ConstraintTypeInfoNonGeneric + | ConstraintTypeInfoUnconstrained; + +/** + * Returns whether the type is a generic and what its constraint is. + * + * If the type is not a generic, `isTypeParameter` will be `false`, and + * `constraintType` will be the same as the input type. + * + * If the type is a generic, and it is constrained, `isTypeParameter` will be + * `true`, and `constraintType` will be the constraint type. + * + * If the type is a generic, but it is not constrained, `constraintType` will be + * `undefined` (rather than an `unknown` type), due to https://github.com/microsoft/TypeScript/issues/60475 + * + * Successor to {@link getConstrainedTypeAtLocation} due to https://github.com/typescript-eslint/typescript-eslint/issues/10438 + * + * This is considered internal since it is unstable for now and may have breaking changes at any time. + * Use at your own risk. + * + * @internal + * + */ +export function getConstraintInfo( + checker: ts.TypeChecker, + type: ts.Type, +): ConstraintTypeInfo { + if (tsutils.isTypeParameter(type)) { + const constraintType = checker.getBaseConstraintOfType(type); + return { + constraintType, + isTypeParameter: true, + }; + } + return { + constraintType: type, + isTypeParameter: false, + }; +} diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index 3aa935a51a95..934956e91ad0 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -23,9 +23,11 @@ export * from './objectIterators'; export * from './needsToBeAwaited'; export * from './scopeUtils'; export * from './types'; +export * from './getConstraintInfo'; // this is done for convenience - saves migrating all of the old rules export * from '@typescript-eslint/type-utils'; + const { applyDefault, deepMerge, diff --git a/packages/eslint-plugin/src/util/needsToBeAwaited.ts b/packages/eslint-plugin/src/util/needsToBeAwaited.ts index e6d675cc321e..c9affe4d797e 100644 --- a/packages/eslint-plugin/src/util/needsToBeAwaited.ts +++ b/packages/eslint-plugin/src/util/needsToBeAwaited.ts @@ -6,6 +6,8 @@ import { } from '@typescript-eslint/type-utils'; import * as tsutils from 'ts-api-utils'; +import { getConstraintInfo } from './getConstraintInfo'; + export enum Awaitable { Always, Never, @@ -17,24 +19,20 @@ export function needsToBeAwaited( node: ts.Node, type: ts.Type, ): Awaitable { - // can't use `getConstrainedTypeAtLocation` directly since it's bugged for - // unconstrained generics. - const constrainedType = !tsutils.isTypeParameter(type) - ? type - : checker.getBaseConstraintOfType(type); + const { constraintType, isTypeParameter } = getConstraintInfo(checker, type); // unconstrained generic types should be treated as unknown - if (constrainedType == null) { + if (isTypeParameter && constraintType == null) { return Awaitable.May; } // `any` and `unknown` types may need to be awaited - if (isTypeAnyType(constrainedType) || isTypeUnknownType(constrainedType)) { + if (isTypeAnyType(constraintType) || isTypeUnknownType(constraintType)) { return Awaitable.May; } // 'thenable' values should always be be awaited - if (tsutils.isThenableType(checker, node, constrainedType)) { + if (tsutils.isThenableType(checker, node, constraintType)) { return Awaitable.Always; } diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-boolean-literal-compare.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-boolean-literal-compare.test.ts index f99a78ff9df6..77d395aeb4d1 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-boolean-literal-compare.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-boolean-literal-compare.test.ts @@ -109,6 +109,20 @@ ruleTester.run('no-unnecessary-boolean-literal-compare', rule, { }, "'false' === true;", "'true' === false;", + ` +const unconstrained: (someCondition: T) => void = someCondition => { + if (someCondition === true) { + } +}; + `, + ` +const extendsUnknown: ( + someCondition: T, +) => void = someCondition => { + if (someCondition === true) { + } +}; + `, ], invalid: [ diff --git a/packages/eslint-plugin/tests/util/getConstraintInfo.test.ts b/packages/eslint-plugin/tests/util/getConstraintInfo.test.ts new file mode 100644 index 000000000000..4e92270db32e --- /dev/null +++ b/packages/eslint-plugin/tests/util/getConstraintInfo.test.ts @@ -0,0 +1,196 @@ +import type { + TSESTree, + ParserServicesWithTypeInformation, +} from '@typescript-eslint/utils'; + +import { parseForESLint } from '@typescript-eslint/parser'; +import path from 'node:path'; +import * as tsutils from 'ts-api-utils'; + +import { getConstraintInfo } from '../../src/util/getConstraintInfo'; + +function parseCodeForEslint(code: string): ReturnType & { + services: ParserServicesWithTypeInformation; +} { + const fixturesDir = path.join(__dirname, '../fixtures/'); + + // @ts-expect-error -- services will have type information. + return parseForESLint(code, { + disallowAutomaticSingleRunInference: true, + filePath: path.join(fixturesDir, 'file.ts'), + project: './tsconfig.json', + tsconfigRootDir: fixturesDir, + }); +} + +describe('getConstraintInfo', () => { + it('returns undefined for unconstrained generic', () => { + const sourceCode = ` +function foo(x: T); + `; + + const { ast, services } = parseCodeForEslint(sourceCode); + const checker = services.program.getTypeChecker(); + + const functionNode = ast.body[0] as TSESTree.FunctionDeclaration; + const parameterNode = functionNode.params[0]; + const parameterType = services.getTypeAtLocation(parameterNode); + + const { constraintType, isTypeParameter } = getConstraintInfo( + checker, + parameterType, + ); + + expect(isTypeParameter).toBe(true); + // ideally one day we'll be able to change this to assert that it be the intrinsic unknown type. + // Requires https://github.com/microsoft/TypeScript/issues/60475 + expect(constraintType).toBeUndefined(); + }); + + it('returns unknown for extends unknown', () => { + const sourceCode = ` +function foo(x: T); + `; + + const { ast, services } = parseCodeForEslint(sourceCode); + const checker = services.program.getTypeChecker(); + + const functionNode = ast.body[0] as TSESTree.FunctionDeclaration; + const parameterNode = functionNode.params[0]; + const parameterType = services.getTypeAtLocation(parameterNode); + + const { constraintType, isTypeParameter } = getConstraintInfo( + checker, + parameterType, + ); + + expect(isTypeParameter).toBe(true); + expect(constraintType).toBeDefined(); + expect(tsutils.isTypeParameter(constraintType!)).toBe(false); + expect(tsutils.isIntrinsicUnknownType(constraintType!)).toBe(true); + }); + + it('returns unknown for extends any', () => { + const sourceCode = ` +function foo(x: T); + `; + + const { ast, services } = parseCodeForEslint(sourceCode); + const checker = services.program.getTypeChecker(); + + const functionNode = ast.body[0] as TSESTree.FunctionDeclaration; + const parameterNode = functionNode.params[0]; + const parameterType = services.getTypeAtLocation(parameterNode); + + const { constraintType, isTypeParameter } = getConstraintInfo( + checker, + parameterType, + ); + + expect(isTypeParameter).toBe(true); + expect(constraintType).toBeDefined(); + expect(tsutils.isTypeParameter(constraintType!)).toBe(false); + expect(tsutils.isIntrinsicUnknownType(constraintType!)).toBe(true); + }); + + it('returns string for extends string', () => { + const sourceCode = ` +function foo(x: T); + `; + + const { ast, services } = parseCodeForEslint(sourceCode); + const checker = services.program.getTypeChecker(); + + const functionNode = ast.body[0] as TSESTree.FunctionDeclaration; + const parameterNode = functionNode.params[0]; + const parameterType = services.getTypeAtLocation(parameterNode); + + const { constraintType, isTypeParameter } = getConstraintInfo( + checker, + parameterType, + ); + + expect(isTypeParameter).toBe(true); + expect(constraintType).toBeDefined(); + expect(tsutils.isTypeParameter(constraintType!)).toBe(false); + expect(tsutils.isIntrinsicStringType(constraintType!)).toBe(true); + }); + + it('returns string for non-generic string', () => { + const sourceCode = ` +function foo(x: string); + `; + + const { ast, services } = parseCodeForEslint(sourceCode); + const checker = services.program.getTypeChecker(); + + const functionNode = ast.body[0] as TSESTree.FunctionDeclaration; + const parameterNode = functionNode.params[0]; + const parameterType = services.getTypeAtLocation(parameterNode); + + const { constraintType, isTypeParameter } = getConstraintInfo( + checker, + parameterType, + ); + + expect(isTypeParameter).toBe(false); + expect(constraintType).toBeDefined(); + expect(tsutils.isTypeParameter(constraintType!)).toBe(false); + expect(tsutils.isIntrinsicStringType(constraintType!)).toBe(true); + expect(constraintType).toBe(parameterType); + }); + + it('handles type parameter whose constraint is a constrained type parameter', () => { + const sourceCode = ` +function foo() { + function bar(x: V) { + } +} + `; + + const { ast, services } = parseCodeForEslint(sourceCode); + const checker = services.program.getTypeChecker(); + + const outerFunctionNode = ast.body[0] as TSESTree.FunctionDeclaration; + const innerFunctionNode = outerFunctionNode.body + .body[0] as TSESTree.FunctionDeclaration; + const parameterNode = innerFunctionNode.params[0]; + const parameterType = services.getTypeAtLocation(parameterNode); + + const { constraintType, isTypeParameter } = getConstraintInfo( + checker, + parameterType, + ); + + expect(constraintType).toBeDefined(); + expect(tsutils.isTypeParameter(constraintType!)).toBe(false); + expect(tsutils.isIntrinsicStringType(constraintType!)).toBe(true); + expect(isTypeParameter).toBe(true); + }); + + it('handles type parameter whose constraint is an unconstrained type parameter', () => { + const sourceCode = ` +function foo() { + function bar(x: V) { + } +} + `; + + const { ast, services } = parseCodeForEslint(sourceCode); + const checker = services.program.getTypeChecker(); + + const outerFunctionNode = ast.body[0] as TSESTree.FunctionDeclaration; + const innerFunctionNode = outerFunctionNode.body + .body[0] as TSESTree.FunctionDeclaration; + const parameterNode = innerFunctionNode.params[0]; + const parameterType = services.getTypeAtLocation(parameterNode); + + const { constraintType, isTypeParameter } = getConstraintInfo( + checker, + parameterType, + ); + + expect(isTypeParameter).toBe(true); + expect(constraintType).toBeUndefined(); + }); +}); diff --git a/packages/type-utils/src/getConstrainedTypeAtLocation.ts b/packages/type-utils/src/getConstrainedTypeAtLocation.ts index cbd332f98da3..d31df29ab5b1 100644 --- a/packages/type-utils/src/getConstrainedTypeAtLocation.ts +++ b/packages/type-utils/src/getConstrainedTypeAtLocation.ts @@ -5,7 +5,13 @@ import type { import type * as ts from 'typescript'; /** - * Resolves the given node's type. Will resolve to the type's generic constraint, if it has one. + * Resolves the given node's type. Will return the type's generic constraint, if it has one. + * + * Warning - if the type is generic and does _not_ have a constraint, the type will be + * returned as-is, rather than returning an `unknown` type. This can be checked + * for by checking for the type flag ts.TypeFlags.TypeParameter. + * + * @see https://github.com/typescript-eslint/typescript-eslint/issues/10438 */ export function getConstrainedTypeAtLocation( services: ParserServicesWithTypeInformation, diff --git a/packages/type-utils/tests/getConstrainedTypeAtLocation.test.ts b/packages/type-utils/tests/getConstrainedTypeAtLocation.test.ts index 592c45b1520b..598e78500afe 100644 --- a/packages/type-utils/tests/getConstrainedTypeAtLocation.test.ts +++ b/packages/type-utils/tests/getConstrainedTypeAtLocation.test.ts @@ -1,57 +1,146 @@ import type { TSESTree } from '@typescript-eslint/types'; import type { ParserServicesWithTypeInformation } from '@typescript-eslint/typescript-estree'; -import type * as ts from 'typescript'; - -import { getConstrainedTypeAtLocation } from '../src'; - -const node = {} as TSESTree.Node; - -const mockType = (): ts.Type => { - return {} as ts.Type; -}; - -const mockServices = ({ - baseConstraintOfType, - typeAtLocation, -}: { - baseConstraintOfType?: ts.Type; - typeAtLocation: ts.Type; -}): ParserServicesWithTypeInformation => { - const typeChecker = { - getBaseConstraintOfType: (_: ts.Type) => baseConstraintOfType, - } as ts.TypeChecker; - const program = { - getTypeChecker: () => typeChecker, - } as ts.Program; - - return { - getTypeAtLocation: (_: TSESTree.Node) => typeAtLocation, - program, - } as ParserServicesWithTypeInformation; -}; + +import { parseForESLint } from '@typescript-eslint/parser'; +import path from 'node:path'; +import * as tsutils from 'ts-api-utils'; + +import { getConstrainedTypeAtLocation, isTypeUnknownType } from '../src'; + +function parseCodeForEslint(code: string): ReturnType & { + services: ParserServicesWithTypeInformation; +} { + const rootDir = path.join(__dirname, 'fixtures'); + + // @ts-expect-error -- services will have type information. + return parseForESLint(code, { + disallowAutomaticSingleRunInference: true, + filePath: path.join(rootDir, 'file.ts'), + project: './tsconfig.json', + tsconfigRootDir: rootDir, + }); +} describe('getConstrainedTypeAtLocation', () => { - describe('when the node has a generic constraint', () => { - it('returns the generic constraint type', () => { - const typeAtLocation = mockType(); - const baseConstraintOfType = mockType(); - const services = mockServices({ - baseConstraintOfType, - typeAtLocation, - }); - - expect(getConstrainedTypeAtLocation(services, node)).toBe( - baseConstraintOfType, - ); - }); + // See https://github.com/typescript-eslint/typescript-eslint/issues/10438 + // eslint-disable-next-line jest/no-disabled-tests -- known issue. + it.skip('returns unknown for unconstrained generic', () => { + const sourceCode = ` +function foo(x: T); + `; + + const { ast, services } = parseCodeForEslint(sourceCode); + + const functionNode = ast.body[0] as TSESTree.FunctionDeclaration; + const parameterNode = functionNode.params[0]; + + const constraintAtLocation = getConstrainedTypeAtLocation( + services, + parameterNode, + ); + + expect(tsutils.isTypeParameter(constraintAtLocation)).toBe(false); + // Requires https://github.com/microsoft/TypeScript/issues/60475 to solve. + expect(isTypeUnknownType(constraintAtLocation)).toBe(true); + }); + + it('returns unknown for extends unknown', () => { + const sourceCode = ` +function foo(x: T); + `; + + const { ast, services } = parseCodeForEslint(sourceCode); + + const functionNode = ast.body[0] as TSESTree.FunctionDeclaration; + const parameterNode = functionNode.params[0]; + + const constraintAtLocation = getConstrainedTypeAtLocation( + services, + parameterNode, + ); + + expect(tsutils.isTypeParameter(constraintAtLocation)).toBe(false); + expect(tsutils.isIntrinsicUnknownType(constraintAtLocation)).toBe(true); + }); + + it('returns unknown for extends any', () => { + const sourceCode = ` +function foo(x: T); + `; + + const { ast, services } = parseCodeForEslint(sourceCode); + + const functionNode = ast.body[0] as TSESTree.FunctionDeclaration; + const parameterNode = functionNode.params[0]; + + const constraintAtLocation = getConstrainedTypeAtLocation( + services, + parameterNode, + ); + + expect(tsutils.isTypeParameter(constraintAtLocation)).toBe(false); + expect(tsutils.isIntrinsicUnknownType(constraintAtLocation)).toBe(true); + }); + + it('returns string for extends string', () => { + const sourceCode = ` +function foo(x: T); + `; + + const { ast, services } = parseCodeForEslint(sourceCode); + + const functionNode = ast.body[0] as TSESTree.FunctionDeclaration; + const parameterNode = functionNode.params[0]; + + const constraintAtLocation = getConstrainedTypeAtLocation( + services, + parameterNode, + ); + + expect(tsutils.isTypeParameter(constraintAtLocation)).toBe(false); + expect(tsutils.isIntrinsicStringType(constraintAtLocation)).toBe(true); }); - describe('when the node does not have a generic constraint', () => { - it('returns the node type', () => { - const typeAtLocation = mockType(); - const services = mockServices({ typeAtLocation }); + it('returns string for non-generic string', () => { + const sourceCode = ` +function foo(x: string); + `; + + const { ast, services } = parseCodeForEslint(sourceCode); + + const functionNode = ast.body[0] as TSESTree.FunctionDeclaration; + const parameterNode = functionNode.params[0]; + + const constraintAtLocation = getConstrainedTypeAtLocation( + services, + parameterNode, + ); + + expect(tsutils.isTypeParameter(constraintAtLocation)).toBe(false); + expect(tsutils.isIntrinsicStringType(constraintAtLocation)).toBe(true); + }); + + it('handles type parameter whose constraint is a constrained type parameter', () => { + const sourceCode = ` +function foo() { + function bar(x: V) { + } +} + `; + + const { ast, services } = parseCodeForEslint(sourceCode); + + const outerFunctionNode = ast.body[0] as TSESTree.FunctionDeclaration; + const innerFunctionNode = outerFunctionNode.body + .body[0] as TSESTree.FunctionDeclaration; + const parameterNode = innerFunctionNode.params[0]; + + const constraintAtLocation = getConstrainedTypeAtLocation( + services, + parameterNode, + ); - expect(getConstrainedTypeAtLocation(services, node)).toBe(typeAtLocation); - }); + expect(tsutils.isTypeParameter(constraintAtLocation)).toBe(false); + expect(tsutils.isIntrinsicStringType(constraintAtLocation)).toBe(true); }); });