📄 strict-boolean-expressions¶
📊 Analysis Summary¶
| Metric | Count |
|---|---|
| 🔧 Functions | 17 |
| 📦 Imports | 14 |
| 📑 Type Aliases | 5 |
📚 Table of Contents¶
🛠️ File Location:¶
📂 packages/eslint-plugin/src/rules/strict-boolean-expressions.ts
📤 Default Export¶
| Property | Value |
|---|---|
name |
'strict-boolean-expressions' |
meta.type |
'suggestion' |
meta.docs.description |
'Disallow certain types in boolean expressions' |
meta.docs.requiresTypeChecking |
true |
meta.hasSuggestions |
true |
meta.messages.conditionErrorAny |
'Unexpected any value in {{context}}. ' + 'An explicit comparison or type conversion is required.' |
meta.messages.conditionErrorNullableBoolean |
'Unexpected nullable boolean value in {{context}}. ' + 'Please handle the nullish case explicitly.' |
meta.messages.conditionErrorNullableEnum |
'Unexpected nullable enum value in {{context}}. ' + 'Please handle the nullish/zero/NaN cases explicitly.' |
meta.messages.conditionErrorNullableNumber |
'Unexpected nullable number value in {{context}}. ' + 'Please handle the nullish/zero/NaN cases explicitly.' |
meta.messages.conditionErrorNullableObject |
'Unexpected nullable object value in {{context}}. ' + 'An explicit null check is required.' |
meta.messages.conditionErrorNullableString |
'Unexpected nullable string value in {{context}}. ' + 'Please handle the nullish/empty cases explicitly.' |
meta.messages.conditionErrorNullish |
'Unexpected nullish value in conditional. ' + 'The condition is always false.' |
meta.messages.conditionErrorNumber |
'Unexpected number value in {{context}}. ' + 'An explicit zero/NaN check is required.' |
meta.messages.conditionErrorObject |
'Unexpected object value in {{context}}. ' + 'The condition is always true.' |
meta.messages.conditionErrorOther |
'Unexpected value in conditional. ' + 'A boolean expression is required.' |
meta.messages.conditionErrorString |
'Unexpected string value in {{context}}. ' + 'An explicit empty string check is required.' |
meta.messages.conditionFixCastBoolean |
'Explicitly convert value to a boolean (Boolean(value))' |
meta.messages.conditionFixCompareArrayLengthNonzero |
"Change condition to check array's length (value.length > 0)" |
meta.messages.conditionFixCompareArrayLengthZero |
"Change condition to check array's length (value.length === 0)" |
meta.messages.conditionFixCompareEmptyString |
'Change condition to check for empty string (value !== "")' |
meta.messages.conditionFixCompareFalse |
'Change condition to check if false (value === false)' |
meta.messages.conditionFixCompareNaN |
'Change condition to check for NaN (!Number.isNaN(value))' |
meta.messages.conditionFixCompareNullish |
'Change condition to check for null/undefined (value != null)' |
meta.messages.conditionFixCompareStringLength |
"Change condition to check string's length (value.length !== 0)" |
meta.messages.conditionFixCompareTrue |
'Change condition to check if true (value === true)' |
meta.messages.conditionFixCompareZero |
'Change condition to check for 0 (value !== 0)' |
meta.messages.conditionFixDefaultEmptyString |
'Explicitly treat nullish value the same as an empty string (value ?? "")' |
meta.messages.conditionFixDefaultFalse |
'Explicitly treat nullish value the same as false (value ?? false)' |
meta.messages.conditionFixDefaultZero |
'Explicitly treat nullish value the same as 0 (value ?? 0)' |
meta.messages.explicitBooleanReturnType |
'Add an explicit boolean return type annotation.' |
meta.messages.noStrictNullCheck |
'This rule requires the strictNullChecks compiler option to be turned on to function correctly.' |
meta.messages.predicateCannotBeAsync |
"Predicate function should not be 'async'; expected a boolean return type." |
meta.schema |
[ { type: 'object', additionalProperties: false, properties: { allowAny: { type: 'boolean', description: 'Whether to ... |
defaultOptions |
[ { allowAny: false, allowNullableBoolean: false, allowNullableEnum: false, allowNullableNumber: false, allowNullable... |
Entry point: create — documented under Functions.
📦 Imports¶
| Name | Source |
|---|---|
ParserServicesWithTypeInformation |
@typescript-eslint/utils |
TSESTree |
@typescript-eslint/utils |
ReportSuggestionArray |
@typescript-eslint/utils/ts-eslint |
AST_NODE_TYPES |
@typescript-eslint/utils |
ASTUtils |
@typescript-eslint/utils |
createRule |
../util |
getConstrainedTypeAtLocation |
../util |
getParserServices |
../util |
getWrappingFixer |
../util |
isArrayMethodCallWithPredicate |
../util |
isParenlessArrowFunction |
../util |
isTypeArrayTypeOrUnionOfArrayTypes |
../util |
nullThrows |
../util |
findTruthinessAssertedArgument |
../util/assertionFunctionUtils |
Functions¶
create(context: any, [options]: any): { CallExpression: (node: TSESTree.CallExpression) => void; …¶
Parameters:
contextany[options]any
Returns: { CallExpression: (node: TSESTree.CallExpression) => void; ConditionalExpression: (node: any) => void; DoWhileStatement: (node: any) => void; ForStatement: (node: any) => void; IfStatement: (node: any) => void; 'LogicalExpression[operator!="??"]': (node: TSESTree.LogicalExpression, isCondition?: boolean) => void; 'UnaryExpression[operator="!"]': (node: TSESTree.UnaryExpression) => void; WhileStatement: (node: any) => void; }
Calls:
getParserServices (from ../util)services.program.getTypeCheckerservices.program.getCompilerOptionstsutils.isStrictCompilerOptionEnabledcontext.reporttraverseNodefindTruthinessAssertedArgument (from ../util/assertionFunctionUtils)isArrayMethodCallWithPredicate (from ../util)node.arguments.atcheckArrayMethodCallPredicateASTUtils.isFunctionservices .getTypeAtLocation(predicateNode) .getCallSignatures() .mapsignature.getReturnTypetsutils.isTypeParameterchecker.getBaseConstraintOfTypereturnTypes.flatMaptsutils.unionConstituentsinspectVariantTypesdetermineReportTypesuggestions.pushgetSuggestionsForConditionErrorisParenlessArrowFunction (from ../util)fixer.insertTextBeforefixer.insertTextAfternullThrows (from ../util)context.sourceCode.getFirstTokencontext.sourceCode.getTokenAftertraversedNodes.hastraversedNodes.addtraverseLogicalExpressioncheckNodewantedTypes.everytypes.hasisgetWrappingFixer (from ../util)isLogicalNegationExpressionisArrayLengthExpressiongetConstrainedTypeAtLocation (from ../util)types.sometsutils.isTypeFlagSetvariantTypes.addtypes.filtertsutils.isTrueLiteralTypestrings.everytype.isStringLiteralnumbers.everytype.isNumberLiteral
Internal Comments:
/**
* Inspects condition of a test expression. (`if`, `while`, `for`, etc.)
*/
/**
* Inspects the argument of a unary logical expression (`!`).
*/
/**
* Inspects the arguments of a logical expression (`&&`, `||`).
*
* If the logical expression is a descendant of a test expression,
* the `isCondition` flag should be set to true.
* Otherwise, if the logical expression is there on it's own,
* it's used for control flow and is not a condition itself.
*/
// left argument is always treated as a condition (x3)
// if the logical expression is used for control flow, (x3)
// then its right argument is used for its side effects only (x3)
/**
* Dedicated function to check array method predicate calls. Reports predicate
* arguments that don't return a boolean value.
*/
// custom message for accidental `async` function expressions
/**
* Inspects any node.
*
* If it's a logical expression then it recursively traverses its arguments.
* If it's any other kind of node then it's type is finally checked against the rule,
* unless `isCondition` flag is set to false, in which case
* it's assumed to be used for side effects only and is skipped.
*/
// prevent checking the same node multiple times
// for logical operator, we check its operands
// skip if node is not a condition
// boolean
// boolean is always ok
// never
// never is always okay
// nullish
// condition is always false
// Known edge case: boolean `true` and nullish values are always valid boolean expressions
// nullable boolean
// Known edge case: truthy primitives and nullish values are always valid boolean expressions
// string
// nullable string
// number
// nullable number
// object
// nullable object
// nullable enum
// mixed enums (x2)
// any
// if (!nullableBoolean)
// if (nullableBoolean)
// if (!nullableNumber)
// if (nullableNumber)
// if (!nullableObject)
// if (nullableObject)
// if (!nullableString)
// if (nullableString)
// if (!array.length)
// if (array.length)
// if (!number)
// TODO: we have to compare to 0n if the type is bigint (x2)
// TODO: don't suggest this for bigint because it can't be NaN (x2)
// if (number)
// if (!string)
// if (string)
/**
* This function does the actual type check on a node.
* It analyzes the type of a node and checks if it is allowed in a boolean context.
*/
/** The types we care about */
/**
* Check union variants for the types we care about
*/
// If incoming type is either "true" or "false", there will be one type
// object with intrinsicName set accordingly
// If incoming type is boolean, there will be two type objects with
// intrinsicName set "true" and "false" each because of ts-api-utils.unionConstituents()
Code
create(context, [options]) {
const services = getParserServices(context);
const checker = services.program.getTypeChecker();
const compilerOptions = services.program.getCompilerOptions();
const isStrictNullChecks = tsutils.isStrictCompilerOptionEnabled(
compilerOptions,
'strictNullChecks',
);
if (
!isStrictNullChecks &&
options.allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing !== true
) {
context.report({
loc: {
start: { column: 0, line: 0 },
end: { column: 0, line: 0 },
},
messageId: 'noStrictNullCheck',
});
}
const traversedNodes = new Set<TSESTree.Node>();
return {
CallExpression: traverseCallExpression,
ConditionalExpression: traverseTestExpression,
DoWhileStatement: traverseTestExpression,
ForStatement: traverseTestExpression,
IfStatement: traverseTestExpression,
'LogicalExpression[operator!="??"]': traverseLogicalExpression,
'UnaryExpression[operator="!"]': traverseUnaryLogicalExpression,
WhileStatement: traverseTestExpression,
};
type TestExpression =
| TSESTree.ConditionalExpression
| TSESTree.DoWhileStatement
| TSESTree.ForStatement
| TSESTree.IfStatement
| TSESTree.WhileStatement;
/**
* Inspects condition of a test expression. (`if`, `while`, `for`, etc.)
*/
function traverseTestExpression(node: TestExpression): void {
if (node.test == null) {
return;
}
traverseNode(node.test, true);
}
/**
* Inspects the argument of a unary logical expression (`!`).
*/
function traverseUnaryLogicalExpression(
node: TSESTree.UnaryExpression,
): void {
traverseNode(node.argument, true);
}
/**
* Inspects the arguments of a logical expression (`&&`, `||`).
*
* If the logical expression is a descendant of a test expression,
* the `isCondition` flag should be set to true.
* Otherwise, if the logical expression is there on it's own,
* it's used for control flow and is not a condition itself.
*/
function traverseLogicalExpression(
node: TSESTree.LogicalExpression,
isCondition = false,
): void {
// left argument is always treated as a condition
traverseNode(node.left, true);
// if the logical expression is used for control flow,
// then its right argument is used for its side effects only
traverseNode(node.right, isCondition);
}
function traverseCallExpression(node: TSESTree.CallExpression): void {
const assertedArgument = findTruthinessAssertedArgument(services, node);
if (assertedArgument != null) {
traverseNode(assertedArgument, true);
}
if (isArrayMethodCallWithPredicate(context, services, node)) {
const predicate = node.arguments.at(0);
if (predicate) {
checkArrayMethodCallPredicate(predicate);
}
}
}
/**
* Dedicated function to check array method predicate calls. Reports predicate
* arguments that don't return a boolean value.
*/
function checkArrayMethodCallPredicate(
predicateNode: TSESTree.CallExpressionArgument,
): void {
const isFunctionExpression = ASTUtils.isFunction(predicateNode);
// custom message for accidental `async` function expressions
if (isFunctionExpression && predicateNode.async) {
return context.report({
node: predicateNode,
messageId: 'predicateCannotBeAsync',
});
}
const returnTypes = services
.getTypeAtLocation(predicateNode)
.getCallSignatures()
.map(signature => {
const type = signature.getReturnType();
if (tsutils.isTypeParameter(type)) {
return checker.getBaseConstraintOfType(type) ?? type;
}
return type;
});
const flattenTypes = [
...new Set(
returnTypes.flatMap(type => tsutils.unionConstituents(type)),
),
];
const types = inspectVariantTypes(flattenTypes);
const reportType = determineReportType(types);
if (reportType == null) {
return;
}
const suggestions: ReportSuggestionArray<MessageId> = [];
if (
isFunctionExpression &&
predicateNode.body.type !== AST_NODE_TYPES.BlockStatement
) {
suggestions.push(
...getSuggestionsForConditionError(predicateNode.body, reportType),
);
}
if (isFunctionExpression && !predicateNode.returnType) {
suggestions.push({
messageId: 'explicitBooleanReturnType',
fix: fixer => {
if (
predicateNode.type === AST_NODE_TYPES.ArrowFunctionExpression &&
isParenlessArrowFunction(predicateNode, context.sourceCode)
) {
return [
fixer.insertTextBefore(predicateNode.params[0], '('),
fixer.insertTextAfter(predicateNode.params[0], '): boolean'),
];
}
if (predicateNode.params.length === 0) {
const closingBracket = nullThrows(
context.sourceCode.getFirstToken(
predicateNode,
token => token.value === ')',
),
'function expression has to have a closing parenthesis.',
);
return fixer.insertTextAfter(closingBracket, ': boolean');
}
const lastClosingParenthesis = nullThrows(
context.sourceCode.getTokenAfter(
predicateNode.params[predicateNode.params.length - 1],
token => token.value === ')',
),
'function expression has to have a closing parenthesis.',
);
return fixer.insertTextAfter(lastClosingParenthesis, ': boolean');
},
});
}
return context.report({
node: predicateNode,
messageId: reportType,
data: {
context: 'array predicate return type',
},
suggest: suggestions,
});
}
/**
* Inspects any node.
*
* If it's a logical expression then it recursively traverses its arguments.
* If it's any other kind of node then it's type is finally checked against the rule,
* unless `isCondition` flag is set to false, in which case
* it's assumed to be used for side effects only and is skipped.
*/
function traverseNode(
node: TSESTree.Expression,
isCondition: boolean,
): void {
// prevent checking the same node multiple times
if (traversedNodes.has(node)) {
return;
}
traversedNodes.add(node);
// for logical operator, we check its operands
if (
node.type === AST_NODE_TYPES.LogicalExpression &&
node.operator !== '??'
) {
traverseLogicalExpression(node, isCondition);
return;
}
// skip if node is not a condition
if (!isCondition) {
return;
}
checkNode(node);
}
function determineReportType(
types: Set<VariantType>,
): ConditionErrorMessageId | undefined {
const is = (...wantedTypes: readonly VariantType[]): boolean =>
types.size === wantedTypes.length &&
wantedTypes.every(type => types.has(type));
// boolean
if (is('boolean') || is('truthy boolean')) {
// boolean is always ok
return undefined;
}
// never
if (is('never')) {
// never is always okay
return undefined;
}
// nullish
if (is('nullish')) {
// condition is always false
return 'conditionErrorNullish';
}
// Known edge case: boolean `true` and nullish values are always valid boolean expressions
if (is('nullish', 'truthy boolean')) {
return;
}
// nullable boolean
if (is('nullish', 'boolean')) {
return !options.allowNullableBoolean
? 'conditionErrorNullableBoolean'
: undefined;
}
// Known edge case: truthy primitives and nullish values are always valid boolean expressions
if (
(options.allowNumber && is('nullish', 'truthy number')) ||
(options.allowString && is('nullish', 'truthy string'))
) {
return;
}
// string
if (is('string') || is('truthy string')) {
return !options.allowString ? 'conditionErrorString' : undefined;
}
// nullable string
if (is('nullish', 'string')) {
return !options.allowNullableString
? 'conditionErrorNullableString'
: undefined;
}
// number
if (is('number') || is('truthy number')) {
return !options.allowNumber ? 'conditionErrorNumber' : undefined;
}
// nullable number
if (is('nullish', 'number')) {
return !options.allowNullableNumber
? 'conditionErrorNullableNumber'
: undefined;
}
// object
if (is('object')) {
return 'conditionErrorObject';
}
// nullable object
if (is('nullish', 'object')) {
return !options.allowNullableObject
? 'conditionErrorNullableObject'
: undefined;
}
// nullable enum
if (
is('nullish', 'number', 'enum') ||
is('nullish', 'string', 'enum') ||
is('nullish', 'truthy number', 'enum') ||
is('nullish', 'truthy string', 'enum') ||
// mixed enums
is('nullish', 'truthy number', 'truthy string', 'enum') ||
is('nullish', 'truthy number', 'string', 'enum') ||
is('nullish', 'truthy string', 'number', 'enum') ||
is('nullish', 'number', 'string', 'enum')
) {
return !options.allowNullableEnum
? 'conditionErrorNullableEnum'
: undefined;
}
// any
if (is('any')) {
return !options.allowAny ? 'conditionErrorAny' : undefined;
}
return 'conditionErrorOther';
}
function getSuggestionsForConditionError(
node: TSESTree.Expression,
conditionError: ConditionErrorMessageId,
): ReportSuggestionArray<MessageId> {
switch (conditionError) {
case 'conditionErrorAny':
return [
{
messageId: 'conditionFixCastBoolean',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `Boolean(${code})`,
}),
},
];
case 'conditionErrorNullableBoolean':
if (isLogicalNegationExpression(node.parent)) {
// if (!nullableBoolean)
return [
{
messageId: 'conditionFixDefaultFalse',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} ?? false`,
}),
},
{
messageId: 'conditionFixCompareFalse',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `${code} === false`,
}),
},
];
}
// if (nullableBoolean)
return [
{
messageId: 'conditionFixDefaultFalse',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} ?? false`,
}),
},
{
messageId: 'conditionFixCompareTrue',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} === true`,
}),
},
];
case 'conditionErrorNullableEnum':
if (isLogicalNegationExpression(node.parent)) {
return [
{
messageId: 'conditionFixCompareNullish',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `${code} == null`,
}),
},
];
}
return [
{
messageId: 'conditionFixCompareNullish',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} != null`,
}),
},
];
case 'conditionErrorNullableNumber':
if (isLogicalNegationExpression(node.parent)) {
// if (!nullableNumber)
return [
{
messageId: 'conditionFixCompareNullish',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `${code} == null`,
}),
},
{
messageId: 'conditionFixDefaultZero',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} ?? 0`,
}),
},
{
messageId: 'conditionFixCastBoolean',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `!Boolean(${code})`,
}),
},
];
}
// if (nullableNumber)
return [
{
messageId: 'conditionFixCompareNullish',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} != null`,
}),
},
{
messageId: 'conditionFixDefaultZero',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} ?? 0`,
}),
},
{
messageId: 'conditionFixCastBoolean',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `Boolean(${code})`,
}),
},
];
case 'conditionErrorNullableObject':
if (isLogicalNegationExpression(node.parent)) {
// if (!nullableObject)
return [
{
messageId: 'conditionFixCompareNullish',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `${code} == null`,
}),
},
];
}
// if (nullableObject)
return [
{
messageId: 'conditionFixCompareNullish',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} != null`,
}),
},
];
case 'conditionErrorNullableString':
if (isLogicalNegationExpression(node.parent)) {
// if (!nullableString)
return [
{
messageId: 'conditionFixCompareNullish',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `${code} == null`,
}),
},
{
messageId: 'conditionFixDefaultEmptyString',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} ?? ""`,
}),
},
{
messageId: 'conditionFixCastBoolean',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `!Boolean(${code})`,
}),
},
];
}
// if (nullableString)
return [
{
messageId: 'conditionFixCompareNullish',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} != null`,
}),
},
{
messageId: 'conditionFixDefaultEmptyString',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} ?? ""`,
}),
},
{
messageId: 'conditionFixCastBoolean',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `Boolean(${code})`,
}),
},
];
case 'conditionErrorNumber':
if (isArrayLengthExpression(node, checker, services)) {
if (isLogicalNegationExpression(node.parent)) {
// if (!array.length)
return [
{
messageId: 'conditionFixCompareArrayLengthZero',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `${code} === 0`,
}),
},
];
}
// if (array.length)
return [
{
messageId: 'conditionFixCompareArrayLengthNonzero',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} > 0`,
}),
},
];
}
if (isLogicalNegationExpression(node.parent)) {
// if (!number)
return [
{
messageId: 'conditionFixCompareZero',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
// TODO: we have to compare to 0n if the type is bigint
wrap: code => `${code} === 0`,
}),
},
{
// TODO: don't suggest this for bigint because it can't be NaN
messageId: 'conditionFixCompareNaN',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `Number.isNaN(${code})`,
}),
},
{
messageId: 'conditionFixCastBoolean',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `!Boolean(${code})`,
}),
},
];
}
// if (number)
return [
{
messageId: 'conditionFixCompareZero',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} !== 0`,
}),
},
{
messageId: 'conditionFixCompareNaN',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `!Number.isNaN(${code})`,
}),
},
{
messageId: 'conditionFixCastBoolean',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `Boolean(${code})`,
}),
},
];
case 'conditionErrorString':
if (isLogicalNegationExpression(node.parent)) {
// if (!string)
return [
{
messageId: 'conditionFixCompareStringLength',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `${code}.length === 0`,
}),
},
{
messageId: 'conditionFixCompareEmptyString',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `${code} === ""`,
}),
},
{
messageId: 'conditionFixCastBoolean',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `!Boolean(${code})`,
}),
},
];
}
// if (string)
return [
{
messageId: 'conditionFixCompareStringLength',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code}.length > 0`,
}),
},
{
messageId: 'conditionFixCompareEmptyString',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} !== ""`,
}),
},
{
messageId: 'conditionFixCastBoolean',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `Boolean(${code})`,
}),
},
];
case 'conditionErrorObject':
case 'conditionErrorNullish':
case 'conditionErrorOther':
return [];
default:
conditionError satisfies never;
throw new Error('Unreachable');
}
}
/**
* This function does the actual type check on a node.
* It analyzes the type of a node and checks if it is allowed in a boolean context.
*/
function checkNode(node: TSESTree.Expression): void {
const type = getConstrainedTypeAtLocation(services, node);
const types = inspectVariantTypes(tsutils.unionConstituents(type));
const reportType = determineReportType(types);
if (reportType != null) {
context.report({
node,
messageId: reportType,
data: {
context: 'conditional',
},
suggest: getSuggestionsForConditionError(node, reportType),
});
}
}
/** The types we care about */
type VariantType =
| 'any'
| 'boolean'
| 'enum'
| 'never'
| 'nullish'
| 'number'
| 'object'
| 'string'
| 'truthy boolean'
| 'truthy number'
| 'truthy string';
/**
* Check union variants for the types we care about
*/
function inspectVariantTypes(types: ts.Type[]): Set<VariantType> {
const variantTypes = new Set<VariantType>();
if (
types.some(type =>
tsutils.isTypeFlagSet(
type,
ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.VoidLike,
),
)
) {
variantTypes.add('nullish');
}
const booleans = types.filter(type =>
tsutils.isTypeFlagSet(type, ts.TypeFlags.BooleanLike),
);
// If incoming type is either "true" or "false", there will be one type
// object with intrinsicName set accordingly
// If incoming type is boolean, there will be two type objects with
// intrinsicName set "true" and "false" each because of ts-api-utils.unionConstituents()
if (booleans.length === 1) {
variantTypes.add(
tsutils.isTrueLiteralType(booleans[0]) ? 'truthy boolean' : 'boolean',
);
} else if (booleans.length === 2) {
variantTypes.add('boolean');
}
const strings = types.filter(type =>
tsutils.isTypeFlagSet(type, ts.TypeFlags.StringLike),
);
if (strings.length) {
if (
strings.every(type => type.isStringLiteral() && type.value !== '')
) {
variantTypes.add('truthy string');
} else {
variantTypes.add('string');
}
}
const numbers = types.filter(type =>
tsutils.isTypeFlagSet(
type,
ts.TypeFlags.NumberLike | ts.TypeFlags.BigIntLike,
),
);
if (numbers.length) {
if (numbers.every(type => type.isNumberLiteral() && type.value !== 0)) {
variantTypes.add('truthy number');
} else {
variantTypes.add('number');
}
}
if (
types.some(type => tsutils.isTypeFlagSet(type, ts.TypeFlags.EnumLike))
) {
variantTypes.add('enum');
}
if (
types.some(
type =>
!tsutils.isTypeFlagSet(
type,
ts.TypeFlags.Null |
ts.TypeFlags.Undefined |
ts.TypeFlags.VoidLike |
ts.TypeFlags.BooleanLike |
ts.TypeFlags.StringLike |
ts.TypeFlags.NumberLike |
ts.TypeFlags.BigIntLike |
ts.TypeFlags.TypeParameter |
ts.TypeFlags.Any |
ts.TypeFlags.Unknown |
ts.TypeFlags.Never,
),
)
) {
variantTypes.add(types.some(isBrandedBoolean) ? 'boolean' : 'object');
}
if (
types.some(type =>
tsutils.isTypeFlagSet(
type,
ts.TypeFlags.TypeParameter |
ts.TypeFlags.Any |
ts.TypeFlags.Unknown,
),
)
) {
variantTypes.add('any');
}
if (types.some(type => tsutils.isTypeFlagSet(type, ts.TypeFlags.Never))) {
variantTypes.add('never');
}
return variantTypes;
}
}
isLogicalNegationExpression(node: TSESTree.Node): node is TSESTree.UnaryExpression¶
Parameters:
nodeTSESTree.Node
Returns: node is TSESTree.UnaryExpression
Code
isArrayLengthExpression(node: TSESTree.Node, typeChecker: ts.TypeChecker, services: ParserServicesWithTypeInformation): node is TSESTree.MemberExpressionNonComputedName¶
Parameters:
nodeTSESTree.NodetypeCheckerts.TypeCheckerservicesParserServicesWithTypeInformation
Returns: node is TSESTree.MemberExpressionNonComputedName
Calls:
getConstrainedTypeAtLocation (from ../util)isTypeArrayTypeOrUnionOfArrayTypes (from ../util)
Code
function isArrayLengthExpression(
node: TSESTree.Node,
typeChecker: ts.TypeChecker,
services: ParserServicesWithTypeInformation,
): node is TSESTree.MemberExpressionNonComputedName {
if (node.type !== AST_NODE_TYPES.MemberExpression) {
return false;
}
if (node.computed) {
return false;
}
if (node.property.name !== 'length') {
return false;
}
const objectType = getConstrainedTypeAtLocation(services, node.object);
return isTypeArrayTypeOrUnionOfArrayTypes(objectType, typeChecker);
}
isBrandedBoolean(type: ts.Type): boolean¶
Verify is the type is a branded boolean (e.g. type Foo = boolean & { __brand: 'Foo' })
Parameters:
typeany: The type checked
Raw JSDoc
Calls:
type.isIntersectiontype.types.someisBooleanType
Code
isBooleanType(expressionType: ts.Type): boolean¶
Parameters:
expressionTypets.Type
Returns: boolean
Calls:
tsutils.isTypeFlagSet
Code
Internal helpers¶
Declared inside another function in this file.
traverseTestExpression(node: TestExpression): void¶
Inspects condition of a test expression. (if, while, for, etc.)
Calls:
traverseNode
Code
traverseUnaryLogicalExpression(node: TSESTree.UnaryExpression): void¶
Inspects the argument of a unary logical expression (!).
Calls:
traverseNode
Code
traverseLogicalExpression(node: TSESTree.LogicalExpression, isCondition: boolean): void¶
Inspects the arguments of a logical expression (&&, ||).
If the logical expression is a descendant of a test expression,
the isCondition flag should be set to true.
Otherwise, if the logical expression is there on it's own,
it's used for control flow and is not a condition itself.
Raw JSDoc
/**
* Inspects the arguments of a logical expression (`&&`, `||`).
*
* If the logical expression is a descendant of a test expression,
* the `isCondition` flag should be set to true.
* Otherwise, if the logical expression is there on it's own,
* it's used for control flow and is not a condition itself.
*/
Calls:
traverseNode
Internal Comments:
// left argument is always treated as a condition (x3)
// if the logical expression is used for control flow, (x3)
// then its right argument is used for its side effects only (x3)
Code
function traverseLogicalExpression(
node: TSESTree.LogicalExpression,
isCondition = false,
): void {
// left argument is always treated as a condition
traverseNode(node.left, true);
// if the logical expression is used for control flow,
// then its right argument is used for its side effects only
traverseNode(node.right, isCondition);
}
traverseCallExpression(node: TSESTree.CallExpression): void¶
Parameters:
nodeTSESTree.CallExpression
Returns: void
Calls:
findTruthinessAssertedArgument (from ../util/assertionFunctionUtils)traverseNodeisArrayMethodCallWithPredicate (from ../util)node.arguments.atcheckArrayMethodCallPredicate
Code
function traverseCallExpression(node: TSESTree.CallExpression): void {
const assertedArgument = findTruthinessAssertedArgument(services, node);
if (assertedArgument != null) {
traverseNode(assertedArgument, true);
}
if (isArrayMethodCallWithPredicate(context, services, node)) {
const predicate = node.arguments.at(0);
if (predicate) {
checkArrayMethodCallPredicate(predicate);
}
}
}
checkArrayMethodCallPredicate(predicateNode: TSESTree.CallExpressionArgument): void¶
Dedicated function to check array method predicate calls. Reports predicate arguments that don't return a boolean value.
Raw JSDoc
Calls:
ASTUtils.isFunctioncontext.reportservices .getTypeAtLocation(predicateNode) .getCallSignatures() .mapsignature.getReturnTypetsutils.isTypeParameterchecker.getBaseConstraintOfTypereturnTypes.flatMaptsutils.unionConstituentsinspectVariantTypesdetermineReportTypesuggestions.pushgetSuggestionsForConditionErrorisParenlessArrowFunction (from ../util)fixer.insertTextBeforefixer.insertTextAfternullThrows (from ../util)context.sourceCode.getFirstTokencontext.sourceCode.getTokenAfter
Internal Comments:
Code
function checkArrayMethodCallPredicate(
predicateNode: TSESTree.CallExpressionArgument,
): void {
const isFunctionExpression = ASTUtils.isFunction(predicateNode);
// custom message for accidental `async` function expressions
if (isFunctionExpression && predicateNode.async) {
return context.report({
node: predicateNode,
messageId: 'predicateCannotBeAsync',
});
}
const returnTypes = services
.getTypeAtLocation(predicateNode)
.getCallSignatures()
.map(signature => {
const type = signature.getReturnType();
if (tsutils.isTypeParameter(type)) {
return checker.getBaseConstraintOfType(type) ?? type;
}
return type;
});
const flattenTypes = [
...new Set(
returnTypes.flatMap(type => tsutils.unionConstituents(type)),
),
];
const types = inspectVariantTypes(flattenTypes);
const reportType = determineReportType(types);
if (reportType == null) {
return;
}
const suggestions: ReportSuggestionArray<MessageId> = [];
if (
isFunctionExpression &&
predicateNode.body.type !== AST_NODE_TYPES.BlockStatement
) {
suggestions.push(
...getSuggestionsForConditionError(predicateNode.body, reportType),
);
}
if (isFunctionExpression && !predicateNode.returnType) {
suggestions.push({
messageId: 'explicitBooleanReturnType',
fix: fixer => {
if (
predicateNode.type === AST_NODE_TYPES.ArrowFunctionExpression &&
isParenlessArrowFunction(predicateNode, context.sourceCode)
) {
return [
fixer.insertTextBefore(predicateNode.params[0], '('),
fixer.insertTextAfter(predicateNode.params[0], '): boolean'),
];
}
if (predicateNode.params.length === 0) {
const closingBracket = nullThrows(
context.sourceCode.getFirstToken(
predicateNode,
token => token.value === ')',
),
'function expression has to have a closing parenthesis.',
);
return fixer.insertTextAfter(closingBracket, ': boolean');
}
const lastClosingParenthesis = nullThrows(
context.sourceCode.getTokenAfter(
predicateNode.params[predicateNode.params.length - 1],
token => token.value === ')',
),
'function expression has to have a closing parenthesis.',
);
return fixer.insertTextAfter(lastClosingParenthesis, ': boolean');
},
});
}
return context.report({
node: predicateNode,
messageId: reportType,
data: {
context: 'array predicate return type',
},
suggest: suggestions,
});
}
fix(fixer: any): any¶
Parameters:
fixerany
Returns: any
Calls:
isParenlessArrowFunction (from ../util)fixer.insertTextBeforefixer.insertTextAfternullThrows (from ../util)context.sourceCode.getFirstTokencontext.sourceCode.getTokenAfter
Code
fixer => {
if (
predicateNode.type === AST_NODE_TYPES.ArrowFunctionExpression &&
isParenlessArrowFunction(predicateNode, context.sourceCode)
) {
return [
fixer.insertTextBefore(predicateNode.params[0], '('),
fixer.insertTextAfter(predicateNode.params[0], '): boolean'),
];
}
if (predicateNode.params.length === 0) {
const closingBracket = nullThrows(
context.sourceCode.getFirstToken(
predicateNode,
token => token.value === ')',
),
'function expression has to have a closing parenthesis.',
);
return fixer.insertTextAfter(closingBracket, ': boolean');
}
const lastClosingParenthesis = nullThrows(
context.sourceCode.getTokenAfter(
predicateNode.params[predicateNode.params.length - 1],
token => token.value === ')',
),
'function expression has to have a closing parenthesis.',
);
return fixer.insertTextAfter(lastClosingParenthesis, ': boolean');
}
traverseNode(node: TSESTree.Expression, isCondition: boolean): void¶
Inspects any node.
If it's a logical expression then it recursively traverses its arguments.
If it's any other kind of node then it's type is finally checked against the rule,
unless isCondition flag is set to false, in which case
it's assumed to be used for side effects only and is skipped.
Raw JSDoc
/**
* Inspects any node.
*
* If it's a logical expression then it recursively traverses its arguments.
* If it's any other kind of node then it's type is finally checked against the rule,
* unless `isCondition` flag is set to false, in which case
* it's assumed to be used for side effects only and is skipped.
*/
Calls:
traversedNodes.hastraversedNodes.addtraverseLogicalExpressioncheckNode
Internal Comments:
// prevent checking the same node multiple times
// for logical operator, we check its operands
// skip if node is not a condition
Code
function traverseNode(
node: TSESTree.Expression,
isCondition: boolean,
): void {
// prevent checking the same node multiple times
if (traversedNodes.has(node)) {
return;
}
traversedNodes.add(node);
// for logical operator, we check its operands
if (
node.type === AST_NODE_TYPES.LogicalExpression &&
node.operator !== '??'
) {
traverseLogicalExpression(node, isCondition);
return;
}
// skip if node is not a condition
if (!isCondition) {
return;
}
checkNode(node);
}
determineReportType(types: Set<VariantType>): ConditionErrorMessageId | undefined¶
Parameters:
typesSet<VariantType>
Returns: ConditionErrorMessageId | undefined
Calls:
wantedTypes.everytypes.hasis
Internal Comments:
// boolean
// boolean is always ok
// never
// never is always okay
// nullish
// condition is always false
// Known edge case: boolean `true` and nullish values are always valid boolean expressions
// nullable boolean
// Known edge case: truthy primitives and nullish values are always valid boolean expressions
// string
// nullable string
// number
// nullable number
// object
// nullable object
// nullable enum
// mixed enums (x2)
// any
Code
function determineReportType(
types: Set<VariantType>,
): ConditionErrorMessageId | undefined {
const is = (...wantedTypes: readonly VariantType[]): boolean =>
types.size === wantedTypes.length &&
wantedTypes.every(type => types.has(type));
// boolean
if (is('boolean') || is('truthy boolean')) {
// boolean is always ok
return undefined;
}
// never
if (is('never')) {
// never is always okay
return undefined;
}
// nullish
if (is('nullish')) {
// condition is always false
return 'conditionErrorNullish';
}
// Known edge case: boolean `true` and nullish values are always valid boolean expressions
if (is('nullish', 'truthy boolean')) {
return;
}
// nullable boolean
if (is('nullish', 'boolean')) {
return !options.allowNullableBoolean
? 'conditionErrorNullableBoolean'
: undefined;
}
// Known edge case: truthy primitives and nullish values are always valid boolean expressions
if (
(options.allowNumber && is('nullish', 'truthy number')) ||
(options.allowString && is('nullish', 'truthy string'))
) {
return;
}
// string
if (is('string') || is('truthy string')) {
return !options.allowString ? 'conditionErrorString' : undefined;
}
// nullable string
if (is('nullish', 'string')) {
return !options.allowNullableString
? 'conditionErrorNullableString'
: undefined;
}
// number
if (is('number') || is('truthy number')) {
return !options.allowNumber ? 'conditionErrorNumber' : undefined;
}
// nullable number
if (is('nullish', 'number')) {
return !options.allowNullableNumber
? 'conditionErrorNullableNumber'
: undefined;
}
// object
if (is('object')) {
return 'conditionErrorObject';
}
// nullable object
if (is('nullish', 'object')) {
return !options.allowNullableObject
? 'conditionErrorNullableObject'
: undefined;
}
// nullable enum
if (
is('nullish', 'number', 'enum') ||
is('nullish', 'string', 'enum') ||
is('nullish', 'truthy number', 'enum') ||
is('nullish', 'truthy string', 'enum') ||
// mixed enums
is('nullish', 'truthy number', 'truthy string', 'enum') ||
is('nullish', 'truthy number', 'string', 'enum') ||
is('nullish', 'truthy string', 'number', 'enum') ||
is('nullish', 'number', 'string', 'enum')
) {
return !options.allowNullableEnum
? 'conditionErrorNullableEnum'
: undefined;
}
// any
if (is('any')) {
return !options.allowAny ? 'conditionErrorAny' : undefined;
}
return 'conditionErrorOther';
}
is(wantedTypes: readonly VariantType[]): boolean¶
Parameters:
wantedTypesreadonly VariantType[]
Returns: boolean
Code
getSuggestionsForConditionError(node: TSESTree.Expression, conditionError: ConditionErrorMessageId): ReportSuggestionArray<MessageId>¶
Parameters:
nodeTSESTree.ExpressionconditionErrorConditionErrorMessageId
Returns: ReportSuggestionArray<MessageId>
Calls:
getWrappingFixer (from ../util)isLogicalNegationExpressionisArrayLengthExpression
Internal Comments:
// if (!nullableBoolean)
// if (nullableBoolean)
// if (!nullableNumber)
// if (nullableNumber)
// if (!nullableObject)
// if (nullableObject)
// if (!nullableString)
// if (nullableString)
// if (!array.length)
// if (array.length)
// if (!number)
// TODO: we have to compare to 0n if the type is bigint (x2)
// TODO: don't suggest this for bigint because it can't be NaN (x2)
// if (number)
// if (!string)
// if (string)
Code
function getSuggestionsForConditionError(
node: TSESTree.Expression,
conditionError: ConditionErrorMessageId,
): ReportSuggestionArray<MessageId> {
switch (conditionError) {
case 'conditionErrorAny':
return [
{
messageId: 'conditionFixCastBoolean',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `Boolean(${code})`,
}),
},
];
case 'conditionErrorNullableBoolean':
if (isLogicalNegationExpression(node.parent)) {
// if (!nullableBoolean)
return [
{
messageId: 'conditionFixDefaultFalse',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} ?? false`,
}),
},
{
messageId: 'conditionFixCompareFalse',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `${code} === false`,
}),
},
];
}
// if (nullableBoolean)
return [
{
messageId: 'conditionFixDefaultFalse',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} ?? false`,
}),
},
{
messageId: 'conditionFixCompareTrue',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} === true`,
}),
},
];
case 'conditionErrorNullableEnum':
if (isLogicalNegationExpression(node.parent)) {
return [
{
messageId: 'conditionFixCompareNullish',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `${code} == null`,
}),
},
];
}
return [
{
messageId: 'conditionFixCompareNullish',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} != null`,
}),
},
];
case 'conditionErrorNullableNumber':
if (isLogicalNegationExpression(node.parent)) {
// if (!nullableNumber)
return [
{
messageId: 'conditionFixCompareNullish',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `${code} == null`,
}),
},
{
messageId: 'conditionFixDefaultZero',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} ?? 0`,
}),
},
{
messageId: 'conditionFixCastBoolean',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `!Boolean(${code})`,
}),
},
];
}
// if (nullableNumber)
return [
{
messageId: 'conditionFixCompareNullish',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} != null`,
}),
},
{
messageId: 'conditionFixDefaultZero',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} ?? 0`,
}),
},
{
messageId: 'conditionFixCastBoolean',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `Boolean(${code})`,
}),
},
];
case 'conditionErrorNullableObject':
if (isLogicalNegationExpression(node.parent)) {
// if (!nullableObject)
return [
{
messageId: 'conditionFixCompareNullish',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `${code} == null`,
}),
},
];
}
// if (nullableObject)
return [
{
messageId: 'conditionFixCompareNullish',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} != null`,
}),
},
];
case 'conditionErrorNullableString':
if (isLogicalNegationExpression(node.parent)) {
// if (!nullableString)
return [
{
messageId: 'conditionFixCompareNullish',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `${code} == null`,
}),
},
{
messageId: 'conditionFixDefaultEmptyString',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} ?? ""`,
}),
},
{
messageId: 'conditionFixCastBoolean',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `!Boolean(${code})`,
}),
},
];
}
// if (nullableString)
return [
{
messageId: 'conditionFixCompareNullish',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} != null`,
}),
},
{
messageId: 'conditionFixDefaultEmptyString',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} ?? ""`,
}),
},
{
messageId: 'conditionFixCastBoolean',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `Boolean(${code})`,
}),
},
];
case 'conditionErrorNumber':
if (isArrayLengthExpression(node, checker, services)) {
if (isLogicalNegationExpression(node.parent)) {
// if (!array.length)
return [
{
messageId: 'conditionFixCompareArrayLengthZero',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `${code} === 0`,
}),
},
];
}
// if (array.length)
return [
{
messageId: 'conditionFixCompareArrayLengthNonzero',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} > 0`,
}),
},
];
}
if (isLogicalNegationExpression(node.parent)) {
// if (!number)
return [
{
messageId: 'conditionFixCompareZero',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
// TODO: we have to compare to 0n if the type is bigint
wrap: code => `${code} === 0`,
}),
},
{
// TODO: don't suggest this for bigint because it can't be NaN
messageId: 'conditionFixCompareNaN',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `Number.isNaN(${code})`,
}),
},
{
messageId: 'conditionFixCastBoolean',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `!Boolean(${code})`,
}),
},
];
}
// if (number)
return [
{
messageId: 'conditionFixCompareZero',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} !== 0`,
}),
},
{
messageId: 'conditionFixCompareNaN',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `!Number.isNaN(${code})`,
}),
},
{
messageId: 'conditionFixCastBoolean',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `Boolean(${code})`,
}),
},
];
case 'conditionErrorString':
if (isLogicalNegationExpression(node.parent)) {
// if (!string)
return [
{
messageId: 'conditionFixCompareStringLength',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `${code}.length === 0`,
}),
},
{
messageId: 'conditionFixCompareEmptyString',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `${code} === ""`,
}),
},
{
messageId: 'conditionFixCastBoolean',
fix: getWrappingFixer({
node: node.parent,
innerNode: node,
sourceCode: context.sourceCode,
wrap: code => `!Boolean(${code})`,
}),
},
];
}
// if (string)
return [
{
messageId: 'conditionFixCompareStringLength',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code}.length > 0`,
}),
},
{
messageId: 'conditionFixCompareEmptyString',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `${code} !== ""`,
}),
},
{
messageId: 'conditionFixCastBoolean',
fix: getWrappingFixer({
node,
sourceCode: context.sourceCode,
wrap: code => `Boolean(${code})`,
}),
},
];
case 'conditionErrorObject':
case 'conditionErrorNullish':
case 'conditionErrorOther':
return [];
default:
conditionError satisfies never;
throw new Error('Unreachable');
}
}
checkNode(node: TSESTree.Expression): void¶
This function does the actual type check on a node. It analyzes the type of a node and checks if it is allowed in a boolean context.
Raw JSDoc
Calls:
getConstrainedTypeAtLocation (from ../util)inspectVariantTypestsutils.unionConstituentsdetermineReportTypecontext.reportgetSuggestionsForConditionError
Code
function checkNode(node: TSESTree.Expression): void {
const type = getConstrainedTypeAtLocation(services, node);
const types = inspectVariantTypes(tsutils.unionConstituents(type));
const reportType = determineReportType(types);
if (reportType != null) {
context.report({
node,
messageId: reportType,
data: {
context: 'conditional',
},
suggest: getSuggestionsForConditionError(node, reportType),
});
}
}
inspectVariantTypes(types: ts.Type[]): Set<VariantType>¶
Check union variants for the types we care about
Calls:
types.sometsutils.isTypeFlagSetvariantTypes.addtypes.filtertsutils.isTrueLiteralTypestrings.everytype.isStringLiteralnumbers.everytype.isNumberLiteral
Internal Comments:
// If incoming type is either "true" or "false", there will be one type
// object with intrinsicName set accordingly
// If incoming type is boolean, there will be two type objects with
// intrinsicName set "true" and "false" each because of ts-api-utils.unionConstituents()
Code
function inspectVariantTypes(types: ts.Type[]): Set<VariantType> {
const variantTypes = new Set<VariantType>();
if (
types.some(type =>
tsutils.isTypeFlagSet(
type,
ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.VoidLike,
),
)
) {
variantTypes.add('nullish');
}
const booleans = types.filter(type =>
tsutils.isTypeFlagSet(type, ts.TypeFlags.BooleanLike),
);
// If incoming type is either "true" or "false", there will be one type
// object with intrinsicName set accordingly
// If incoming type is boolean, there will be two type objects with
// intrinsicName set "true" and "false" each because of ts-api-utils.unionConstituents()
if (booleans.length === 1) {
variantTypes.add(
tsutils.isTrueLiteralType(booleans[0]) ? 'truthy boolean' : 'boolean',
);
} else if (booleans.length === 2) {
variantTypes.add('boolean');
}
const strings = types.filter(type =>
tsutils.isTypeFlagSet(type, ts.TypeFlags.StringLike),
);
if (strings.length) {
if (
strings.every(type => type.isStringLiteral() && type.value !== '')
) {
variantTypes.add('truthy string');
} else {
variantTypes.add('string');
}
}
const numbers = types.filter(type =>
tsutils.isTypeFlagSet(
type,
ts.TypeFlags.NumberLike | ts.TypeFlags.BigIntLike,
),
);
if (numbers.length) {
if (numbers.every(type => type.isNumberLiteral() && type.value !== 0)) {
variantTypes.add('truthy number');
} else {
variantTypes.add('number');
}
}
if (
types.some(type => tsutils.isTypeFlagSet(type, ts.TypeFlags.EnumLike))
) {
variantTypes.add('enum');
}
if (
types.some(
type =>
!tsutils.isTypeFlagSet(
type,
ts.TypeFlags.Null |
ts.TypeFlags.Undefined |
ts.TypeFlags.VoidLike |
ts.TypeFlags.BooleanLike |
ts.TypeFlags.StringLike |
ts.TypeFlags.NumberLike |
ts.TypeFlags.BigIntLike |
ts.TypeFlags.TypeParameter |
ts.TypeFlags.Any |
ts.TypeFlags.Unknown |
ts.TypeFlags.Never,
),
)
) {
variantTypes.add(types.some(isBrandedBoolean) ? 'boolean' : 'object');
}
if (
types.some(type =>
tsutils.isTypeFlagSet(
type,
ts.TypeFlags.TypeParameter |
ts.TypeFlags.Any |
ts.TypeFlags.Unknown,
),
)
) {
variantTypes.add('any');
}
if (types.some(type => tsutils.isTypeFlagSet(type, ts.TypeFlags.Never))) {
variantTypes.add('never');
}
return variantTypes;
}
Type Aliases¶
Options¶
type Options = [
{
allowAny?: boolean;
allowNullableBoolean?: boolean;
allowNullableEnum?: boolean;
allowNullableNumber?: boolean;
allowNullableObject?: boolean;
allowNullableString?: boolean;
allowNumber?: boolean;
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean;
allowString?: boolean;
},
];
ConditionErrorMessageId¶
type ConditionErrorMessageId = | 'conditionErrorAny'
| 'conditionErrorNullableBoolean'
| 'conditionErrorNullableEnum'
| 'conditionErrorNullableNumber'
| 'conditionErrorNullableObject'
| 'conditionErrorNullableString'
| 'conditionErrorNullish'
| 'conditionErrorNumber'
| 'conditionErrorObject'
| 'conditionErrorOther'
| 'conditionErrorString';
MessageId¶
type MessageId = | 'conditionFixCastBoolean'
| 'conditionFixCompareArrayLengthNonzero'
| 'conditionFixCompareArrayLengthZero'
| 'conditionFixCompareEmptyString'
| 'conditionFixCompareFalse'
| 'conditionFixCompareNaN'
| 'conditionFixCompareNullish'
| 'conditionFixCompareStringLength'
| 'conditionFixCompareTrue'
| 'conditionFixCompareZero'
| 'conditionFixDefaultEmptyString'
| 'conditionFixDefaultFalse'
| 'conditionFixDefaultZero'
| 'explicitBooleanReturnType'
| 'noStrictNullCheck'
| 'predicateCannotBeAsync'
| ConditionErrorMessageId;
TestExpression¶
type TestExpression = | TSESTree.ConditionalExpression
| TSESTree.DoWhileStatement
| TSESTree.ForStatement
| TSESTree.IfStatement
| TSESTree.WhileStatement;
VariantType¶
/* The types we care about /
type VariantType = | 'any'
| 'boolean'
| 'enum'
| 'never'
| 'nullish'
| 'number'
| 'object'
| 'string'
| 'truthy boolean'
| 'truthy number'
| 'truthy string';
Generated by Syntax Scribe