β¬ οΈ Back to Table of Contents
π no-unnecessary-condition¶
π Analysis Summary¶
| Metric | Count |
|---|---|
| π§ Functions | 30 |
| π¦ Imports | 22 |
| π Variables & Constants | 3 |
| π Type Aliases | 5 |
π Table of Contents¶
π οΈ File Location:¶
π packages/eslint-plugin/src/rules/no-unnecessary-condition.ts
π€ Default Export¶
| Property | Value |
|---|---|
name |
'no-unnecessary-condition' |
meta.type |
'suggestion' |
meta.docs.description |
'Disallow conditionals where the type is always truthy or always falsy' |
meta.docs.recommended |
'strict' |
meta.docs.requiresTypeChecking |
true |
meta.hasSuggestions |
true |
meta.messages.alwaysFalsy |
'Unnecessary conditional, value is always falsy.' |
meta.messages.alwaysFalsyFunc |
'This callback should return a conditional, but return is always falsy.' |
meta.messages.alwaysNullish |
'Unnecessary conditional, left-hand side of ?? operator is always null or undefined.' |
meta.messages.alwaysTruthy |
'Unnecessary conditional, value is always truthy.' |
meta.messages.alwaysTruthyFunc |
'This callback should return a conditional, but return is always truthy.' |
meta.messages.comparisonBetweenLiteralTypes |
'Unnecessary conditional, comparison is always {{trueOrFalse}}, since {{left}} {{operator}} {{right}} is {{trueOrFa... |
meta.messages.never |
'Unnecessary conditional, value is never.' |
meta.messages.neverNullish |
'Unnecessary conditional, expected left-hand side of ?? operator to be possibly null or undefined.' |
meta.messages.neverOptionalChain |
'Unnecessary optional chain on a non-nullish value.' |
meta.messages.noOverlapBooleanExpression |
'Unnecessary conditional, the types have no overlap.' |
meta.messages.noStrictNullCheck |
'This rule requires the strictNullChecks compiler option to be turned on to function correctly.' |
meta.messages.suggestRemoveOptionalChain |
'Remove unnecessary optional chain' |
meta.messages.typeGuardAlreadyIsType |
'Unnecessary conditional, expression already has the type being checked by the {{typeGuardOrAssertionFunction}}.' |
meta.schema |
[ { type: 'object', additionalProperties: false, properties: { allowConstantLoopConditions: { description: 'Whether t... |
defaultOptions |
[ { allowConstantLoopConditions: 'never', allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false, checkTypePre... |
Entry point: create β documented under Functions.
π¦ Imports¶
| Name | Source |
|---|---|
TSESTree |
@typescript-eslint/utils |
AST_NODE_TYPES |
@typescript-eslint/utils |
AST_TOKEN_TYPES |
@typescript-eslint/utils |
createRule |
../util |
getConstrainedTypeAtLocation |
../util |
getConstraintInfo |
../util |
getParserServices |
../util |
getTypeName |
../util |
getTypeOfPropertyOfName |
../util |
getValueOfLiteralType |
../util |
isArrayMethodCallWithPredicate |
../util |
isIdentifier |
../util |
isNullableType |
../util |
isPossiblyFalsy |
../util |
isPossiblyTruthy |
../util |
isTypeAnyType |
../util |
isTypeFlagSet |
../util |
isTypeUnknownType |
../util |
nullThrows |
../util |
NullThrowsReasons |
../util |
findTruthinessAssertedArgument |
../util/assertionFunctionUtils |
findTypeGuardAssertedArgument |
../util/assertionFunctionUtils |
Variables & Constants¶
| Name | Type | Kind | Value | Exported |
|---|---|---|---|---|
nullishFlag |
number |
const | ts.TypeFlags.Undefined \| ts.TypeFlags.Null |
β |
BOOL_OPERATORS |
Set<"==" \| "===" \| "<" \| ">" \| "<... |
const | new Set([ '<', '>', '<=', '>=', '==', '===', '!=', '!==', ] as const) |
β |
constantLoopConditionsAllow... |
Set<unknown> |
const | new Set<unknown>([ true, false, 1, 0, ]) |
β |
Functions¶
create(context: any, [ { allowConstantLoopConditionsβ¦: any): { AssignmentExpression: (node: TSESTree.AssignmentExpressio⦶
Parameters:
contextany[ { allowConstantLoopConditions, allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing, checkTypePredicates, }, ]any
Returns: { AssignmentExpression: (node: TSESTree.AssignmentExpression) => void; BinaryExpression(node: any): void; CallExpression: (node: TSESTree.CallExpression) => void; 'CallExpression[optional = true]': (node: TSESTree.CallExpression) => void; ConditionalExpression: (node: any) => void; DoWhileStatement: (node: TSESTree.DoWhileStatement | TSESTree.ForStatement | TSESTree.WhileStatement) => void; ForStatement: (node: TSESTree.DoWhileStatement | TSESTree.ForStatement | TSESTree.WhileStatement) => void; IfStatement: (node: any) => void; LogicalExpression: (node: TSESTree.LogicalExpression) => void; 'MemberExpression[optional = true]': (node: TSESTree.MemberExpression) => void; SwitchCase({ parent, test }: { parent: any; test: any; }): void; WhileStatement: (node: TSESTree.DoWhileStatement | TSESTree.ForStatement | TSESTree.WhileStatement) => void; }
Calls:
getParserServices (from ../util)services.program.getTypeCheckerservices.program.getCompilerOptionstsutils.isStrictCompilerOptionEnabledtsutils.isCompilerOptionEnablednormalizeAllowConstantLoopConditionscontext.reportgetConstrainedTypeAtLocation (from ../util)tsutils .unionConstituents(nodeType) .somechecker.isArrayTypechecker.isTupleTypenodeIsArrayTypenodeIsTupleTypetsutils .unionConstituents(type) .someisTypeAnyType (from ../util)isTypeUnknownType (from ../util)isTypeFlagSet (from ../util)services.getTypeAtLocationisNullablePropertyTypecontext.sourceCode.getTextobjectType .getProperties() .findtsutils.isSymbolFlagSetcheckNodeisArrayIndexExpressionisOnlyUsedForTruthinessisConditionalAlwaysNecessaryisPossiblyTruthy (from ../util)isPossiblyFalsy (from ../util)isPossiblyNullishisNullableMemberExpressionoptionChainContainsOptionArrayIndexisAlwaysNullishtoStaticValuebooleanComparisonchecker.typeToStringisComparablecheckNodeForNullishconstantLoopConditionsAllowedLiterals.hastsutils.isTrueLiteralTypefindTruthinessAssertedArgument (from ../util/assertionFunctionUtils)findTypeGuardAssertedArgument (from ../util/assertionFunctionUtils)tsutils.isTypeFlagSetchecker.isTypeAssignableTotypeGuardAssertedArgument.type.isUnionisArrayMethodCallWithPredicate (from ../util)tsutils .getCallSignaturesOfType( getConstrainedTypeAtLocation(services, callback), ) .mapsig.getReturnTypegetConstraintInfo (from ../util)propertyType.isUnionpropertyType.types.somepropertyType.isNumberLiteralpropertyType.isStringLiteralgetTypeOfPropertyOfName (from ../util)propertyType.value.toStringisNullableType (from ../util)getTypeName (from ../util)checker .getIndexInfosOfType(objType) .someprevType.isUnionisIdentifier (from ../util)prevType.types.somechecker.getIndexInfosOfTypeindexInfo.sometype.getCallSignaturessignatures.someisMemberExpressionNullableOriginFromObjectisCallExpressionNullableOriginFromCalleeisOptionableExpressionnullThrows (from ../util)context.sourceCode.getTokenAfterNullThrowsReasons.MissingTokenfixer.replaceTextcheckOptionalChain['&&=', '||='].includesisBoolOperatorcheckIfBoolExpressionIsNecessaryConditional
Internal Comments:
// https://github.com/typescript-eslint/typescript-eslint/issues/5439 (x2)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion (x2)
// Is an index signature (x5)
// ...into an array type
// ... or a tuple type
// Exception: literal index into a tuple - will have a sound type (x4)
// Conditional is always necessary if it involves:
// `any` or `unknown` or a naked type variable
// Get the actual property name, to account for private properties (this.#prop). (x2)
/**
* Checks if a conditional node is necessary:
* if the type of the node is always true or always false, it's not necessary.
*/
// Check if the node is Unary Negation expression and handle it
// Since typescript array index signature types don't represent the (x3)
// possibility of out-of-bounds access, if we're indexing into an array (x3)
// just skip the check, to avoid false positives (x3)
// Only check the right side when the logical expression is used for
// truthiness, as the left side has already been checked by
// checkLogicalExpressionForUnnecessaryConditionals.
//
// Unless the node is nullish coalescing, as it's common to use patterns like
// `nullBool ?? true` to perform strict boolean checks. If we inspect the right
// here, it'll usually be a constant condition on purpose.
// In this case it's better to inspect the type of the expression as a whole.
// Conditional is always necessary if it involves `any`, `unknown` or a naked type parameter
/**
* Checks that a binary expression is necessarily conditional, reports otherwise.
* If both sides of the binary expression are literal values, it's not a necessary condition.
*
* NOTE: It's also unnecessary if the types that don't overlap at all
* but that case is handled by the Typescript compiler itself.
* Known exceptions:
* - https://github.com/microsoft/TypeScript/issues/32627
* - https://github.com/microsoft/TypeScript/issues/37160 (handled)
*/
// Workaround for https://github.com/microsoft/TypeScript/issues/37160
// Allow comparison to `any`, `unknown` or a naked type parameter. (x3)
// Allow loose comparison to nullish values.
/**
* Checks that a logical expression contains a boolean, reports otherwise.
*/
// Only checks the left side, since the right side might not be "conditional" at all. (x3)
// The right side will be checked if the LogicalExpression is used in a conditional context (x3)
/**
* Checks that a testable expression of a loop is necessarily conditional, reports otherwise.
*/
// e.g. `for(;;)`
// Skip `any` β it is assignable to everything, producing (x3)
// false positives for meaningful runtime type guards. (x3)
// Only flag if the types are mutually assignable (i.e. equivalent,
// like Narrower β Wider with optional props) or the predicate type
// is a union that the argument is a strict subtype of. This avoids
// false positives with structural subtypes whose extra members are
// all optional in the *predicate* type (e.g. custom MappedType
// interfaces extending ts.Type).
// If this is something like arr.filter(x => /*condition*/), check `condition`
// Inline defined functions
// Two special cases, where we can directly check the node that's returned:
// () => something
// () => { return something; } (x2)
// Otherwise just do type analysis on the function as a whole. (x2)
// Not a callable function, e.g. `any`
// Predicate is always necessary if it involves `any` or `unknown`
// bail early if both a possibly-truthy and a possibly-falsy have been detected
// Recursively searches an optional chain for an array index expression
// Has to search the entire chain, because an array index will "infect" the rest of the types
// Example: (x2)
// ``` (x4)
// [{x: {y: "z"} }][n] // type is {x: {y: "z"}}
// ?.x // type is {y: "z"}
// ?.y // This access is considered "unnecessary" according to the types
// Checks whether a member expression is nullable or not regardless of it's previous node.
// // 'bar' is nullable if 'foo' is null.
// // but this function checks regardless of 'foo' type, so returns 'true'.
// declare const foo: { bar : { baz: string } } | null
// foo?.bar;
// We only care if this step in the chain is optional. If just descend
// from an optional chain, then that's fine.
// Similar to checkLogicalExpressionForUnnecessaryConditionals, since
// a ||= b is equivalent to a || (a = b)
// only check `case ...:`, not `default:`
Code
create(
context,
[
{
allowConstantLoopConditions,
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing,
checkTypePredicates,
},
],
) {
const services = getParserServices(context);
const checker = services.program.getTypeChecker();
const compilerOptions = services.program.getCompilerOptions();
const isStrictNullChecks = tsutils.isStrictCompilerOptionEnabled(
compilerOptions,
'strictNullChecks',
);
const isNoUncheckedIndexedAccess = tsutils.isCompilerOptionEnabled(
compilerOptions,
'noUncheckedIndexedAccess',
);
const allowConstantLoopConditionsOption =
normalizeAllowConstantLoopConditions(
// https://github.com/typescript-eslint/typescript-eslint/issues/5439
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
allowConstantLoopConditions!,
);
if (
!isStrictNullChecks &&
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing !== true
) {
context.report({
loc: {
start: { column: 0, line: 0 },
end: { column: 0, line: 0 },
},
messageId: 'noStrictNullCheck',
});
}
function nodeIsArrayType(node: TSESTree.Expression): boolean {
const nodeType = getConstrainedTypeAtLocation(services, node);
return tsutils
.unionConstituents(nodeType)
.some(part => checker.isArrayType(part));
}
function nodeIsTupleType(node: TSESTree.Expression): boolean {
const nodeType = getConstrainedTypeAtLocation(services, node);
return tsutils
.unionConstituents(nodeType)
.some(part => checker.isTupleType(part));
}
function isArrayIndexExpression(node: TSESTree.Expression): boolean {
return (
// Is an index signature
node.type === AST_NODE_TYPES.MemberExpression &&
node.computed &&
// ...into an array type
(nodeIsArrayType(node.object) ||
// ... or a tuple type
(nodeIsTupleType(node.object) &&
// Exception: literal index into a tuple - will have a sound type
node.property.type !== AST_NODE_TYPES.Literal))
);
}
// Conditional is always necessary if it involves:
// `any` or `unknown` or a naked type variable
function isConditionalAlwaysNecessary(type: ts.Type): boolean {
return tsutils
.unionConstituents(type)
.some(
part =>
isTypeAnyType(part) ||
isTypeUnknownType(part) ||
isTypeFlagSet(part, ts.TypeFlags.TypeVariable),
);
}
function isNullableMemberExpression(
node: TSESTree.MemberExpression,
): boolean {
const objectType = services.getTypeAtLocation(node.object);
if (node.computed) {
const propertyType = services.getTypeAtLocation(node.property);
return isNullablePropertyType(objectType, propertyType);
}
const property = node.property;
// Get the actual property name, to account for private properties (this.#prop).
const propertyName = context.sourceCode.getText(property);
const propertyType = objectType
.getProperties()
.find(prop => prop.name === propertyName);
if (
propertyType &&
tsutils.isSymbolFlagSet(propertyType, ts.SymbolFlags.Optional)
) {
return true;
}
return false;
}
/**
* Checks if a conditional node is necessary:
* if the type of the node is always true or always false, it's not necessary.
*/
function checkNode(
expression: TSESTree.Expression,
isUnaryNotArgument = false,
node = expression,
): void {
// Check if the node is Unary Negation expression and handle it
if (
expression.type === AST_NODE_TYPES.UnaryExpression &&
expression.operator === '!'
) {
return checkNode(expression.argument, !isUnaryNotArgument, node);
}
// Since typescript array index signature types don't represent the
// possibility of out-of-bounds access, if we're indexing into an array
// just skip the check, to avoid false positives
if (!isNoUncheckedIndexedAccess && isArrayIndexExpression(expression)) {
return;
}
// Only check the right side when the logical expression is used for
// truthiness, as the left side has already been checked by
// checkLogicalExpressionForUnnecessaryConditionals.
//
// Unless the node is nullish coalescing, as it's common to use patterns like
// `nullBool ?? true` to perform strict boolean checks. If we inspect the right
// here, it'll usually be a constant condition on purpose.
// In this case it's better to inspect the type of the expression as a whole.
if (
expression.type === AST_NODE_TYPES.LogicalExpression &&
expression.operator !== '??'
) {
if (isOnlyUsedForTruthiness(expression)) {
checkNode(expression.right);
}
return;
}
const type = getConstrainedTypeAtLocation(services, expression);
if (isConditionalAlwaysNecessary(type)) {
return;
}
let messageId: MessageId | null = null;
if (isTypeFlagSet(type, ts.TypeFlags.Never)) {
messageId = 'never';
} else if (!isPossiblyTruthy(type)) {
messageId = !isUnaryNotArgument ? 'alwaysFalsy' : 'alwaysTruthy';
} else if (!isPossiblyFalsy(type)) {
messageId = !isUnaryNotArgument ? 'alwaysTruthy' : 'alwaysFalsy';
}
if (messageId) {
context.report({ node, messageId });
}
}
function checkNodeForNullish(node: TSESTree.Expression): void {
const type = getConstrainedTypeAtLocation(services, node);
// Conditional is always necessary if it involves `any`, `unknown` or a naked type parameter
if (
isTypeFlagSet(
type,
ts.TypeFlags.Any |
ts.TypeFlags.Unknown |
ts.TypeFlags.TypeParameter |
ts.TypeFlags.TypeVariable,
)
) {
return;
}
let messageId: MessageId | null = null;
if (isTypeFlagSet(type, ts.TypeFlags.Never)) {
messageId = 'never';
} else if (
!isPossiblyNullish(type) &&
!(
node.type === AST_NODE_TYPES.MemberExpression &&
isNullableMemberExpression(node)
)
) {
// Since typescript array index signature types don't represent the
// possibility of out-of-bounds access, if we're indexing into an array
// just skip the check, to avoid false positives
if (
isNoUncheckedIndexedAccess ||
(!isArrayIndexExpression(node) &&
!(
node.type === AST_NODE_TYPES.ChainExpression &&
node.expression.type !== AST_NODE_TYPES.TSNonNullExpression &&
optionChainContainsOptionArrayIndex(node.expression)
))
) {
messageId = 'neverNullish';
}
} else if (isAlwaysNullish(type)) {
messageId = 'alwaysNullish';
}
if (messageId) {
context.report({ node, messageId });
}
}
/**
* Checks that a binary expression is necessarily conditional, reports otherwise.
* If both sides of the binary expression are literal values, it's not a necessary condition.
*
* NOTE: It's also unnecessary if the types that don't overlap at all
* but that case is handled by the Typescript compiler itself.
* Known exceptions:
* - https://github.com/microsoft/TypeScript/issues/32627
* - https://github.com/microsoft/TypeScript/issues/37160 (handled)
*/
function checkIfBoolExpressionIsNecessaryConditional(
node: TSESTree.Node,
left: TSESTree.Node,
right: TSESTree.Node,
operator: BoolOperator,
): void {
const leftType = getConstrainedTypeAtLocation(services, left);
const rightType = getConstrainedTypeAtLocation(services, right);
const leftStaticValue = toStaticValue(leftType);
const rightStaticValue = toStaticValue(rightType);
if (leftStaticValue != null && rightStaticValue != null) {
const conditionIsTrue = booleanComparison(
leftStaticValue.value,
operator,
rightStaticValue.value,
);
context.report({
node,
messageId: 'comparisonBetweenLiteralTypes',
data: {
left: checker.typeToString(leftType),
operator,
right: checker.typeToString(rightType),
trueOrFalse: conditionIsTrue ? 'true' : 'false',
},
});
return;
}
// Workaround for https://github.com/microsoft/TypeScript/issues/37160
if (isStrictNullChecks) {
const UNDEFINED = ts.TypeFlags.Undefined;
const NULL = ts.TypeFlags.Null;
const VOID = ts.TypeFlags.Void;
const isComparable = (type: ts.Type, flag: ts.TypeFlags): boolean => {
// Allow comparison to `any`, `unknown` or a naked type parameter.
flag |=
ts.TypeFlags.Any |
ts.TypeFlags.Unknown |
ts.TypeFlags.TypeParameter |
ts.TypeFlags.TypeVariable;
// Allow loose comparison to nullish values.
if (operator === '==' || operator === '!=') {
flag |= NULL | UNDEFINED | VOID;
}
return isTypeFlagSet(type, flag);
};
if (
(leftType.flags === UNDEFINED &&
!isComparable(rightType, UNDEFINED | VOID)) ||
(rightType.flags === UNDEFINED &&
!isComparable(leftType, UNDEFINED | VOID)) ||
(leftType.flags === NULL && !isComparable(rightType, NULL)) ||
(rightType.flags === NULL && !isComparable(leftType, NULL))
) {
context.report({ node, messageId: 'noOverlapBooleanExpression' });
return;
}
}
}
/**
* Checks that a logical expression contains a boolean, reports otherwise.
*/
function checkLogicalExpressionForUnnecessaryConditionals(
node: TSESTree.LogicalExpression,
): void {
if (node.operator === '??') {
checkNodeForNullish(node.left);
return;
}
// Only checks the left side, since the right side might not be "conditional" at all.
// The right side will be checked if the LogicalExpression is used in a conditional context
checkNode(node.left);
}
/**
* Checks that a testable expression of a loop is necessarily conditional, reports otherwise.
*/
function checkIfLoopIsNecessaryConditional(
node:
| TSESTree.DoWhileStatement
| TSESTree.ForStatement
| TSESTree.WhileStatement,
): void {
if (node.test == null) {
// e.g. `for(;;)`
return;
}
if (
allowConstantLoopConditionsOption === 'only-allowed-literals' &&
node.test.type === AST_NODE_TYPES.Literal &&
constantLoopConditionsAllowedLiterals.has(node.test.value)
) {
return;
}
if (
allowConstantLoopConditionsOption === 'always' &&
tsutils.isTrueLiteralType(
getConstrainedTypeAtLocation(services, node.test),
)
) {
return;
}
checkNode(node.test);
}
function checkCallExpression(node: TSESTree.CallExpression): void {
if (checkTypePredicates) {
const truthinessAssertedArgument = findTruthinessAssertedArgument(
services,
node,
);
if (truthinessAssertedArgument != null) {
checkNode(truthinessAssertedArgument);
}
const typeGuardAssertedArgument = findTypeGuardAssertedArgument(
services,
node,
);
if (typeGuardAssertedArgument != null) {
const typeOfArgument = getConstrainedTypeAtLocation(
services,
typeGuardAssertedArgument.argument,
);
if (
// Skip `any` β it is assignable to everything, producing
// false positives for meaningful runtime type guards.
!tsutils.isTypeFlagSet(
typeOfArgument,
ts.TypeFlags.Any | ts.TypeFlags.Unknown,
) &&
checker.isTypeAssignableTo(
typeOfArgument,
typeGuardAssertedArgument.type,
) &&
// Only flag if the types are mutually assignable (i.e. equivalent,
// like Narrower β Wider with optional props) or the predicate type
// is a union that the argument is a strict subtype of. This avoids
// false positives with structural subtypes whose extra members are
// all optional in the *predicate* type (e.g. custom MappedType
// interfaces extending ts.Type).
(checker.isTypeAssignableTo(
typeGuardAssertedArgument.type,
typeOfArgument,
) ||
typeGuardAssertedArgument.type.isUnion())
) {
context.report({
node: typeGuardAssertedArgument.argument,
messageId: 'typeGuardAlreadyIsType',
data: {
typeGuardOrAssertionFunction: typeGuardAssertedArgument.asserts
? 'assertion function'
: 'type guard',
},
});
}
}
}
// If this is something like arr.filter(x => /*condition*/), check `condition`
if (
isArrayMethodCallWithPredicate(context, services, node) &&
node.arguments.length
) {
const callback = node.arguments[0];
// Inline defined functions
if (
callback.type === AST_NODE_TYPES.ArrowFunctionExpression ||
callback.type === AST_NODE_TYPES.FunctionExpression
) {
// Two special cases, where we can directly check the node that's returned:
// () => something
if (callback.body.type !== AST_NODE_TYPES.BlockStatement) {
return checkNode(callback.body);
}
// () => { return something; }
const callbackBody = callback.body.body;
if (
callbackBody.length === 1 &&
callbackBody[0].type === AST_NODE_TYPES.ReturnStatement &&
callbackBody[0].argument
) {
return checkNode(callbackBody[0].argument);
}
// Potential enhancement: could use code-path analysis to check
// any function with a single return statement
// (Value to complexity ratio is dubious however)
}
// Otherwise just do type analysis on the function as a whole.
const returnTypes = tsutils
.getCallSignaturesOfType(
getConstrainedTypeAtLocation(services, callback),
)
.map(sig => sig.getReturnType());
if (returnTypes.length === 0) {
// Not a callable function, e.g. `any`
return;
}
let hasFalsyReturnTypes = false;
let hasTruthyReturnTypes = false;
for (const type of returnTypes) {
const { constraintType } = getConstraintInfo(checker, type);
// Predicate is always necessary if it involves `any` or `unknown`
if (
!constraintType ||
isTypeAnyType(constraintType) ||
isTypeUnknownType(constraintType)
) {
return;
}
if (isPossiblyFalsy(constraintType)) {
hasFalsyReturnTypes = true;
}
if (isPossiblyTruthy(constraintType)) {
hasTruthyReturnTypes = true;
}
// bail early if both a possibly-truthy and a possibly-falsy have been detected
if (hasFalsyReturnTypes && hasTruthyReturnTypes) {
return;
}
}
if (!hasFalsyReturnTypes) {
return context.report({
node: callback,
messageId: 'alwaysTruthyFunc',
});
}
if (!hasTruthyReturnTypes) {
return context.report({
node: callback,
messageId: 'alwaysFalsyFunc',
});
}
}
}
// Recursively searches an optional chain for an array index expression
// Has to search the entire chain, because an array index will "infect" the rest of the types
// Example:
// ```
// [{x: {y: "z"} }][n] // type is {x: {y: "z"}}
// ?.x // type is {y: "z"}
// ?.y // This access is considered "unnecessary" according to the types
// ```
function optionChainContainsOptionArrayIndex(
node: TSESTree.CallExpression | TSESTree.MemberExpression,
): boolean {
const lhsNode =
node.type === AST_NODE_TYPES.CallExpression ? node.callee : node.object;
if (node.optional && isArrayIndexExpression(lhsNode)) {
return true;
}
if (
lhsNode.type === AST_NODE_TYPES.MemberExpression ||
lhsNode.type === AST_NODE_TYPES.CallExpression
) {
return optionChainContainsOptionArrayIndex(lhsNode);
}
return false;
}
function isNullablePropertyType(
objType: ts.Type,
propertyType: ts.Type,
): boolean {
if (propertyType.isUnion()) {
return propertyType.types.some(type =>
isNullablePropertyType(objType, type),
);
}
if (propertyType.isNumberLiteral() || propertyType.isStringLiteral()) {
const propType = getTypeOfPropertyOfName(
checker,
objType,
propertyType.value.toString(),
);
if (propType) {
return isNullableType(propType);
}
}
const typeName = getTypeName(checker, propertyType);
return checker
.getIndexInfosOfType(objType)
.some(info => getTypeName(checker, info.keyType) === typeName);
}
// Checks whether a member expression is nullable or not regardless of it's previous node.
// Example:
// ```
// // 'bar' is nullable if 'foo' is null.
// // but this function checks regardless of 'foo' type, so returns 'true'.
// declare const foo: { bar : { baz: string } } | null
// foo?.bar;
// ```
function isMemberExpressionNullableOriginFromObject(
node: TSESTree.MemberExpression,
): boolean {
const prevType = getConstrainedTypeAtLocation(services, node.object);
const property = node.property;
if (prevType.isUnion() && isIdentifier(property)) {
const isOwnNullable = prevType.types.some(type => {
if (node.computed) {
const propertyType = getConstrainedTypeAtLocation(
services,
node.property,
);
return isNullablePropertyType(type, propertyType);
}
const propType = getTypeOfPropertyOfName(
checker,
type,
property.name,
);
if (propType) {
return isNullableType(propType);
}
const indexInfo = checker.getIndexInfosOfType(type);
return indexInfo.some(info => {
const isStringTypeName =
getTypeName(checker, info.keyType) === 'string';
return (
isStringTypeName &&
(isNoUncheckedIndexedAccess || isNullableType(info.type))
);
});
});
return !isOwnNullable && isNullableType(prevType);
}
return false;
}
function isCallExpressionNullableOriginFromCallee(
node: TSESTree.CallExpression,
): boolean {
const prevType = getConstrainedTypeAtLocation(services, node.callee);
if (prevType.isUnion()) {
const isOwnNullable = prevType.types.some(type => {
const signatures = type.getCallSignatures();
return signatures.some(sig => isNullableType(sig.getReturnType()));
});
return !isOwnNullable && isNullableType(prevType);
}
return false;
}
function isOptionableExpression(node: TSESTree.Expression): boolean {
const type = getConstrainedTypeAtLocation(services, node);
const isOwnNullable =
node.type === AST_NODE_TYPES.MemberExpression
? !isMemberExpressionNullableOriginFromObject(node)
: node.type === AST_NODE_TYPES.CallExpression
? !isCallExpressionNullableOriginFromCallee(node)
: true;
return (
isConditionalAlwaysNecessary(type) ||
(isOwnNullable && isNullableType(type))
);
}
function checkOptionalChain(
node: TSESTree.CallExpression | TSESTree.MemberExpression,
beforeOperator: TSESTree.Node,
fix: '' | '.',
): void {
// We only care if this step in the chain is optional. If just descend
// from an optional chain, then that's fine.
if (!node.optional) {
return;
}
// Since typescript array index signature types don't represent the
// possibility of out-of-bounds access, if we're indexing into an array
// just skip the check, to avoid false positives
if (
!isNoUncheckedIndexedAccess &&
optionChainContainsOptionArrayIndex(node)
) {
return;
}
const nodeToCheck =
node.type === AST_NODE_TYPES.CallExpression ? node.callee : node.object;
if (isOptionableExpression(nodeToCheck)) {
return;
}
const questionDotOperator = nullThrows(
context.sourceCode.getTokenAfter(
beforeOperator,
token =>
token.type === AST_TOKEN_TYPES.Punctuator && token.value === '?.',
),
NullThrowsReasons.MissingToken('operator', node.type),
);
context.report({
loc: questionDotOperator.loc,
node,
messageId: 'neverOptionalChain',
suggest: [
{
messageId: 'suggestRemoveOptionalChain',
fix(fixer) {
return fixer.replaceText(questionDotOperator, fix);
},
},
],
});
}
function checkOptionalMemberExpression(
node: TSESTree.MemberExpression,
): void {
checkOptionalChain(node, node.object, node.computed ? '' : '.');
}
function checkOptionalCallExpression(node: TSESTree.CallExpression): void {
checkOptionalChain(node, node.callee, '');
}
function checkAssignmentExpression(
node: TSESTree.AssignmentExpression,
): void {
// Similar to checkLogicalExpressionForUnnecessaryConditionals, since
// a ||= b is equivalent to a || (a = b)
if (['&&=', '||='].includes(node.operator)) {
checkNode(node.left);
} else if (node.operator === '??=') {
checkNodeForNullish(node.left);
}
}
return {
AssignmentExpression: checkAssignmentExpression,
BinaryExpression(node): void {
const { operator } = node;
if (isBoolOperator(operator)) {
checkIfBoolExpressionIsNecessaryConditional(
node,
node.left,
node.right,
operator,
);
}
},
CallExpression: checkCallExpression,
'CallExpression[optional = true]': checkOptionalCallExpression,
ConditionalExpression: (node): void => checkNode(node.test),
DoWhileStatement: checkIfLoopIsNecessaryConditional,
ForStatement: checkIfLoopIsNecessaryConditional,
IfStatement: (node): void => checkNode(node.test),
LogicalExpression: checkLogicalExpressionForUnnecessaryConditionals,
'MemberExpression[optional = true]': checkOptionalMemberExpression,
SwitchCase({ parent, test }): void {
// only check `case ...:`, not `default:`
if (test) {
checkIfBoolExpressionIsNecessaryConditional(
test,
parent.discriminant,
test,
'===',
);
}
},
WhileStatement: checkIfLoopIsNecessaryConditional,
};
}
isNullishType(type: ts.Type): boolean¶
Parameters:
typets.Type
Returns: boolean
Calls:
tsutils.isTypeFlagSet
Code
isAlwaysNullish(type: ts.Type): boolean¶
Parameters:
typets.Type
Returns: boolean
Calls:
tsutils.unionConstituents(type).every
Code
isPossiblyNullish(type: ts.Type): boolean¶
Note that this differs from in that it doesn't consider
any or unknown to be nullable.
Raw JSDoc
Calls:
tsutils .unionConstituents(type) .someisNullishTypeisTypeFlagSet (from ../util)
Code
toStaticValue(type: ts.Type): { value: bigint | boolean | number | string | null | undefi⦶
Parameters:
typets.Type
Returns: { value: bigint | boolean | number | string | null | undefined } | undefined
Calls:
tsutils.isBooleanLiteralTypetsutils.isTrueLiteralTypetype.isLiteralgetValueOfLiteralType (from ../util)
Internal Comments:
Code
function toStaticValue(
type: ts.Type,
):
{ value: bigint | boolean | number | string | null | undefined } | undefined {
// type.isLiteral() only covers numbers/bigints and strings, hence the rest of the branches.
if (tsutils.isBooleanLiteralType(type)) {
return { value: tsutils.isTrueLiteralType(type) };
}
if (type.flags === ts.TypeFlags.Undefined) {
return { value: undefined };
}
if (type.flags === ts.TypeFlags.Null) {
return { value: null };
}
if (type.isLiteral()) {
return { value: getValueOfLiteralType(type) };
}
return undefined;
}
isBoolOperator(operator: string): operator is BoolOperator¶
Parameters:
operatorstring
Returns: operator is BoolOperator
Calls:
(BOOL_OPERATORS as Set<string>).has
Code
booleanComparison(left: unknown, operator: BoolOperator, right: unknown): boolean¶
Parameters:
leftunknownoperatorBoolOperatorrightunknown
Returns: boolean
Internal Comments:
// eslint-disable-next-line eqeqeq -- intentionally comparing with loose equality (x2)
// @ts-expect-error: we don't care if the comparison seems unintentional. (x4)
Code
function booleanComparison(
left: unknown,
operator: BoolOperator,
right: unknown,
): boolean {
switch (operator) {
case '!=':
// eslint-disable-next-line eqeqeq -- intentionally comparing with loose equality
return left != right;
case '!==':
return left !== right;
case '<':
// @ts-expect-error: we don't care if the comparison seems unintentional.
return left < right;
case '<=':
// @ts-expect-error: we don't care if the comparison seems unintentional.
return left <= right;
case '==':
// eslint-disable-next-line eqeqeq -- intentionally comparing with loose equality
return left == right;
case '===':
return left === right;
case '>':
// @ts-expect-error: we don't care if the comparison seems unintentional.
return left > right;
case '>=':
// @ts-expect-error: we don't care if the comparison seems unintentional.
return left >= right;
}
}
isOnlyUsedForTruthiness(node: TSESTree.Expression): boolean¶
Parameters:
nodeTSESTree.Expression
Returns: boolean
Calls:
isOnlyUsedForTruthiness
Code
function isOnlyUsedForTruthiness(node: TSESTree.Expression): boolean {
const parent = node.parent;
switch (parent.type) {
case AST_NODE_TYPES.ConditionalExpression:
case AST_NODE_TYPES.DoWhileStatement:
case AST_NODE_TYPES.ForStatement:
case AST_NODE_TYPES.IfStatement:
case AST_NODE_TYPES.WhileStatement:
return parent.test === node;
case AST_NODE_TYPES.LogicalExpression:
return (
(parent.operator === '&&' && parent.left === node) ||
isOnlyUsedForTruthiness(parent)
);
case AST_NODE_TYPES.UnaryExpression:
return parent.operator === '!';
default:
return false;
}
}
normalizeAllowConstantLoopConditions(allowConstantLoopConditions: AllowConstantLoopConditions | LegacyAllβ¦): AllowConstantLoopConditions¶
Parameters:
allowConstantLoopConditionsAllowConstantLoopConditions | LegacyAllowConstantLoopConditions
Returns: AllowConstantLoopConditions
Code
function normalizeAllowConstantLoopConditions(
allowConstantLoopConditions:
AllowConstantLoopConditions | LegacyAllowConstantLoopConditions,
): AllowConstantLoopConditions {
if (allowConstantLoopConditions === true) {
return 'always';
}
if (allowConstantLoopConditions === false) {
return 'never';
}
return allowConstantLoopConditions;
}
Internal helpers¶
Declared inside another function in this file.
nodeIsArrayType(node: TSESTree.Expression): boolean¶
Parameters:
nodeTSESTree.Expression
Returns: boolean
Calls:
getConstrainedTypeAtLocation (from ../util)tsutils .unionConstituents(nodeType) .somechecker.isArrayType
Code
nodeIsTupleType(node: TSESTree.Expression): boolean¶
Parameters:
nodeTSESTree.Expression
Returns: boolean
Calls:
getConstrainedTypeAtLocation (from ../util)tsutils .unionConstituents(nodeType) .somechecker.isTupleType
Code
isArrayIndexExpression(node: TSESTree.Expression): boolean¶
Parameters:
nodeTSESTree.Expression
Returns: boolean
Calls:
nodeIsArrayTypenodeIsTupleType
Internal Comments:
// Is an index signature (x5)
// ...into an array type
// ... or a tuple type
// Exception: literal index into a tuple - will have a sound type (x4)
Code
function isArrayIndexExpression(node: TSESTree.Expression): boolean {
return (
// Is an index signature
node.type === AST_NODE_TYPES.MemberExpression &&
node.computed &&
// ...into an array type
(nodeIsArrayType(node.object) ||
// ... or a tuple type
(nodeIsTupleType(node.object) &&
// Exception: literal index into a tuple - will have a sound type
node.property.type !== AST_NODE_TYPES.Literal))
);
}
isConditionalAlwaysNecessary(type: ts.Type): boolean¶
Parameters:
typets.Type
Returns: boolean
Calls:
tsutils .unionConstituents(type) .someisTypeAnyType (from ../util)isTypeUnknownType (from ../util)isTypeFlagSet (from ../util)
Code
isNullableMemberExpression(node: TSESTree.MemberExpression): boolean¶
Parameters:
nodeTSESTree.MemberExpression
Returns: boolean
Calls:
services.getTypeAtLocationisNullablePropertyTypecontext.sourceCode.getTextobjectType .getProperties() .findtsutils.isSymbolFlagSet
Internal Comments:
Code
function isNullableMemberExpression(
node: TSESTree.MemberExpression,
): boolean {
const objectType = services.getTypeAtLocation(node.object);
if (node.computed) {
const propertyType = services.getTypeAtLocation(node.property);
return isNullablePropertyType(objectType, propertyType);
}
const property = node.property;
// Get the actual property name, to account for private properties (this.#prop).
const propertyName = context.sourceCode.getText(property);
const propertyType = objectType
.getProperties()
.find(prop => prop.name === propertyName);
if (
propertyType &&
tsutils.isSymbolFlagSet(propertyType, ts.SymbolFlags.Optional)
) {
return true;
}
return false;
}
checkNode(expression: TSESTree.Expression, isUnaryNotArgument: boolean, node: TSESTree.Expression): void¶
Checks if a conditional node is necessary: if the type of the node is always true or always false, it's not necessary.
Raw JSDoc
Calls:
checkNodeisArrayIndexExpressionisOnlyUsedForTruthinessgetConstrainedTypeAtLocation (from ../util)isConditionalAlwaysNecessaryisTypeFlagSet (from ../util)isPossiblyTruthy (from ../util)isPossiblyFalsy (from ../util)context.report
Internal Comments:
// Check if the node is Unary Negation expression and handle it
// Since typescript array index signature types don't represent the
// possibility of out-of-bounds access, if we're indexing into an array
// just skip the check, to avoid false positives
// Only check the right side when the logical expression is used for
// truthiness, as the left side has already been checked by
// checkLogicalExpressionForUnnecessaryConditionals.
//
// Unless the node is nullish coalescing, as it's common to use patterns like
// `nullBool ?? true` to perform strict boolean checks. If we inspect the right
// here, it'll usually be a constant condition on purpose.
// In this case it's better to inspect the type of the expression as a whole.
Code
function checkNode(
expression: TSESTree.Expression,
isUnaryNotArgument = false,
node = expression,
): void {
// Check if the node is Unary Negation expression and handle it
if (
expression.type === AST_NODE_TYPES.UnaryExpression &&
expression.operator === '!'
) {
return checkNode(expression.argument, !isUnaryNotArgument, node);
}
// Since typescript array index signature types don't represent the
// possibility of out-of-bounds access, if we're indexing into an array
// just skip the check, to avoid false positives
if (!isNoUncheckedIndexedAccess && isArrayIndexExpression(expression)) {
return;
}
// Only check the right side when the logical expression is used for
// truthiness, as the left side has already been checked by
// checkLogicalExpressionForUnnecessaryConditionals.
//
// Unless the node is nullish coalescing, as it's common to use patterns like
// `nullBool ?? true` to perform strict boolean checks. If we inspect the right
// here, it'll usually be a constant condition on purpose.
// In this case it's better to inspect the type of the expression as a whole.
if (
expression.type === AST_NODE_TYPES.LogicalExpression &&
expression.operator !== '??'
) {
if (isOnlyUsedForTruthiness(expression)) {
checkNode(expression.right);
}
return;
}
const type = getConstrainedTypeAtLocation(services, expression);
if (isConditionalAlwaysNecessary(type)) {
return;
}
let messageId: MessageId | null = null;
if (isTypeFlagSet(type, ts.TypeFlags.Never)) {
messageId = 'never';
} else if (!isPossiblyTruthy(type)) {
messageId = !isUnaryNotArgument ? 'alwaysFalsy' : 'alwaysTruthy';
} else if (!isPossiblyFalsy(type)) {
messageId = !isUnaryNotArgument ? 'alwaysTruthy' : 'alwaysFalsy';
}
if (messageId) {
context.report({ node, messageId });
}
}
checkNodeForNullish(node: TSESTree.Expression): void¶
Parameters:
nodeTSESTree.Expression
Returns: void
Calls:
getConstrainedTypeAtLocation (from ../util)isTypeFlagSet (from ../util)isPossiblyNullishisNullableMemberExpressionisArrayIndexExpressionoptionChainContainsOptionArrayIndexisAlwaysNullishcontext.report
Internal Comments:
// Conditional is always necessary if it involves `any`, `unknown` or a naked type parameter
// Since typescript array index signature types don't represent the
// possibility of out-of-bounds access, if we're indexing into an array
// just skip the check, to avoid false positives
Code
function checkNodeForNullish(node: TSESTree.Expression): void {
const type = getConstrainedTypeAtLocation(services, node);
// Conditional is always necessary if it involves `any`, `unknown` or a naked type parameter
if (
isTypeFlagSet(
type,
ts.TypeFlags.Any |
ts.TypeFlags.Unknown |
ts.TypeFlags.TypeParameter |
ts.TypeFlags.TypeVariable,
)
) {
return;
}
let messageId: MessageId | null = null;
if (isTypeFlagSet(type, ts.TypeFlags.Never)) {
messageId = 'never';
} else if (
!isPossiblyNullish(type) &&
!(
node.type === AST_NODE_TYPES.MemberExpression &&
isNullableMemberExpression(node)
)
) {
// Since typescript array index signature types don't represent the
// possibility of out-of-bounds access, if we're indexing into an array
// just skip the check, to avoid false positives
if (
isNoUncheckedIndexedAccess ||
(!isArrayIndexExpression(node) &&
!(
node.type === AST_NODE_TYPES.ChainExpression &&
node.expression.type !== AST_NODE_TYPES.TSNonNullExpression &&
optionChainContainsOptionArrayIndex(node.expression)
))
) {
messageId = 'neverNullish';
}
} else if (isAlwaysNullish(type)) {
messageId = 'alwaysNullish';
}
if (messageId) {
context.report({ node, messageId });
}
}
checkIfBoolExpressionIsNecessaryConditional(node: TSESTree.Node, left: TSESTree.Node, right: TSESTree.Node, operator: BoolOperator): void¶
Checks that a binary expression is necessarily conditional, reports otherwise. If both sides of the binary expression are literal values, it's not a necessary condition.
NOTE: It's also unnecessary if the types that don't overlap at all but that case is handled by the Typescript compiler itself. Known exceptions: - https://github.com/microsoft/TypeScript/issues/32627 - https://github.com/microsoft/TypeScript/issues/37160 (handled)
Raw JSDoc
/**
* Checks that a binary expression is necessarily conditional, reports otherwise.
* If both sides of the binary expression are literal values, it's not a necessary condition.
*
* NOTE: It's also unnecessary if the types that don't overlap at all
* but that case is handled by the Typescript compiler itself.
* Known exceptions:
* - https://github.com/microsoft/TypeScript/issues/32627
* - https://github.com/microsoft/TypeScript/issues/37160 (handled)
*/
Calls:
getConstrainedTypeAtLocation (from ../util)toStaticValuebooleanComparisoncontext.reportchecker.typeToStringisTypeFlagSet (from ../util)isComparable
Internal Comments:
// Workaround for https://github.com/microsoft/TypeScript/issues/37160
// Allow comparison to `any`, `unknown` or a naked type parameter. (x3)
// Allow loose comparison to nullish values.
Code
function checkIfBoolExpressionIsNecessaryConditional(
node: TSESTree.Node,
left: TSESTree.Node,
right: TSESTree.Node,
operator: BoolOperator,
): void {
const leftType = getConstrainedTypeAtLocation(services, left);
const rightType = getConstrainedTypeAtLocation(services, right);
const leftStaticValue = toStaticValue(leftType);
const rightStaticValue = toStaticValue(rightType);
if (leftStaticValue != null && rightStaticValue != null) {
const conditionIsTrue = booleanComparison(
leftStaticValue.value,
operator,
rightStaticValue.value,
);
context.report({
node,
messageId: 'comparisonBetweenLiteralTypes',
data: {
left: checker.typeToString(leftType),
operator,
right: checker.typeToString(rightType),
trueOrFalse: conditionIsTrue ? 'true' : 'false',
},
});
return;
}
// Workaround for https://github.com/microsoft/TypeScript/issues/37160
if (isStrictNullChecks) {
const UNDEFINED = ts.TypeFlags.Undefined;
const NULL = ts.TypeFlags.Null;
const VOID = ts.TypeFlags.Void;
const isComparable = (type: ts.Type, flag: ts.TypeFlags): boolean => {
// Allow comparison to `any`, `unknown` or a naked type parameter.
flag |=
ts.TypeFlags.Any |
ts.TypeFlags.Unknown |
ts.TypeFlags.TypeParameter |
ts.TypeFlags.TypeVariable;
// Allow loose comparison to nullish values.
if (operator === '==' || operator === '!=') {
flag |= NULL | UNDEFINED | VOID;
}
return isTypeFlagSet(type, flag);
};
if (
(leftType.flags === UNDEFINED &&
!isComparable(rightType, UNDEFINED | VOID)) ||
(rightType.flags === UNDEFINED &&
!isComparable(leftType, UNDEFINED | VOID)) ||
(leftType.flags === NULL && !isComparable(rightType, NULL)) ||
(rightType.flags === NULL && !isComparable(leftType, NULL))
) {
context.report({ node, messageId: 'noOverlapBooleanExpression' });
return;
}
}
}
isComparable(type: ts.Type, flag: ts.TypeFlags): boolean¶
Parameters:
typets.Typeflagts.TypeFlags
Returns: boolean
Calls:
isTypeFlagSet (from ../util)
Internal Comments:
// Allow comparison to `any`, `unknown` or a naked type parameter. (x3)
// Allow loose comparison to nullish values.
Code
(type: ts.Type, flag: ts.TypeFlags): boolean => {
// Allow comparison to `any`, `unknown` or a naked type parameter.
flag |=
ts.TypeFlags.Any |
ts.TypeFlags.Unknown |
ts.TypeFlags.TypeParameter |
ts.TypeFlags.TypeVariable;
// Allow loose comparison to nullish values.
if (operator === '==' || operator === '!=') {
flag |= NULL | UNDEFINED | VOID;
}
return isTypeFlagSet(type, flag);
}
checkLogicalExpressionForUnnecessaryConditionals(node: TSESTree.LogicalExpression): void¶
Checks that a logical expression contains a boolean, reports otherwise.
Calls:
checkNodeForNullishcheckNode
Internal Comments:
// Only checks the left side, since the right side might not be "conditional" at all. (x3)
// The right side will be checked if the LogicalExpression is used in a conditional context (x3)
Code
function checkLogicalExpressionForUnnecessaryConditionals(
node: TSESTree.LogicalExpression,
): void {
if (node.operator === '??') {
checkNodeForNullish(node.left);
return;
}
// Only checks the left side, since the right side might not be "conditional" at all.
// The right side will be checked if the LogicalExpression is used in a conditional context
checkNode(node.left);
}
checkIfLoopIsNecessaryConditional(node: | TSESTree.DoWhileStatement | TSESTree.β¦): void¶
Checks that a testable expression of a loop is necessarily conditional, reports otherwise.
Raw JSDoc
Calls:
constantLoopConditionsAllowedLiterals.hastsutils.isTrueLiteralTypegetConstrainedTypeAtLocation (from ../util)checkNode
Internal Comments:
Code
function checkIfLoopIsNecessaryConditional(
node:
| TSESTree.DoWhileStatement
| TSESTree.ForStatement
| TSESTree.WhileStatement,
): void {
if (node.test == null) {
// e.g. `for(;;)`
return;
}
if (
allowConstantLoopConditionsOption === 'only-allowed-literals' &&
node.test.type === AST_NODE_TYPES.Literal &&
constantLoopConditionsAllowedLiterals.has(node.test.value)
) {
return;
}
if (
allowConstantLoopConditionsOption === 'always' &&
tsutils.isTrueLiteralType(
getConstrainedTypeAtLocation(services, node.test),
)
) {
return;
}
checkNode(node.test);
}
checkCallExpression(node: TSESTree.CallExpression): void¶
Parameters:
nodeTSESTree.CallExpression
Returns: void
Calls:
findTruthinessAssertedArgument (from ../util/assertionFunctionUtils)checkNodefindTypeGuardAssertedArgument (from ../util/assertionFunctionUtils)getConstrainedTypeAtLocation (from ../util)tsutils.isTypeFlagSetchecker.isTypeAssignableTotypeGuardAssertedArgument.type.isUnioncontext.reportisArrayMethodCallWithPredicate (from ../util)tsutils .getCallSignaturesOfType( getConstrainedTypeAtLocation(services, callback), ) .mapsig.getReturnTypegetConstraintInfo (from ../util)isTypeAnyType (from ../util)isTypeUnknownType (from ../util)isPossiblyFalsy (from ../util)isPossiblyTruthy (from ../util)
Internal Comments:
// Skip `any` β it is assignable to everything, producing (x3)
// false positives for meaningful runtime type guards. (x3)
// Only flag if the types are mutually assignable (i.e. equivalent,
// like Narrower β Wider with optional props) or the predicate type
// is a union that the argument is a strict subtype of. This avoids
// false positives with structural subtypes whose extra members are
// all optional in the *predicate* type (e.g. custom MappedType
// interfaces extending ts.Type).
// If this is something like arr.filter(x => /*condition*/), check `condition`
// Inline defined functions
// Two special cases, where we can directly check the node that's returned:
// () => something
// () => { return something; } (x2)
// Otherwise just do type analysis on the function as a whole. (x2)
// Not a callable function, e.g. `any`
// Predicate is always necessary if it involves `any` or `unknown`
// bail early if both a possibly-truthy and a possibly-falsy have been detected
Code
function checkCallExpression(node: TSESTree.CallExpression): void {
if (checkTypePredicates) {
const truthinessAssertedArgument = findTruthinessAssertedArgument(
services,
node,
);
if (truthinessAssertedArgument != null) {
checkNode(truthinessAssertedArgument);
}
const typeGuardAssertedArgument = findTypeGuardAssertedArgument(
services,
node,
);
if (typeGuardAssertedArgument != null) {
const typeOfArgument = getConstrainedTypeAtLocation(
services,
typeGuardAssertedArgument.argument,
);
if (
// Skip `any` β it is assignable to everything, producing
// false positives for meaningful runtime type guards.
!tsutils.isTypeFlagSet(
typeOfArgument,
ts.TypeFlags.Any | ts.TypeFlags.Unknown,
) &&
checker.isTypeAssignableTo(
typeOfArgument,
typeGuardAssertedArgument.type,
) &&
// Only flag if the types are mutually assignable (i.e. equivalent,
// like Narrower β Wider with optional props) or the predicate type
// is a union that the argument is a strict subtype of. This avoids
// false positives with structural subtypes whose extra members are
// all optional in the *predicate* type (e.g. custom MappedType
// interfaces extending ts.Type).
(checker.isTypeAssignableTo(
typeGuardAssertedArgument.type,
typeOfArgument,
) ||
typeGuardAssertedArgument.type.isUnion())
) {
context.report({
node: typeGuardAssertedArgument.argument,
messageId: 'typeGuardAlreadyIsType',
data: {
typeGuardOrAssertionFunction: typeGuardAssertedArgument.asserts
? 'assertion function'
: 'type guard',
},
});
}
}
}
// If this is something like arr.filter(x => /*condition*/), check `condition`
if (
isArrayMethodCallWithPredicate(context, services, node) &&
node.arguments.length
) {
const callback = node.arguments[0];
// Inline defined functions
if (
callback.type === AST_NODE_TYPES.ArrowFunctionExpression ||
callback.type === AST_NODE_TYPES.FunctionExpression
) {
// Two special cases, where we can directly check the node that's returned:
// () => something
if (callback.body.type !== AST_NODE_TYPES.BlockStatement) {
return checkNode(callback.body);
}
// () => { return something; }
const callbackBody = callback.body.body;
if (
callbackBody.length === 1 &&
callbackBody[0].type === AST_NODE_TYPES.ReturnStatement &&
callbackBody[0].argument
) {
return checkNode(callbackBody[0].argument);
}
// Potential enhancement: could use code-path analysis to check
// any function with a single return statement
// (Value to complexity ratio is dubious however)
}
// Otherwise just do type analysis on the function as a whole.
const returnTypes = tsutils
.getCallSignaturesOfType(
getConstrainedTypeAtLocation(services, callback),
)
.map(sig => sig.getReturnType());
if (returnTypes.length === 0) {
// Not a callable function, e.g. `any`
return;
}
let hasFalsyReturnTypes = false;
let hasTruthyReturnTypes = false;
for (const type of returnTypes) {
const { constraintType } = getConstraintInfo(checker, type);
// Predicate is always necessary if it involves `any` or `unknown`
if (
!constraintType ||
isTypeAnyType(constraintType) ||
isTypeUnknownType(constraintType)
) {
return;
}
if (isPossiblyFalsy(constraintType)) {
hasFalsyReturnTypes = true;
}
if (isPossiblyTruthy(constraintType)) {
hasTruthyReturnTypes = true;
}
// bail early if both a possibly-truthy and a possibly-falsy have been detected
if (hasFalsyReturnTypes && hasTruthyReturnTypes) {
return;
}
}
if (!hasFalsyReturnTypes) {
return context.report({
node: callback,
messageId: 'alwaysTruthyFunc',
});
}
if (!hasTruthyReturnTypes) {
return context.report({
node: callback,
messageId: 'alwaysFalsyFunc',
});
}
}
}
optionChainContainsOptionArrayIndex(node: TSESTree.CallExpression | TSESTree.Membβ¦): boolean¶
Parameters:
nodeTSESTree.CallExpression | TSESTree.MemberExpression
Returns: boolean
Calls:
isArrayIndexExpressionoptionChainContainsOptionArrayIndex
Code
function optionChainContainsOptionArrayIndex(
node: TSESTree.CallExpression | TSESTree.MemberExpression,
): boolean {
const lhsNode =
node.type === AST_NODE_TYPES.CallExpression ? node.callee : node.object;
if (node.optional && isArrayIndexExpression(lhsNode)) {
return true;
}
if (
lhsNode.type === AST_NODE_TYPES.MemberExpression ||
lhsNode.type === AST_NODE_TYPES.CallExpression
) {
return optionChainContainsOptionArrayIndex(lhsNode);
}
return false;
}
isNullablePropertyType(objType: ts.Type, propertyType: ts.Type): boolean¶
Parameters:
objTypets.TypepropertyTypets.Type
Returns: boolean
Calls:
propertyType.isUnionpropertyType.types.someisNullablePropertyTypepropertyType.isNumberLiteralpropertyType.isStringLiteralgetTypeOfPropertyOfName (from ../util)propertyType.value.toStringisNullableType (from ../util)getTypeName (from ../util)checker .getIndexInfosOfType(objType) .some
Code
function isNullablePropertyType(
objType: ts.Type,
propertyType: ts.Type,
): boolean {
if (propertyType.isUnion()) {
return propertyType.types.some(type =>
isNullablePropertyType(objType, type),
);
}
if (propertyType.isNumberLiteral() || propertyType.isStringLiteral()) {
const propType = getTypeOfPropertyOfName(
checker,
objType,
propertyType.value.toString(),
);
if (propType) {
return isNullableType(propType);
}
}
const typeName = getTypeName(checker, propertyType);
return checker
.getIndexInfosOfType(objType)
.some(info => getTypeName(checker, info.keyType) === typeName);
}
isMemberExpressionNullableOriginFromObject(node: TSESTree.MemberExpression): boolean¶
Parameters:
nodeTSESTree.MemberExpression
Returns: boolean
Calls:
getConstrainedTypeAtLocation (from ../util)prevType.isUnionisIdentifier (from ../util)prevType.types.someisNullablePropertyTypegetTypeOfPropertyOfName (from ../util)isNullableType (from ../util)checker.getIndexInfosOfTypeindexInfo.somegetTypeName (from ../util)
Code
function isMemberExpressionNullableOriginFromObject(
node: TSESTree.MemberExpression,
): boolean {
const prevType = getConstrainedTypeAtLocation(services, node.object);
const property = node.property;
if (prevType.isUnion() && isIdentifier(property)) {
const isOwnNullable = prevType.types.some(type => {
if (node.computed) {
const propertyType = getConstrainedTypeAtLocation(
services,
node.property,
);
return isNullablePropertyType(type, propertyType);
}
const propType = getTypeOfPropertyOfName(
checker,
type,
property.name,
);
if (propType) {
return isNullableType(propType);
}
const indexInfo = checker.getIndexInfosOfType(type);
return indexInfo.some(info => {
const isStringTypeName =
getTypeName(checker, info.keyType) === 'string';
return (
isStringTypeName &&
(isNoUncheckedIndexedAccess || isNullableType(info.type))
);
});
});
return !isOwnNullable && isNullableType(prevType);
}
return false;
}
isCallExpressionNullableOriginFromCallee(node: TSESTree.CallExpression): boolean¶
Parameters:
nodeTSESTree.CallExpression
Returns: boolean
Calls:
getConstrainedTypeAtLocation (from ../util)prevType.isUnionprevType.types.sometype.getCallSignaturessignatures.someisNullableType (from ../util)sig.getReturnType
Code
function isCallExpressionNullableOriginFromCallee(
node: TSESTree.CallExpression,
): boolean {
const prevType = getConstrainedTypeAtLocation(services, node.callee);
if (prevType.isUnion()) {
const isOwnNullable = prevType.types.some(type => {
const signatures = type.getCallSignatures();
return signatures.some(sig => isNullableType(sig.getReturnType()));
});
return !isOwnNullable && isNullableType(prevType);
}
return false;
}
isOptionableExpression(node: TSESTree.Expression): boolean¶
Parameters:
nodeTSESTree.Expression
Returns: boolean
Calls:
getConstrainedTypeAtLocation (from ../util)isMemberExpressionNullableOriginFromObjectisCallExpressionNullableOriginFromCalleeisConditionalAlwaysNecessaryisNullableType (from ../util)
Code
function isOptionableExpression(node: TSESTree.Expression): boolean {
const type = getConstrainedTypeAtLocation(services, node);
const isOwnNullable =
node.type === AST_NODE_TYPES.MemberExpression
? !isMemberExpressionNullableOriginFromObject(node)
: node.type === AST_NODE_TYPES.CallExpression
? !isCallExpressionNullableOriginFromCallee(node)
: true;
return (
isConditionalAlwaysNecessary(type) ||
(isOwnNullable && isNullableType(type))
);
}
checkOptionalChain(node: TSESTree.CallExpression | TSESTree.Membβ¦, beforeOperator: TSESTree.Node, fix: '' | '.'): void¶
Parameters:
nodeTSESTree.CallExpression | TSESTree.MemberExpressionbeforeOperatorTSESTree.Nodefix'' | '.'
Returns: void
Calls:
optionChainContainsOptionArrayIndexisOptionableExpressionnullThrows (from ../util)context.sourceCode.getTokenAfterNullThrowsReasons.MissingTokencontext.reportfixer.replaceText
Internal Comments:
// We only care if this step in the chain is optional. If just descend
// from an optional chain, then that's fine.
// Since typescript array index signature types don't represent the
// possibility of out-of-bounds access, if we're indexing into an array
// just skip the check, to avoid false positives
Code
function checkOptionalChain(
node: TSESTree.CallExpression | TSESTree.MemberExpression,
beforeOperator: TSESTree.Node,
fix: '' | '.',
): void {
// We only care if this step in the chain is optional. If just descend
// from an optional chain, then that's fine.
if (!node.optional) {
return;
}
// Since typescript array index signature types don't represent the
// possibility of out-of-bounds access, if we're indexing into an array
// just skip the check, to avoid false positives
if (
!isNoUncheckedIndexedAccess &&
optionChainContainsOptionArrayIndex(node)
) {
return;
}
const nodeToCheck =
node.type === AST_NODE_TYPES.CallExpression ? node.callee : node.object;
if (isOptionableExpression(nodeToCheck)) {
return;
}
const questionDotOperator = nullThrows(
context.sourceCode.getTokenAfter(
beforeOperator,
token =>
token.type === AST_TOKEN_TYPES.Punctuator && token.value === '?.',
),
NullThrowsReasons.MissingToken('operator', node.type),
);
context.report({
loc: questionDotOperator.loc,
node,
messageId: 'neverOptionalChain',
suggest: [
{
messageId: 'suggestRemoveOptionalChain',
fix(fixer) {
return fixer.replaceText(questionDotOperator, fix);
},
},
],
});
}
checkOptionalMemberExpression(node: TSESTree.MemberExpression): void¶
Parameters:
nodeTSESTree.MemberExpression
Returns: void
Calls:
checkOptionalChain
Code
checkOptionalCallExpression(node: TSESTree.CallExpression): void¶
Parameters:
nodeTSESTree.CallExpression
Returns: void
Calls:
checkOptionalChain
Code
checkAssignmentExpression(node: TSESTree.AssignmentExpression): void¶
Parameters:
nodeTSESTree.AssignmentExpression
Returns: void
Calls:
['&&=', '||='].includescheckNodecheckNodeForNullish
Internal Comments:
// Similar to checkLogicalExpressionForUnnecessaryConditionals, since
// a ||= b is equivalent to a || (a = b)
Code
function checkAssignmentExpression(
node: TSESTree.AssignmentExpression,
): void {
// Similar to checkLogicalExpressionForUnnecessaryConditionals, since
// a ||= b is equivalent to a || (a = b)
if (['&&=', '||='].includes(node.operator)) {
checkNode(node.left);
} else if (node.operator === '??=') {
checkNodeForNullish(node.left);
}
}
Type Aliases¶
BoolOperator¶
LegacyAllowConstantLoopConditions¶
AllowConstantLoopConditions¶
Options¶
type Options = [
{
allowConstantLoopConditions?:
AllowConstantLoopConditions | LegacyAllowConstantLoopConditions;
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean;
checkTypePredicates?: boolean;
},
];
MessageId¶
type MessageId = | 'alwaysFalsy'
| 'alwaysFalsyFunc'
| 'alwaysNullish'
| 'alwaysTruthy'
| 'alwaysTruthyFunc'
| 'comparisonBetweenLiteralTypes'
| 'never'
| 'neverNullish'
| 'neverOptionalChain'
| 'noOverlapBooleanExpression'
| 'noStrictNullCheck'
| 'suggestRemoveOptionalChain'
| 'typeGuardAlreadyIsType';
Generated by Syntax Scribe