Skip to content

fix(eslint-plugin): add getConstraintInfo to handle generic constraints better #10496

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

Merged
15 changes: 11 additions & 4 deletions packages/eslint-plugin/src/rules/await-thenable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as ts from 'typescript';

import {
createRule,
getConstrainedTypeAtLocation,
getConstraintInfo,
getParserServices,
isStrongPrecedenceNode,
} from '../util';
Expand Down Expand Up @@ -85,6 +85,7 @@ export default createRule<Options, MessageIds>({
],
create(context, [options]) {
const services = getParserServices(context);
const checker = services.program.getTypeChecker();

function getBooleanComparison(
node: TSESTree.BinaryExpression,
Expand All @@ -94,19 +95,23 @@ export default createRule<Options, MessageIds>({
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,
Expand Down
23 changes: 10 additions & 13 deletions packages/eslint-plugin/src/rules/no-unnecessary-condition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as ts from 'typescript';
import {
createRule,
getConstrainedTypeAtLocation,
getConstraintInfo,
getParserServices,
getTypeName,
getTypeOfPropertyOfName,
Expand Down Expand Up @@ -653,16 +654,7 @@ export default createRule<Options, MessageId>({
.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`
Expand All @@ -673,16 +665,21 @@ export default createRule<Options, MessageId>({
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;
}

Expand Down
60 changes: 60 additions & 0 deletions packages/eslint-plugin/src/util/getConstraintInfo.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 6 additions & 8 deletions packages/eslint-plugin/src/util/needsToBeAwaited.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,20 @@ ruleTester.run('no-unnecessary-boolean-literal-compare', rule, {
},
"'false' === true;",
"'true' === false;",
`
const unconstrained: <T>(someCondition: T) => void = someCondition => {
if (someCondition === true) {
}
};
`,
`
const extendsUnknown: <T extends unknown>(
someCondition: T,
) => void = someCondition => {
if (someCondition === true) {
}
};
`,
],

invalid: [
Expand Down
Loading
Loading