eslint-plugin-unicorn
Version:
More than 300 powerful ESLint rules
2,885 lines • 87.2 kB
JavaScript
import {isRegExp} from 'node:util/types';
import {findVariable, getPropertyName, getStaticValue} from '@eslint-community/eslint-utils';
import {renameVariable} from './fix/index.js';
import {combineBooleanStates, getPromisedTypeBooleanState, getTypeBooleanState} from './utils/get-type-boolean-state.js';
import resolveVariableName from './utils/resolve-variable-name.js';
import {getBooleanWrapperVariableState} from './utils/get-boolean-wrapper-variable-state.js';
import {
getAvailableVariableName,
getScopes,
getVariableIdentifiers,
isReactHookName,
lowerFirst,
upperFirst,
} from './utils/index.js';
import {
isBooleanExpression,
isBooleanFunction,
isBooleanFunctionReference,
isBooleanFunctionTypeAnnotation,
isBooleanTypeAnnotation,
isFunctionTypeAnnotation,
} from './utils/is-boolean.js';
const MESSAGE_ID = 'consistent-boolean-name';
const MESSAGE_ID_NON_BOOLEAN_PREFIX = 'non-boolean-prefix';
const MESSAGE_ID_SUGGESTION = 'rename';
const REMOVED_CHECK_PROPERTIES_MESSAGE = '`checkProperties` was removed. Use `checkMethods` and `checkFields` instead.';
const [ALWAYS, PROHIBIT, NEVER] = ['always', 'prohibit', 'never'];
const messages = {
[MESSAGE_ID]: 'Boolean name `{{name}}` should start with {{prefixes}}.',
[MESSAGE_ID_NON_BOOLEAN_PREFIX]: '`{{name}}` starts with `{{prefix}}`, so it should be boolean.',
[MESSAGE_ID_SUGGESTION]: 'Rename to `{{replacement}}`.',
};
const defaultPrefixes = {
is: true,
are: true,
has: true,
have: true,
can: true,
should: true,
was: true,
were: true,
did: true,
will: true,
// `requireLogin()` reads like an action or assertion, not a boolean.
requires: true,
};
const getEnabledPrefixes = ({prefixes = {}} = {}) =>
Object.entries({
...defaultPrefixes,
...prefixes,
})
.filter(([, enabled]) => enabled)
.map(([prefix]) => prefix);
const formatPrefixes = prefixes =>
prefixes.map(prefix => `\`${prefix}\``).join(', ');
const booleanBinaryOperators = new Set([
'>',
'>=',
'<',
'<=',
'==',
'===',
'!=',
'!==',
'in',
'instanceof',
]);
const boolean = 'boolean';
const nonBoolean = 'non-boolean';
const unknown = 'unknown';
const nullishTypeAnnotationTypes = new Set([
'TSNullKeyword',
'TSUndefinedKeyword',
]);
const unknownTypeAnnotationTypes = new Set([
'TSAnyKeyword',
'TSNeverKeyword',
'TSUnknownKeyword',
'TSConditionalType',
]);
const promiseValueTypeNames = new Set(['Promise', 'PromiseLike']);
const nonBooleanExpressionTypes = new Set([
'ArrayExpression',
'ObjectExpression',
'ClassExpression',
'NewExpression',
'TemplateLiteral',
'UpdateExpression',
]);
const expressionWrapperTypes = new Set([
'AwaitExpression',
'TSNonNullExpression',
'ParenthesizedExpression',
]);
const typeScriptExpressionWrapperTypes = new Set([
'TSAsExpression',
'TSTypeAssertion',
'TSSatisfiesExpression',
]);
const logicalAssignmentOperators = new Set(['&&=', '||=', '??=']);
const isUpperCase = string => string === string.toUpperCase();
const stripLeadingUnderscores = name => name.replace(/^_+/, '');
const isScreamingCase = name => /^[A-Z][\dA-Z_]*$/.test(stripLeadingUnderscores(name));
const hasReactReferenceSuffix = name => /(?:Ref|Reference)$/.test(stripLeadingUnderscores(name));
const isFunction = node => [
'ArrowFunctionExpression',
'FunctionDeclaration',
'FunctionExpression',
'TSDeclareFunction',
].includes(node?.type);
const propertyDefinitionTypes = new Set([
'PropertyDefinition',
'AccessorProperty',
'TSAbstractPropertyDefinition',
'TSAbstractAccessorProperty',
]);
const methodDefinitionTypes = new Set(['MethodDefinition', 'TSAbstractMethodDefinition']);
const typeScriptMemberTypes = new Set(['TSMethodSignature', 'TSPropertySignature']);
const isSetter = node => node?.kind === 'set' || node?.parent?.kind === 'set';
const unwrapParameter = node => node.type === 'TSParameterProperty'
? node.parameter
: node;
const isSameNode = (a, b) =>
a?.range[0] === b?.range[0]
&& a?.range[1] === b?.range[1];
function findParameter(parameters, identifier) {
for (const parameter of parameters) {
const unwrappedParameter = unwrapParameter(parameter);
const parameterName = unwrappedParameter.type === 'AssignmentPattern'
? unwrappedParameter.left
: unwrappedParameter;
if (isSameNode(parameterName, identifier)) {
return unwrappedParameter;
}
}
}
const prepareOptions = options => {
if (Object.hasOwn(options ?? {}, 'checkProperties')) {
throw new Error(REMOVED_CHECK_PROPERTIES_MESSAGE);
}
const {
checkVariables,
checkArguments,
checkFunctions,
checkMethods,
checkFields,
prefixes,
ignore,
wrappers,
} = options ?? {};
const preparedOptions = {
checkVariables,
checkArguments,
checkFunctions,
checkMethods,
checkFields,
prefixes: getEnabledPrefixes({prefixes}),
ignore: ignore.map(pattern => isRegExp(pattern) ? pattern : new RegExp(pattern, 'u')),
wrappers: new Map(Object.entries(wrappers)),
};
return preparedOptions;
};
function isIgnoredName(name, ignore) {
return ignore.some(regexp => {
regexp.lastIndex = 0;
const isIgnored = regexp.test(name);
regexp.lastIndex = 0;
return isIgnored;
});
}
function getBooleanPrefix(name, prefixes) {
name = stripLeadingUnderscores(name);
for (const prefix of prefixes) {
if (name.startsWith(`${prefix.toUpperCase()}_`)) {
return prefix;
}
if (
name.startsWith(prefix)
&& name.length > prefix.length
&& /[\dA-Z_]/.test(name[prefix.length])
) {
return prefix;
}
}
}
function getReplacementName(name, prefix) {
const leadingUnderscores = name.match(/^_*/)[0];
const nameWithoutLeadingUnderscores = stripLeadingUnderscores(name);
return isUpperCase(nameWithoutLeadingUnderscores)
? `${leadingUnderscores}${prefix.toUpperCase()}_${nameWithoutLeadingUnderscores}`
: `${leadingUnderscores}${prefix}${upperFirst(nameWithoutLeadingUnderscores)}`;
}
function getReactHookReplacementName({name, nameForPrefixCheck}, prefix) {
if (name === nameForPrefixCheck) {
return getReplacementName(name, prefix);
}
const hookName = isScreamingCase(nameForPrefixCheck)
? getReplacementName(nameForPrefixCheck, prefix)
: `${prefix}${upperFirst(nameForPrefixCheck)}`;
return `use${upperFirst(hookName)}`;
}
const isExportedIdentifier = identifier => {
if (
identifier.parent.type === 'VariableDeclarator'
&& identifier.parent.id === identifier
) {
return (
identifier.parent.parent.type === 'VariableDeclaration'
&& identifier.parent.parent.parent.type === 'ExportNamedDeclaration'
);
}
if (
identifier.parent.type === 'ExportSpecifier'
&& identifier.parent.local === identifier
&& identifier.parent.local === identifier.parent.exported
) {
return true;
}
if (
identifier.parent.type === 'FunctionDeclaration'
&& identifier.parent.id === identifier
) {
return identifier.parent.parent.type === 'ExportNamedDeclaration';
}
return false;
};
const isExportSpecifierLocal = identifier =>
identifier.parent.type === 'ExportSpecifier'
&& identifier.parent.local === identifier;
const isExportDefaultIdentifier = identifier =>
identifier.parent.type === 'ExportDefaultDeclaration'
&& identifier.parent.declaration === identifier;
const shouldSuggestRename = variable => getVariableIdentifiers(variable)
.every(identifier =>
!isExportedIdentifier(identifier)
&& identifier.type !== 'JSXIdentifier',
);
function isDestructuredDefinition(definition) {
for (let node = definition.name; node && node !== definition.node; node = node.parent) {
if (
node.type === 'ObjectPattern'
|| node.type === 'ArrayPattern'
) {
return true;
}
}
return false;
}
const isDestructuredVariable = variable =>
variable.defs.length > 0
&& variable.defs.every(definition => isDestructuredDefinition(definition));
function isParameterPropertyDefinition(definition) {
return definition.type === 'Parameter'
&& definition.node.params.some(parameter =>
parameter.type === 'TSParameterProperty'
&& isSameNode(getParameterPropertyNameNode(parameter), definition.name),
);
}
const hasWriteAfterInitialization = variable =>
variable.references.some(reference => !reference.init && reference.isWrite());
const isBooleanAsyncFunction = (node, context) =>
getPromisedTypeAnnotationBooleanState(node.returnType, context, context.sourceCode.getScope(node), {allowNullish: false}) === boolean
|| getAsyncFunctionTypeInformationBooleanState(node, context, false) === boolean;
function getAsyncFunctionTypeAnnotationBooleanState(node, context, scope, typeState) {
const normalizedTypeState = getTypeState(typeState);
if (!normalizedTypeState.functionTypesAreBoolean || !isCallableTypeAnnotation(node, context, scope)) {
return unknown;
}
const state = getPromisedTypeAnnotationBooleanState(node, context, scope, {
...normalizedTypeState,
allowNullish: false,
});
if (
state !== unknown
|| !context.sourceCode.parserServices?.program
|| hasNullableType(node, context)
|| hasUnresolvedTypeParameterReference(node, normalizedTypeState, scope, false)
) {
return state;
}
return getAsyncFunctionTypeInformationBooleanState(node, context, false);
}
const isBooleanAsyncFunctionTypeAnnotation = (node, context, scope) =>
getAsyncFunctionTypeAnnotationBooleanState(node, context, scope) === boolean;
const isBooleanFunctionLikeTypeAnnotation = (node, context, scope) =>
isBooleanFunctionTypeAnnotation(node, context, scope)
|| isBooleanAsyncFunctionTypeAnnotation(node, context, scope)
|| (
isCallableTypeAnnotation(node, context, scope)
&& getDirectTypeAnnotationBooleanState(node, context, scope, {allowNullish: false}) === boolean
);
const isBooleanFunctionValue = (node, context) => {
if (node.async) {
return isBooleanAsyncFunction(node, context);
}
const scope = context.sourceCode.getScope(node);
return getDirectTypeAnnotationBooleanState(node.returnType, context, scope, {functionTypesAreBoolean: false, allowNullish: false}) === boolean
|| isBooleanFunction(node, context);
};
const isBooleanFunctionDefinition = (definition, context, isAsync = definition.node.async) =>
definition.type === 'FunctionName'
&& isFunction(definition.node)
&& (isAsync
? isBooleanAsyncFunction(definition.node, context)
: isBooleanFunctionValue(definition.node, context));
const isBooleanAsyncFunctionReference = (node, context) => {
if (node?.type !== 'Identifier') {
return false;
}
const variable = findVariable(context.sourceCode.getScope(node), node);
if (
!variable
|| hasWriteAfterInitialization(variable)
) {
return false;
}
const functionDefinitions = getFunctionDefinitions(variable);
if (functionDefinitions) {
return hasAsyncFunctionImplementation(variable.defs)
&& functionDefinitions.every(definition => isBooleanFunctionDefinition(definition, context, true));
}
if (variable.defs.length !== 1) {
return false;
}
const [definition] = variable.defs;
const scope = context.sourceCode.getScope(definition.name);
if (isBooleanAsyncFunctionTypeAnnotation(definition.name?.typeAnnotation, context, scope)) {
return true;
}
if (definition.type === 'FunctionName') {
return isBooleanFunctionDefinition(definition, context);
}
return definition.type === 'Variable'
&& definition.parent.kind === 'const'
&& definition.node.init?.async
&& isBooleanAsyncFunction(definition.node.init, context);
};
const isBooleanValue = (node, context) => isFunction(node)
? isBooleanFunctionValue(node, context)
: isBooleanFunctionReference(node, context)
|| isBooleanAsyncFunctionReference(node, context)
|| isBooleanExpression(node, context);
function getSupportedVariableDefinition(variable) {
if (variable.defs.length !== 1) {
return;
}
const [definition] = variable.defs;
const {name} = definition;
if (
name?.type !== 'Identifier'
|| !['Variable', 'Parameter', 'FunctionName'].includes(definition.type)
|| (definition.type === 'Variable' && definition.node.id.type !== 'Identifier')
|| (definition.type === 'Parameter' && isSetter(definition.node))
) {
return;
}
return definition;
}
function getVariableOption(variable) {
if (getFunctionDefinitions(variable)) {
return 'checkFunctions';
}
const definition = getSupportedVariableDefinition(variable);
if (!definition) {
return;
}
return {
Variable: 'checkVariables',
Parameter: 'checkArguments',
FunctionName: 'checkFunctions',
}[definition.type];
}
function getFunctionDefinitions(variable) {
if (
variable.defs.length <= 1
|| variable.defs.some(definition => definition.type !== 'FunctionName')
) {
return;
}
const overloadDefinitions = variable.defs.filter(definition => definition.node.type === 'TSDeclareFunction');
return overloadDefinitions.length > 0 ? overloadDefinitions : variable.defs;
}
function hasAsyncFunctionImplementation(definitions) {
return definitions.some(definition => isFunction(definition.node) && definition.node.async);
}
function hasGeneratorFunctionImplementation(definitions) {
return definitions.some(definition => isFunction(definition.node) && definition.node.generator);
}
function isReactHookFunctionBinding(variable, context) {
if (
variable.name === 'use'
|| !isReactHookName(variable.name)
) {
return false;
}
if (getFunctionDefinitions(variable)) {
return true;
}
const definition = getSupportedVariableDefinition(variable);
if (definition?.type === 'FunctionName') {
return isFunction(definition.node);
}
return definition?.type === 'Variable'
&& (
isFunction(definition.node.init)
|| isCallableTypeAnnotation(definition.name.typeAnnotation, context, context.sourceCode.getScope(definition.name))
);
}
function getNameForPrefixCheck(variable, context) {
if (!isReactHookFunctionBinding(variable, context)) {
return variable.name;
}
const hookName = variable.name.slice(3);
return isScreamingCase(hookName) ? hookName : lowerFirst(hookName);
}
function isReactUseReferenceCall(node) {
if (node?.type !== 'CallExpression') {
return false;
}
const {callee} = node;
if (callee.type === 'Identifier') {
return callee.name === 'useRef';
}
return callee.type === 'MemberExpression'
&& !callee.computed
&& !callee.optional
&& callee.object.type === 'Identifier'
&& callee.object.name === 'React'
&& callee.property.type === 'Identifier'
&& callee.property.name === 'useRef';
}
function isDirectVueFunctionReference(node, context) {
const variable = findVariable(context.sourceCode.getScope(node), node);
return !variable
|| variable.defs.every(definition =>
(
definition.type === 'ImportBinding'
&& definition.node.type === 'ImportSpecifier'
&& definition.parent.type === 'ImportDeclaration'
&& definition.parent.source.value === 'vue'
&& definition.node.imported.type === 'Identifier'
&& definition.node.imported.name === node.name
)
|| isInDeclareContext(definition.node),
);
}
function isBooleanReactReferenceVariable(variable, context) {
const definition = getSupportedVariableDefinition(variable);
return definition?.type === 'Variable'
&& !hasWriteAfterInitialization(variable)
&& hasReactReferenceSuffix(variable.name)
&& isReactUseReferenceCall(definition.node.init)
&& getExpressionBooleanState(definition.node.init.arguments[0], context, new Set(), false) === boolean;
}
function unwrapVueComputedGetter(node) {
while (
node?.type === 'ParenthesizedExpression'
|| node?.type === 'TSNonNullExpression'
|| typeScriptExpressionWrapperTypes.has(node?.type)
) {
node = node.expression;
}
return node;
}
function isBooleanVueReferenceVariable(variable, context) {
const definition = getSupportedVariableDefinition(variable);
const callExpression = definition?.type === 'Variable' ? definition.node.init : undefined;
if (
callExpression?.type !== 'CallExpression'
|| callExpression.callee.type !== 'Identifier'
|| !isDirectVueFunctionReference(callExpression.callee, context)
|| hasWriteAfterInitialization(variable)
) {
return false;
}
const [argument] = callExpression.arguments;
if (callExpression.callee.name === 'ref') {
return getExpressionBooleanState(argument, context, new Set(), false) === boolean;
}
const getter = unwrapVueComputedGetter(argument);
return callExpression.callee.name === 'computed'
&& (
(isFunction(getter) && !getter.async && !getter.generator)
|| isBooleanFunctionReference(getter, context)
)
&& getExpressionBooleanState(argument, context) === boolean;
}
function combineVariableBooleanStates(states) {
if (states.includes(nonBoolean)) {
return nonBoolean;
}
return combineBooleanStates(states);
}
function getTypeInformationBooleanState(node, context, functionTypesAreBoolean = true, allowNullish = true) {
const {parserServices} = context.sourceCode;
if (!parserServices?.program) {
return unknown;
}
try {
const checker = parserServices.program.getTypeChecker();
const type = parserServices.getTypeAtLocation(node);
const nonNullableType = checker.getNonNullableType(type);
if (!allowNullish && nonNullableType !== type) {
return unknown;
}
if (!allowNullish && hasNullableType(node, context)) {
return unknown;
}
return getTypeBooleanState(
type,
checker,
new Set(),
functionTypesAreBoolean,
);
} catch {
return unknown;
}
}
function hasNullableType(node, context) {
if (!node) {
return false;
}
if (
node.type === 'TSTypeAnnotation'
|| node.type === 'TSParenthesizedType'
) {
return hasNullableType(node.typeAnnotation, context);
}
if (node.type === 'TSUnionType' && node.types.some(type => nullishTypeAnnotationTypes.has(type.type))) {
return true;
}
const {parserServices} = context.sourceCode;
if (!parserServices?.program) {
return false;
}
try {
const checker = parserServices.program.getTypeChecker();
const hasNullishType = (type, visitedTypes = new Set()) => {
if (!type || visitedTypes.has(type)) {
return false;
}
visitedTypes.add(type);
if (checker.getNonNullableType(type) !== type) {
return true;
}
if (type.isUnion() && type.types.some(type => hasNullishType(type, visitedTypes))) {
return true;
}
const promisedType = checker.getPromisedTypeOfPromise(type);
if (promisedType && hasNullishType(promisedType, visitedTypes)) {
return true;
}
return type.getCallSignatures().some(signature => hasNullishType(signature.getReturnType(), visitedTypes));
};
return hasNullishType(parserServices.getTypeAtLocation(node));
} catch {
return false;
}
}
function getPromisedTypeInformationBooleanState(node, context, allowNullish = true) {
const {parserServices} = context.sourceCode;
if (!parserServices?.program) {
return unknown;
}
try {
const checker = parserServices.program.getTypeChecker();
const type = parserServices.getTypeAtLocation(node);
const nonNullableType = checker.getNonNullableType(type);
if (!allowNullish && nonNullableType !== type) {
return unknown;
}
if (!allowNullish && hasNullableType(node, context)) {
return unknown;
}
return getPromisedTypeBooleanState(nonNullableType, checker);
} catch {
return unknown;
}
}
function getTypeReferenceName(typeName) {
if (typeName?.type === 'Identifier') {
return typeName.name;
}
if (typeName?.type === 'TSQualifiedName') {
const left = getTypeReferenceName(typeName.left);
return left ? `${left}.${typeName.right.name}` : undefined;
}
}
const getTypeDefinitions = (name, scope) =>
(resolveVariableName(name, scope)?.defs ?? []).filter(definition => definition.type === 'Type');
const getInterfaceDefinitions = (name, scope) =>
getTypeDefinitions(name, scope).filter(definition => definition.node.type === 'TSInterfaceDeclaration');
function getPromisedInterfaceState(interfaceNode, context, scope, {visitedTypeReferenceNames, typeState}) {
let hasCallSignature = false;
for (const member of interfaceNode.body.body) {
if (member.type !== 'TSCallSignatureDeclaration') {
continue;
}
hasCallSignature = true;
if (!isPromisedTypeAnnotation(member.returnType, context, scope, {visitedTypeReferenceNames, typeState})) {
return false;
}
}
for (const heritage of interfaceNode.extends ?? []) {
const name = getTypeReferenceName(heritage.expression);
if (!name || visitedTypeReferenceNames.has(name)) {
continue;
}
const nextVisitedTypeReferenceNames = new Set(visitedTypeReferenceNames);
nextVisitedTypeReferenceNames.add(name);
for (const definition of getTypeDefinitions(name, scope)) {
const definitionScope = context.sourceCode.getScope(definition.node);
const definitionTypeState = {
...typeState,
typeParameterTypes: getTypeParameterTypes(definition.node, getTypeArguments(heritage), typeState),
};
if (definition.node.type === 'TSInterfaceDeclaration') {
const state = getPromisedInterfaceState(definition.node, context, definitionScope, {
visitedTypeReferenceNames: nextVisitedTypeReferenceNames,
typeState: definitionTypeState,
});
if (state === false) {
return false;
}
if (state === true) {
hasCallSignature = true;
}
} else if (definition.node.type === 'TSTypeAliasDeclaration') {
const returnTypes = getCallSignatureReturnTypes(definition.node.typeAnnotation, context, definitionScope, {typeState: definitionTypeState});
if (returnTypes.length > 0) {
hasCallSignature = true;
const hasNonPromisedReturnType = returnTypes.some(returnType => !isPromisedTypeAnnotation(returnType, context, definitionScope, {
visitedTypeReferenceNames: nextVisitedTypeReferenceNames,
typeState: definitionTypeState,
}));
if (hasNonPromisedReturnType) {
return false;
}
}
}
}
}
return hasCallSignature ? true : undefined;
}
function getTypeArguments(node) {
while (
node?.type === 'TSTypeAnnotation'
|| node?.type === 'TSParenthesizedType'
) {
node = node.typeAnnotation;
}
return node?.typeArguments?.params ?? node?.typeParameters?.params;
}
function hasMissingRequiredTypeArguments(node, scope) {
while (
node?.type === 'TSTypeAnnotation'
|| node?.type === 'TSParenthesizedType'
) {
node = node.typeAnnotation;
}
if (node?.type !== 'TSTypeReference') {
return false;
}
const name = getTypeReferenceName(node.typeName);
if (!name) {
return false;
}
const typeArguments = getTypeArguments(node) ?? [];
return getTypeDefinitions(name, scope).some(definition =>
(definition.node.typeParameters?.params ?? []).some((parameter, index) => index >= typeArguments.length && !parameter.default),
);
}
function getCallSignatureReturnTypesFromDefinition(definition, context, scope, {typeArguments, typeState, visitedTypeReferenceNames} = {}) {
const definitionScope = context.sourceCode.getScope(definition.node);
const definitionTypeState = {
...typeState,
typeParameterTypes: getTypeParameterTypes(definition.node, typeArguments, typeState),
};
if (definition.node.type === 'TSInterfaceDeclaration') {
return getCallSignatureReturnTypes(definition.node, context, definitionScope, {
typeState: definitionTypeState,
visitedTypeReferenceNames,
});
}
return definition.node.type === 'TSTypeAliasDeclaration'
? getCallSignatureReturnTypes(definition.node.typeAnnotation, context, definitionScope, {
typeState: definitionTypeState,
visitedTypeReferenceNames,
})
: [];
}
function getCallSignatureReturnTypes(node, context, scope, {typeState = getTypeState(), visitedTypeReferenceNames = new Set()} = {}) {
if (hasMissingRequiredTypeArguments(node, scope)) {
return [];
}
if (
node?.type === 'TSParenthesizedType'
|| node?.type === 'TSTypeAnnotation'
) {
return getCallSignatureReturnTypes(node.typeAnnotation, context, scope, {typeState, visitedTypeReferenceNames});
}
if (node?.type === 'TSFunctionType') {
return [resolveTypeParameterType(node.returnType, typeState)];
}
if (node?.type === 'TSTypeLiteral') {
return node.members
.filter(member => member.type === 'TSCallSignatureDeclaration')
.map(member => resolveTypeParameterType(member.returnType, typeState));
}
if (node?.type === 'TSIntersectionType') {
return node.types.flatMap(type => getCallSignatureReturnTypes(type, context, scope, {typeState, visitedTypeReferenceNames}));
}
if (node?.type === 'TSInterfaceDeclaration') {
const returnTypes = node.body.body
.filter(member => member.type === 'TSCallSignatureDeclaration')
.map(member => resolveTypeParameterType(member.returnType, typeState));
for (const heritage of node.extends ?? []) {
const name = getTypeReferenceName(heritage.expression);
if (!name || visitedTypeReferenceNames.has(name)) {
continue;
}
const nextVisitedTypeReferenceNames = new Set(visitedTypeReferenceNames);
nextVisitedTypeReferenceNames.add(name);
for (const definition of getTypeDefinitions(name, scope)) {
returnTypes.push(...getCallSignatureReturnTypesFromDefinition(definition, context, scope, {
typeArguments: getTypeArguments(heritage),
typeState,
visitedTypeReferenceNames: nextVisitedTypeReferenceNames,
}));
}
}
return returnTypes;
}
if (node?.type === 'TSTypeReference') {
const name = getTypeReferenceName(node.typeName);
if (!name || visitedTypeReferenceNames.has(name)) {
return [];
}
const nextVisitedTypeReferenceNames = new Set(visitedTypeReferenceNames);
nextVisitedTypeReferenceNames.add(name);
return getTypeDefinitions(name, scope).flatMap(definition => getCallSignatureReturnTypesFromDefinition(definition, context, scope, {
typeArguments: getTypeArguments(node),
typeState,
visitedTypeReferenceNames: nextVisitedTypeReferenceNames,
}));
}
return [];
}
function isPromisedTypeReference(node, context, scope, {visitedTypeReferenceNames, typeState}) {
const name = getTypeReferenceName(node.typeName);
if (!name || visitedTypeReferenceNames.has(name)) {
return false;
}
if (isGlobalPromiseTypeReference(node, scope)) {
return true;
}
const definitions = getTypeDefinitions(name, scope);
if (definitions.length === 0) {
return false;
}
const nextVisitedTypeReferenceNames = new Set(visitedTypeReferenceNames);
nextVisitedTypeReferenceNames.add(name);
const states = new Set(definitions.map(definition => {
const definitionScope = context.sourceCode.getScope(definition.node);
const definitionTypeState = {
...typeState,
typeParameterTypes: getTypeParameterTypes(definition.node, getTypeArguments(node), typeState),
};
if (definition.node.type === 'TSTypeAliasDeclaration') {
return isPromisedTypeAnnotation(definition.node.typeAnnotation, context, definitionScope, {
visitedTypeReferenceNames: nextVisitedTypeReferenceNames,
typeState: definitionTypeState,
});
}
return definition.node.type === 'TSInterfaceDeclaration'
? getPromisedInterfaceState(definition.node, context, definitionScope, {
visitedTypeReferenceNames: nextVisitedTypeReferenceNames,
typeState: definitionTypeState,
})
: false;
}));
return states.has(true) && !states.has(false);
}
function isPromisedTypeAnnotation(node, context, scope, {visitedTypeReferenceNames = new Set(), typeState = getTypeState()} = {}) {
if (hasMissingRequiredTypeArguments(node, scope)) {
return false;
}
const typeParameter = getTypeParameterResolution(node, typeState);
if (typeParameter) {
return isPromisedTypeAnnotation(typeParameter.type, context, scope, {
visitedTypeReferenceNames,
typeState: typeParameter.typeState,
});
}
if (
node?.type === 'TSTypeAnnotation'
|| node?.type === 'TSParenthesizedType'
) {
return isPromisedTypeAnnotation(node.typeAnnotation, context, scope, {visitedTypeReferenceNames, typeState});
}
if (node?.type === 'TSFunctionType') {
if (isCallableTypeAnnotation(node.returnType, context, scope, {typeState})) {
return false;
}
return isPromisedTypeAnnotation(node.returnType, context, scope, {visitedTypeReferenceNames, typeState});
}
if (node?.type === 'TSIntersectionType') {
const callableTypes = node.types.filter(type => isCallableTypeAnnotation(type, context, scope, {typeState}));
if (callableTypes.length > 0) {
return callableTypes.every(type => isPromisedTypeAnnotation(type, context, scope, {visitedTypeReferenceNames, typeState}));
}
return node.types.some(type => isPromisedTypeAnnotation(type, context, scope, {visitedTypeReferenceNames, typeState}));
}
if (node?.type === 'TSTypeLiteral') {
const callSignatures = node.members.filter(member => member.type === 'TSCallSignatureDeclaration');
return callSignatures.length > 0
&& callSignatures.every(member => isPromisedTypeAnnotation(member.returnType, context, scope, {visitedTypeReferenceNames, typeState}));
}
if (node?.type === 'TSUnionType') {
const types = node.types.filter(type => !nullishTypeAnnotationTypes.has(type.type));
return types.length > 0 && types.every(type => isPromisedTypeAnnotation(type, context, scope, {visitedTypeReferenceNames, typeState}));
}
return node?.type === 'TSTypeReference'
&& isPromisedTypeReference(node, context, scope, {visitedTypeReferenceNames, typeState});
}
function isCallableTypeAnnotation(node, context, scope, {visitedTypeReferenceNodes = new Set(), typeState = getTypeState()} = {}) {
if (hasMissingRequiredTypeArguments(node, scope)) {
return false;
}
const typeParameter = getTypeParameterResolution(node, typeState);
if (typeParameter) {
return isCallableTypeAnnotation(typeParameter.type, context, scope, {visitedTypeReferenceNodes, typeState: typeParameter.typeState});
}
if (isFunctionTypeAnnotation(node, context, scope)) {
return true;
}
if (node?.type === 'TSTypeAnnotation' || node?.type === 'TSParenthesizedType') {
return isCallableTypeAnnotation(node.typeAnnotation, context, scope, {visitedTypeReferenceNodes, typeState});
}
if (node?.type === 'TSUnionType') {
const types = node.types.filter(type => !nullishTypeAnnotationTypes.has(type.type));
return types.length > 0 && types.every(type => isCallableTypeAnnotation(type, context, scope, {visitedTypeReferenceNodes, typeState}));
}
if (node?.type === 'TSIntersectionType') {
return node.types.some(type => isCallableTypeAnnotation(type, context, scope, {visitedTypeReferenceNodes, typeState}));
}
if (node?.type !== 'TSTypeReference') {
return false;
}
if (node.typeName.type === 'TSQualifiedName') {
const {parserServices} = context.sourceCode;
if (!parserServices?.program) {
return false;
}
try {
return parserServices.getTypeAtLocation(node).getCallSignatures().length > 0;
} catch {
return false;
}
}
const name = getTypeReferenceName(node.typeName);
if (!name || visitedTypeReferenceNodes.has(node)) {
return false;
}
const definitions = getTypeDefinitions(name, scope);
if (definitions.length === 0) {
return false;
}
const nextVisitedTypeReferenceNodes = new Set(visitedTypeReferenceNodes);
nextVisitedTypeReferenceNodes.add(node);
return definitions.some(definition => {
const definitionScope = context.sourceCode.getScope(definition.node);
const definitionTypeState = {
...typeState,
typeParameterTypes: getTypeParameterTypes(definition.node, getTypeArguments(node), typeState),
};
if (definition.node.type === 'TSInterfaceDeclaration') {
return getInterfaceCallSignatureBooleanStates(definition.node, context, definitionScope, {
typeState: definitionTypeState,
getReturnTypeBooleanState: () => unknown,
visitedInterfaceNames: new Set([name]),
}).length > 0;
}
return definition.node.type === 'TSTypeAliasDeclaration'
&& isCallableTypeAnnotation(definition.node.typeAnnotation, context, definitionScope, {
visitedTypeReferenceNodes: nextVisitedTypeReferenceNodes,
typeState: definitionTypeState,
});
});
}
function isBooleanTypeAnnotatedValue(node, context) {
const {typeAnnotation} = node;
if (!typeAnnotation) {
return false;
}
const scope = context.sourceCode.getScope(node);
if (hasMissingRequiredTypeArguments(typeAnnotation, scope)) {
return false;
}
return isBooleanTypeAnnotation(typeAnnotation, context, scope)
|| isBooleanFunctionLikeTypeAnnotation(typeAnnotation, context, scope)
|| (
!isFunctionTypeAnnotation(typeAnnotation, context, scope)
&& getDirectTypeAnnotationBooleanState(typeAnnotation, context, scope, {allowNullish: false}) === boolean
);
}
const getTypeState = typeState => ({
visitedTypeReferenceNodes: new Set(),
visitedTypeParameterNames: new Set(),
functionTypesAreBoolean: true,
allowNullish: true,
typeParameterTypes: new Map(),
...typeState,
});
function getTypeParameterResolution(node, typeState) {
let currentNode = node;
let currentTypeState = typeState;
for (;;) {
const name = getTypeReferenceName(currentNode?.typeName);
if (!name || currentTypeState.visitedTypeParameterNames.has(name)) {
return;
}
const typeParameterType = currentTypeState.typeParameterTypes.get(name);
if (!typeParameterType) {
return;
}
const visitedTypeParameterNames = new Set(currentTypeState.visitedTypeParameterNames);
visitedTypeParameterNames.add(name);
const nextTypeState = {
...currentTypeState,
visitedTypeParameterNames,
};
const nextName = getTypeReferenceName(typeParameterType?.typeName);
if (
!nextName
|| nextTypeState.visitedTypeParameterNames.has(nextName)
|| !nextTypeState.typeParameterTypes.get(nextName)
) {
return {
type: typeParameterType,
typeState: hasTypeParameterReference(typeParameterType, name)
? nextTypeState
: {...nextTypeState, visitedTypeParameterNames: new Set()},
};
}
currentNode = typeParameterType;
currentTypeState = nextTypeState;
}
}
function hasTypeParameterReference(node, name) {
const nodes = [node];
const visitedNodes = new Set();
while (nodes.length > 0) {
const currentNode = nodes.pop();
if (currentNode && typeof currentNode === 'object' && !visitedNodes.has(currentNode)) {
visitedNodes.add(currentNode);
if (currentNode.type === 'TSTypeReference' && getTypeReferenceName(currentNode.typeName) === name) {
return true;
}
for (const [key, value] of Object.entries(currentNode)) {
if (key !== 'parent') {
if (Array.isArray(value)) {
nodes.push(...value);
} else {
nodes.push(value);
}
}
}
}
}
return false;
}
function resolveTypeParameterType(node, typeState, resolvedTypeParameterTypes = new Set(), resolvedTypeParameterNames = new Set(), visitedNodes = new Set()) {
const createResolveTask = (node, state) => ({kind: 'resolve', node, ...state});
const initialState = {
typeState, resolvedTypeParameterTypes, resolvedTypeParameterNames, visitedNodes,
};
const stack = [createResolveTask(node, initialState)];
const results = [];
const addTasks = tasks => {
stack.push(...tasks);
};
const processTask = task => {
switch (task.kind) {
case 'combineAnnotation': {
const typeAnnotation = results.pop();
if (typeAnnotation === task.node.typeAnnotation) {
results.push(task.node);
} else {
results.push({
...task.node,
typeAnnotation,
});
}
return;
}
case 'combineTypes': {
const types = results.splice(results.length - task.types.length);
if (types.some((type, index) => type !== task.types[index])) {
results.push({...task.node, types});
} else {
results.push(task.node);
}
return;
}
case 'combineTypeArguments': {
const resolvedTypeArguments = results.splice(results.length - task.typeArguments.length);
if (resolvedTypeArguments.some((type, index) => type !== task.typeArguments[index])) {
results.push({
...task.node,
[task.typeArgumentsProperty]: {
...task.node[task.typeArgumentsProperty],
params: resolvedTypeArguments,
},
});
} else {
results.push(task.node);
}
return;
}
default: {
const {
node,
typeState,
resolvedTypeParameterTypes,
resolvedTypeParameterNames,
visitedNodes,
} = task;
if (!node || typeof node !== 'object' || visitedNodes.has(node)) {
results.push(node);
return;
}
const nextVisitedNodes = new Set(visitedNodes);
nextVisitedNodes.add(node);
const typeParameter = getTypeParameterResolution(node, typeState);
if (typeParameter) {
const name = getTypeReferenceName(node.typeName);
if (resolvedTypeParameterTypes.has(typeParameter.type) || resolvedTypeParameterNames.has(name)) {
results.push(node);
return;
}
const nextResolvedTypeParameterTypes = new Set(resolvedTypeParameterTypes);
nextResolvedTypeParameterTypes.add(typeParameter.type);
const nextResolvedTypeParameterNames = new Set(resolvedTypeParameterNames);
nextResolvedTypeParameterNames.add(name);
addTasks([createResolveTask(typeParameter.type, {
typeState: typeParameter.typeState,
resolvedTypeParameterTypes: nextResolvedTypeParameterTypes,
resolvedTypeParameterNames: nextResolvedTypeParameterNames,
visitedNodes: nextVisitedNodes,
})]);
return;
}
if (
node.type === 'TSTypeAnnotation'
|| node.type === 'TSParenthesizedType'
) {
addTasks([
{kind: 'combineAnnotation', node},
createResolveTask(node.typeAnnotation, {
typeState,
resolvedTypeParameterTypes,
resolvedTypeParameterNames,
visitedNodes: nextVisitedNodes,
}),
]);
return;
}
if (
node.type === 'TSUnionType'
|| node.type === 'TSIntersectionType'
) {
addTasks([
{kind: 'combineTypes', node, types: node.types},
...node.types.toReversed().map(type => createResolveTask(type, {
typeState,
resolvedTypeParameterTypes,
resolvedTypeParameterNames,
visitedNodes: nextVisitedNodes,
})),
]);
return;
}
const typeArguments = node.type === 'TSTypeReference' ? getTypeArguments(node) : undefined;
if (!typeArguments) {
results.push(node);
return;
}
const typeArgumentsProperty = node.typeArguments ? 'typeArguments' : 'typeParameters';
addTasks([
{
kind: 'combineTypeArguments', node, typeArguments, typeArgumentsProperty,
},
...typeArguments.toReversed().map(type => createResolveTask(type, {
typeState,
resolvedTypeParameterTypes,
resolvedTypeParameterNames,
visitedNodes: nextVisitedNodes,
})),
]);
}
}
};
while (stack.length > 0) {
processTask(stack.pop());
}
return results[0];
}
function getTypeParameterTypes(definitionNode, typeArguments, typeState) {
const typeParameterTypes = new Map(typeState.typeParameterTypes);
for (const [index, parameter] of (definitionNode.typeParameters?.params ?? []).entries()) {
const typeArgument = typeArguments?.[index];
if (typeArgument) {
typeParameterTypes.set(parameter.name.name, resolveTypeParameterType(typeArgument, typeState));
} else if (parameter.default) {
typeParameterTypes.set(parameter.name.name, resolveTypeParameterType(parameter.default, {
...typeState,
typeParameterTypes,
}));
} else {
typeParameterTypes.delete(parameter.name.name);
}
}
return typeParameterTypes;
}
const hasUnresolvedTypeParameters = typeState =>
typeState.typeParameterTypes.entries().some(([name, type]) => hasTypeParameterReference(type, name));
const hasTypeParameterReferenceInType = (node, typeState) =>
typeState.typeParameterTypes.keys().some(name => hasTypeParameterReference(node, name));
function hasUnresolvedTypeParameterReference(node, typeState, scope, checkNode = true) {
const nodes = [{node, checkNode}];
const visitedNodes = new Set();
while (nodes.length > 0) {
const current = nodes.pop();
if (!current.node || typeof current.node !== 'object' || visitedNodes.has(current.node)) {
continue;
}
visitedNodes.add(current.node);
const name = current.node.type === 'TSTypeReference' ? getTypeReferenceName(current.node.typeName) : undefined;
if (
current.checkNode
&& name
&& !getTypeParameterResolution(current.node, typeState)
&& getTypeDefinitions(name, scope)
.some(definition => definition.node.type === 'TSTypeParameter')
) {
return true;
}
const childNodes = Object.entries(current.node)
.filter(([key]) => key !== 'parent')
.flatMap(([, value]) => Array.isArray(value) ? value : [value]);
nodes.push(...childNodes.map(child => ({node: child, checkNode: true})));
}
return false;
}
function canUseTypeInformationFallback(node, typeState, scope, definitions) {
return !hasTypeParameterReferenceInType(node, typeState)
&& !hasUnresolvedTypeParameters(typeState)
&& !hasUnresolvedTypeParameterReference(node, typeState, scope)
&& (
definitions.some(definition =>
['TSInterfaceDeclaration', 'TSTypeAliasDeclaration'].includes(definition.node.type),
)
|| node?.typeName?.type === 'TSQualifiedName'
);
}
function getTypeMembersBooleanState(members, context, scope, typeState) {
const normalizedTypeState = getTypeState(typeState);
const callSignatures = members.filter(member => member.type === 'TSCallSignatureDeclaration');
if (callSignatures.length > 0) {
return normalizedTypeState.functionTypesAreBoolean
? combineBooleanStates(callSignatures.map(member => getTypeAnnotationBooleanState(member.returnType, context, scope, {...normalizedTypeState, functionTypesAreBoolean: false})))
: nonBoolean;
}
return members.length > 0 ? nonBoolean : unknown;
}
function getInterfaceCallSignatureBooleanStates(interfaceNode, context, scope, {typeState, getReturnTypeBooleanState, visitedInterfaceNames = new Set()}) {
const normalizedTypeState = getTypeState(typeState);
let callSignatureStates = [];
if (normalizedTypeState.functionTypesAreBoolean) {
callSignatureStates = interfaceNode.body.body
.filter(member => member.type === 'TSCallSignatureDeclaration')
.map(member => getReturnTypeBooleanState(member.returnType, context, scope, normalizedTypeState));
} else if (interfaceNode.body.body.some(member => member.type === 'TSCallSignatureDeclaration')) {
callSignatureStates = [nonBoolean];
}
for (const heritage of interfaceNode.extends ?? []) {
const name = getTypeReferenceName(heritage.expression);
if (!name || visitedInterfaceNames.has(name)) {
continue;
}
const nextVisitedInterfaceNames = new Set(visitedInterfaceNames);
nextVisitedInterfaceNames.add(name);
for (const definition of getTypeDefinitions(name, scope)) {
const definitionScope = context.sourceCode.getScope(definition.node);
const definitionTypeState = {
...normalizedTypeState,
typeParameterTypes: getTypeParameterTypes(definition.node, getTypeArguments(heritage), normalizedTypeState),
};
if (definition.node.type === 'TSInterfaceDeclaration') {
callSignatureStates.push(...getInterfaceCallSignatureBooleanStates(definition.node, context, definitionScope, {
typeState: definitionTypeState,
getReturnTypeBooleanState,
visitedInterfaceNames: nextVisitedInterfaceNames,
}));
} else if (definition.node.type === 'TSTypeAliasDeclaration') {
callSignatureStates.push(...getCallSignatureReturnTypes(definition.node.typeAnnotation, context, definitionScope, {typeState: definitionTypeState}).map(returnType =>
getReturnTypeBooleanState(returnType, context, definitionScope, definitionTypeState),
));
}
}
}
return callSignatureStates;
}
function getTypeReferenceBooleanState(node, context, scope, typeState) {
if (isGlobalPromiseTypeReference(node, scope)) {
return nonBoolean;
}
const normalizedTypeState = getTypeState(typeState);
const {visitedTypeReferenceNodes} = normalizedTypeState;
const name = getTypeReferenceName(node.typeName ?? node.expression);
if (!name || visitedTypeReferenceNodes.has(node)) {
return unknown;
}
visitedTypeReferenceNodes.add(node);
const definitions = getTypeDefinitions(name, scope);
let result = unknown;
const interfaceDefinitions = getInterfaceDefinitions(name, scope);
if (interfaceDefinitions.length > 0) {
const callSignatureStates = interfaceDefinitions.flatMap(definition => {
const definitionScope = context.sourceCode.getScope(definition.node);
const definitionTypeState = {
...normalizedTypeState,
typeParameterTypes: getTypeParameterTypes(definition.node, getTypeArguments(node), normalizedTypeState),
};
return getInterfaceCallSignatureBooleanStates(definition.node, context, definitionScope, {
typeState: definitionTypeState,
getReturnTypeBooleanState: (returnType, context, scope, typeState) =>
getTypeAnnotationBooleanState(returnType, context, scope, {...typeState, functionTypesAreBoolean: false}),
visitedInterfaceNames: new Set([name]),
});
});
result = callSignatureStates.length > 0
? combineBooleanStates(callSignatureStates)
: getTypeMembersBooleanState(
interfaceDefinitions.flatMap(definition => definition.node.body.body),
context,
context.sourceCode.getScope(interfaceDefinitions[0].node),
normalizedTypeState,
);
} else {
const [definition] = definitions;
if (definition?.node.type === 'TSTypeAliasDeclaration') {
const definitionScope = context.sourceCode.getScope(definition.node);
const definitionTypeState = {
...normalizedTypeState,
typeParameterTypes: getTypeParameterTypes(definition.node, getTypeArguments(node), normalizedTypeState),
};
result = getDirectTypeAnnotationBooleanState(definition.node.typeAnnotation, context, definitionScope, definitionTypeState);
}
}
visitedTypeReferenceNodes.delete(node);
if (
result === unknown
&& context.sourceCode.parserServices?.program
&& canUseTypeInformationFallback(node, normalizedTypeState, scope, definitions)
) {
result = getTypeInformationBooleanState(node, context, normalizedTypeState.functionTypesAreBoolean, normalizedTypeState.allowNullish);
}
return result;
}
function getUnionTypeAnnotationBooleanState(node, context, scope, typeState) {
const normalizedTypeState = getTypeState(typeState);
const states = node.types.map(type => getTypeAnnotationBooleanState(type, context, scope, normalizedTypeState));
const nonNullishStates = states.filter((_, index) => !nullishTypeAnnotationTypes.has(node.types[index].type));
const nonNullishState = combineBooleanStates(nonNullishStates);
if (!normalizedTypeState.allowNullish && nonNullishState === nonBoolean) {
return nonBoolean;
}
return combineBooleanStates(normalizedTypeState.allowNullish ? nonNullishStates : states);
}
function getSimpleTypeAnnotationBooleanState(node) {
if (!node) {
return unknown;
}
if (
nullishTypeAnnotationTypes.has(node.type)
|| unknownTypeAnnotationTypes.has(node.type)
) {
return unknown;
}
if (node.type === 'TSBooleanKeyword') {
return boolean;
}
if (node.type === 'TSLiteralType') {
return typeof node.literal.value === 'boolean' ? boolean : nonBoolean;
}
if (node.type === 'TSTypePredicate') {
return node.asserts ? nonBoolean : boolean;
}
if (node.type === 'TypeAnnotation') {
return node.typeAnnotation?.type === 'BooleanTypeAnnotation' ? boolean : nonBoolean;
}
return nonBoolean;
}
function getTypeAnnotationBooleanState(node, context, scope, typeState) {
if (hasMissingRequiredTypeArguments(node, scope)) {
return unknown;
}
const normalizedTypeState = getTypeState(typeState);
if (
node?.type === 'TSTypeAnnotation'
|| node?.type === 'TSParenthesizedType'
) {
return getTypeAnnotationBooleanState(node.typeAnnotation, context, scope, normalizedTypeState);
}
if (node?.type === 'TSTypeReference') {
const typeParameter = getTypeParameterResolution(node, normalizedTypeState);
if (typeParameter) {
return getTypeAnnotationBooleanState(typeParameter.type, context, scope, typeParameter.typeState);
}
}
if (node?.type === 'TSFunctionType') {
return normalizedTypeState.functionTypesAreBoolean
? getTypeAnnotationBooleanState(node.returnType, context, scope, {...normalizedTypeState, functionTypesAreBoolean: false})
: nonBoolean;
}
if (node?.type === 'TSUnionType') {
return getUnionTypeAnnotationBooleanState(node, context, scope, normalizedTypeState);
}
if (node?.type === 'TSIntersectionType') {
const callableTypes = node.types.filter(type => isCallableTypeAnnotation(type, context, scope, {typeState: normalizedTypeState}));
if (callableTypes.length === 0 || !normalizedTypeState.functionTypesAreBoolean) {
return nonBoolean;
}
return combineBooleanStates(callableTypes.map(type => getTypeAnnotationBooleanState(type, context, scope, normalizedTypeState)));
}
if (node?.type === 'TSTypeReference') {
return getTypeReferenceBooleanState(node, context, scope, normalizedTypeState);
}
if (node?.type === 'TSTypeLiteral') {
return getTypeMembersBooleanState(node.members, context, scope, normalizedTypeState);
}
return getSimpleTypeAnnotationBooleanState(node);
}
function getPromisedTypeReferenceBooleanState(node, context, scope, typeState) {
const normalizedTypeState = getTypeState(typeState);
const name = getTypeReferenceName(node.typeName ?? node.expression);
const typeParameter = getTypeParameterResolution(node, normalizedTypeState);
if (typeParameter) {
return getPromisedTypeAnnotationBooleanState(typeParameter.type, context, scope, typeParameter.typeState);
}
const typeArguments = getTypeArguments(node);
if (isGlobalPromiseTypeReference(node, scope)) {
return getTypeAnnotationBooleanState(typeArguments[0], context, scope, {
...normalizedTypeState,
functionTypesAreBoolean: false,
});
}
const {visitedTypeReferenceNodes} = normalizedTypeState;
if (!name || visitedTypeReferenceNodes.has(node)) {
return unknown;
}
visitedTypeReferenceNodes.add(node);
const definitions = getTypeDefinitions(name, scope);
let result = unknown;
const interfaceDefinitions = getInterfaceDefinitions(name, scope);
if (interfaceDefinitions.length > 0) {
const callSignatureStates = interfaceDefinitions.flatMap(definition => {
const definitionScope = context.sourceCode.getScope(definition.node);
const definitionTypeState = {
...normalizedTypeState,
typeParameterTypes: getTypeParameterTypes(definition.node, typeArguments, normalizedTypeState),
};
return getInterfaceCallSignatureBooleanStates(definition.node, context, definitionScope, {
typeState: definitionTypeState,
getReturnTypeBooleanState: (returnType, context, scope, typeState) =>
getPromisedTypeAnnotationBooleanState(returnType, context, scope, typeState),
visitedInterfaceNames: new Set([name]),
});
});
result = callSignatureStates.length > 0
? combineBooleanStates(callSignatureStates)
: getPromisedTypeMembersBooleanState(
interfaceDefinitions.flatMap(definition => definition.node.body.body),
context,
context.sourceCode.getScope(interfaceDefinitions[0].node),
normalizedTypeState,
);
} else {
const [definition] = definitions;
if (definition?.node.type === 'TSTypeAliasDeclaration') {
const definitionScope = context.sourceCode.getScope(definition.node);
const definitionTypeState = {
...normalizedTypeState,
typeParameterTypes: getTypeParameterTypes(definition.node, typeArguments, normalizedTypeState),
};
result = getPromisedTypeAnnotationBooleanState(definition.node.typeAnnotation, context, definitionScope, definitionTypeState);
}
}
visitedTypeReferenceNodes.delete(node);
if (
result === unknown
&& context.sourceCode.parserServices?.program
&& canUseTypeInformationFallback(node, normalizedTypeState, scope, definitions)
) {
result = interfaceDefinitions.length > 0
? getAsyncFunctionTypeInformationBooleanState(node, context, normalizedTypeState.allowNullish)
: getPromisedTypeInformationBooleanState(node, context, normalizedTypeState.allowNullish);
}
return result;
}
function getPromisedTypeAnnotationBooleanState(node, context, scope, typeState) {
if (hasMissingRequiredTypeArguments(node, scope)) {
return unknown;
}
const normalizedTypeState = getTypeState(typeState);
if (
node?.type === 'TSTypeAnnotation'
|| node?.type === 'TSParenthesizedType'
) {
return getPromisedTypeAnnotationBooleanState(node.typeAnnotation, context, scope, normalizedTypeState);
}
if (node?.type === 'TSFunctionType') {
if (!normalizedTypeState.functionTypesAreBoolean) {
return nonBoolean;
}
if (isCallableTypeAnnotation(node.returnType, context, scope, {typeState: normalizedTypeState})) {
return unknown;
}
return getPromisedTypeAnnotationBooleanState(node.returnType, context, scope, normalizedTypeState);
}
if (node?.type === 'TSUnionType') {
return combineBooleanStates(
node.types
.filter(type => !nullishTypeAnnotationTypes.has(type.type) || !normalizedTypeState.allowNullish)
.map(type => getPromisedTypeAnnotationBooleanState(type, context, scope, normalizedTypeState)),
);
}
if (node?.type === 'TSIntersectionType') {
const callableTypes = node.types.filter(type => isCallableTypeAnnotation(type, context, scope, {typeState: normalizedTypeState}));
if (callableTypes.length > 0) {
if (!normalizedTypeState.functionTypesAreBoolean) {
return nonBoolean;
}
return combineBooleanStates(callableTypes.map(type => getPromisedTypeAnnotationBooleanState(type, context, scope, normalizedTypeState)));
}
const promisedTypes = node.types.filter(type => isPromisedTypeAnnotation(type, context, scope, {typeState: normalizedTypeState}));
return promisedTypes.length > 0
? combineBooleanStates(promisedTypes.map(type => getPromisedTypeAnnotationBooleanState(type, context, scope, normalizedTypeState)))
: unknown;
}
if (node?.type === 'TSTypeLiteral') {
return getPromisedTypeMembersBooleanState(node.members, context, scope, normalizedTypeState);
}
return node?.type === 'TSTypeReference'
? getPromisedTypeReferenceBooleanState(node, context, scope, normalizedTypeState)
: unknown;
}
function getPromisedTypeMembersBooleanState(members, context, scope, typeState) {
const normalizedTypeState = getTypeState(typeState);
const callSignatures = members.filter(member => member.type === 'TSCallSignatureDeclaration');
if (callSignatures.length > 0) {
return normalizedTypeState.functionTypesAreBoolean
? combineBooleanStates(callSignatures.map(member => getPromisedTypeAnnotationBooleanState(member.returnType, context, scope, normalizedTypeState)))
: nonBoolean;
}
return members.length > 0 ? nonBoolean : unknown;
}
function isGlobalTypeReferenceName(name, scope) {
return getTypeDefinitions(name, scope).length === 0;
}
function isGlobalPromiseTypeReference(node, scope) {
if (node?.type !== 'TSTypeReference') {
return false;
}
const typeName = getTypeReferenceName(node.typeName);
return promiseValueTypeNames.has(typeName)
&& getTypeArguments(node)?.length === 1
&& isGlobalTypeReferenceName(typeName, scope);
}
function isGlobalPromiseTypeAnnotation(node, scope) {
if (
node?.type === 'TSTypeAnnotation'
|| node?.type === 'TSParenthesizedType'
) {
return isGlobalPromiseTypeAnnotation(node.typeAnnotation, scope);
}
if (node?.type === 'TSUnionType') {
const types = node.types.filter(type => !nullishTypeAnnotationTypes.has(type.type));
return types.length > 0 && types.every(type => isGlobalPromiseTypeAnnotation(type, scope));
}
return isGlobalPromiseTypeReference(node, scope);
}
function getAsyncFunctionTypeInformationBooleanState(node, context, allowNullish = true) {
const {parserServices} = context.sourceCode;
if (!parserServices?.program) {
return unknown;
}
while (
node?.type === 'TSTypeAnnotation'
|| node?.type === 'TSParenthesizedType'
) {
node = node.typeAnnotation;
}
try {
const checker = parserServices.program.getTypeChecker();
const typeScriptNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const signature = isFunction(node) ? checker.getSignatureFromDeclaration(typeScriptNode) : undefined;
const signatures = signature ? [signature] : parserServices.getTypeAtLocation(node).getCallSignatures();
return combineBooleanStates(signatures.map(signature => {
const returnType = checker.getReturnTypeOfSignature(signature);
const nonNullableReturnType = checker.getNonNullableType(returnType);
if (!allowNullish && nonNullableReturnType !== returnType) {
return unknown;
}
const promisedType = checker.getPromisedTypeOfPromise(nonNullableReturnType);
if (!promisedType) {
return unknown;
}
const nonNullableType = checker.getNonNullableType(promisedType);
if (!allowNullish && nonNullableType !== promisedType) {
return unknown;
}
return getTypeBooleanState(nonNullableType, checker, new Set(), false);
}));
} catch {
return unknown;
}
}
function getFunctionBooleanState(node, context, visitedVariables = new Set(), isAsync = node.async) {
if (node.generator) {
return nonBoolean;
}
const scope = context.sourceCode.getScope(node);
const hasUnresolvedReturnType = isAsync
&& hasUnresolvedTypeParameterReference(node.returnType, getTypeState(), scope, false);
// Only actual async function implementations and their overload signatures get `Promise<T>` unwrapped in function-body analysis. Promise-valued variables are not predicates; type-only callable signatures are handled separately by direct annotation analysis.
const stateFromPromisedReturnType = isAsync && !hasUnresolvedReturnType
? getPromisedReturnTypeBooleanState(node.returnType, context, scope)
: unknown;
if (stateFromPromisedReturnType !== unknown) {
return stateFromPromisedReturnType;
}
const stateFromReturnType = isAsync ? unknown : getTypeAnnotationBooleanState(node.returnType, context, scope, {functionTypesAreBoolean: false});
if (stateFromReturnType !== unknown) {
return stateFromReturnType;
}
const stateFromTypeInformation = isAsync
? (hasUnresolvedReturnType
? unknown
: getAsyncFunctionTypeInformationBooleanState(node, context, false))
: getTypeInformationBooleanState(node, context);
if (stateFromTypeInformation !== unknown) {
return stateFromTypeInformation;
}
if (
isAsync
&& !hasUnresolvedReturnType
&& getAsyncFunctionTypeInformationBooleanState(node, context) === nonBoolean
) {
return nonBoolean;
}
if (isAsync && (hasUnresolvedReturnType || hasNullableType(node, context))) {
return unknown;
}
if (!node.body) {
return unknown;
}
if (node.body.type === 'BlockStatement') {
if (node.body.body.length === 0) {
return nonBoolean;
}
if (
node.body.body.length === 1
&& node.body.body[0].type === 'ReturnStatement'
) {
return node.body.body[0].argument
? getExpressionBooleanState(node.body.body[0].argument, context, visitedVariables, false)
: nonBoolean;
}
}
return node.type === 'ArrowFunctionExpression' && node.body.type !== 'BlockStatement'
? getExpressionBooleanState(node.body, context, visitedVariables, false)
: unknown;
}
function getKnownIdentifierBooleanState(node, context, visitedVariables, functionValuesAreBoolean) {
const variable = findVariable(context.sourceCode.getScope(node), node);
return variable ? getVariableBooleanState(variable, context, visitedVariables, functionValuesAreBoolean) : unknown;
}
function getStaticExpressionBooleanState(node, scope) {
if (node.type === 'Identifier') {
return unknown;
}
const staticValue = getStaticValue(node, scope)?.value;
return staticValue === undefined
? unknown
: (typeof staticValue === 'boolean' ? boolean : nonBoolean);
}
function getSimpleExpressionBooleanState(node) {
if (nonBooleanExpressionTypes.has(node.type)) {
return nonBoolean;
}
if (node.type === 'Literal') {
return node.value === null ? unknown : nonBoolean;
}
if (node.type === 'UnaryExpression') {
return ['!', 'delete'].includes(node.operator) ? boolean : nonBoolean;
}
if (node.type === 'BinaryExpression') {
return booleanBinaryOperators.has(node.operator) ? boolean : nonBoolean;
}
return unknown;
}
function getWrappedExpression(node) {
if (expressionWrapperTypes.has(node.type)) {
return node.argument ?? node.expression;
}
if (typeScriptExpressionWrapperTypes.has(node.type)) {
return node.expression;
}
}
function getDerivedExpressionBooleanState(node, context, visitedVariables, functionValuesAreBoolean) {
if (node.type === 'Identifier') {
return getKnownIdentifierBooleanState(node, context, visitedVariables, functionValuesAreBoolean);
}
const wrappedExpression = getWrappedExpression(node);
if (wrappedExpression) {
return getExpressionBooleanState(wrappedExpression, context, visitedVariables, functionValuesAreBoolean);
}
if (node.type === 'AssignmentExpression') {
return node.operator === '=' ? getExpressionBooleanState(node.right, context, visitedVariables, functionValuesAreBoolean) : unknown;
}
if (node.type === 'SequenceExpression') {
return getExpressionBooleanState(node.expressions.at(-1), context, visitedVariables, functionValuesAreBoolean);
}
if (node.type === 'ConditionalExpression') {
return combineBooleanStates([
getExpressionBooleanState(node.consequent, context, visitedVariables, functionValuesAreBoolean),
getExpressionBooleanState(node.alternate, context, visitedVariables, functionValuesAreBoolean),
]);
}
return unknown;
}
function getExpressionBooleanState(node, context, visitedVariables = new Set(), functionValuesAreBoolean = true) {
if (!node) {
return unknown;
}
if (isFunction(node)) {
return functionValuesAreBoolean
? getFunctionBooleanState(node, context, visitedVariables)
: nonBoolean;
}
const scope = context.sourceCode.getScope(node);
const typeState = getTypeState();
const stateFromTypeInformation = hasUnresolvedTypeParameterReference(node.typeAnnotation, typeState, scope, false)
? unknown
: getTypeInformationBooleanState(node, context, functionValuesAreBoolean);
if (stateFromTypeInformation !== unknown) {
return stateFromTypeInformation;
}
const stateFromTypeAnnotation = getDirectTypeAnnotationBooleanState(node.typeAnnotation, context, scope, {functionTypesAreBoolean: functionValuesAreBoolean});
if (stateFromTypeAnnotation !== unknown) {
return stateFromTypeAnnotation;
}
if (isBooleanExpression(node, context, visitedVariables)) {
return boolean;
}
const stateFromStaticValue = getStaticExpressionBooleanState(node, scope);
if (stateFromStaticValue !== unknown) {
return stateFromStaticValue;
}
const stateFromSimpleExpression = getSimpleExpressionBooleanState(node);
if (stateFromSimpleExpression !== unknown) {
return stateFromSimpleExpression;
}
return getDerivedExpressionBooleanState(node, context, visitedVariables, functionValuesAreBoolean);
}
const isBooleanVariable = (variable, context) => {
const {sourceCode} = context;
const functionDefinitions = getFunctionDefinitions(variable);
if (functionDefinitions) {
const hasAsyncImplementation = hasAsyncFunctionImplementation(variable.defs);
return functionDefinitions.every(definition => isBooleanFunctionDefinition(definition, context, hasAsyncImplementation));
}
const definition = getSupportedVariableDefinition(variable);
if (!definition) {
return false;
}
const {name} = definition;
if (name.typeAnnotation) {
return isBooleanTypeAnnotatedValue(name, context);
}
if (definition.type === 'Parameter') {
// Some parsers (such as Svelte's `{#each}` bindings) create `Parameter` definitions whose owner is not a function and has no `params` list.
if (!isFunction(definition.node)) {
return false;
}
const parameter = findParameter(definition.node.params, name);
return parameter?.type === 'AssignmentPattern'
&& isBooleanExpression(parameter.right, context);
}
if (isBooleanExpression(name, context)) {
return true;
}
if (definition.type === 'Variable') {
return isBooleanValue(definition.node.init, context);
}
return definition.type === 'FunctionName' && isBooleanFunctionDefinition(definition, context);
};
function getParameterBooleanState(definition, context, visitedVariables, functionValuesAreBoolean) {
if (!isFunction(definition.node)) {
return unknown;
}
const parameter = findParameter(definition.node.params, definition.name);
return parameter?.type === 'AssignmentPattern'
? getExpressionBooleanState(parameter.right, context, visitedVariables, functionValuesAreBoolean)
: unknown;
}
function getDirectTypeAnnotationBooleanState(node, context, scope, typeState) {
if (hasMissingRequiredTypeArguments(node, scope)) {
return unknown;
}
if (isGlobalPromiseTypeAnnotation(node, scope)) {
return nonBoolean;
}
const normalizedTypeState = getTypeState(typeState);
const stateFromAsyncFunctionType = getAsyncFunctionTypeAnnotationBooleanState(node, context, scope, normalizedTypeState);
if (stateFromAsyncFunctionType !== unknown) {
return stateFromAsyncFunctionType;
}
if (
isCallableTypeAnnotation(node, context, scope)
&& isPromisedTypeAnnotation(node, context, scope)
) {
return unknown;
}
return getTypeAnnotationBooleanState(node, context, scope, normalizedTypeState);
}
function getDefinitionBooleanState(definition, context, visitedVariables, functionValuesAreBoolean) {
const scope = context.sourceCode.getScope(definition.name);
const stateFromTypeAnnotation = getDirectTypeAnnotationBooleanState(definition.name.typeAnnotation, context, scope, {functionTypesAreBoolean: functionValuesAreBoolean});
if (stateFromTypeAnnotation !== unknown) {
return stateFromTypeAnnotation;
}
if (definition.type === 'Parameter') {
return getParameterBooleanState(definition, context, visitedVariables, functionValuesAreBoolean);
}
if (definition.type === 'Variable') {
if (definition.node.parent.parent?.type === 'ForInStatement') {
return nonBoolean;
}
return getExpressionBooleanState(definition.node.init, context, visitedVariables, functionValuesAreBoolean);
}
if (definition.type === 'FunctionName') {
return functionValuesAreBoolean
? getFunctionBooleanState(definition.node, context, visitedVariables)
: nonBoolean;
}
return unknown;
}
function getReferenceWriteBooleanState(reference, context, visitedVariables, functionValuesAreBoolean) {
const {parent} = reference.identifier;
if (
parent.type === 'ForInStatement'
|| parent.type === 'UpdateExpression'
|| (
parent.type === 'AssignmentExpression'
&& parent.operator !== '='
&& !logicalAssignmentOperators.has(parent.operator)
)
) {
return nonBoolean;
}
if (parent.type !== 'AssignmentExpression') {
return unknown;
}
return getExpressionBooleanState(reference.writeExpr, context, visitedVariables, functionValuesAreBoolean);
}
function getVariableBooleanState(variable, context, visitedVariables = new Set(), functionValuesAreBoolean = true) {
if (!variable || visitedVariables.has(variable)) {
return unknown;
}
visitedVariables.add(variable);
const functionDefinitions = getFunctionDefinitions(variable);
const definition = getSupportedVariableDefinition(variable);
if (
!functionDefinitions
&& !definition
) {
visitedVariables.delete(variable);
return unknown;
}
let result;
if (functionDefinitions && !functionValuesAreBoolean) {
result = nonBoolean;
} else if (functionDefinitions) {
if (hasGeneratorFunctionImplementation(variable.defs)) {
result = nonBoolean;
} else {
const hasAsyncImplementation = hasAsyncFunctionImplementation(variable.defs);
result = combineBooleanStates(functionDefinitions.map(definition =>
getFunctionBooleanState(definition.node, context, visitedVariables, hasAsyncImplementation || definition.node.async),
));
}
} else {
result = getDefinitionBooleanState(definition, context, visitedVariables, functionValuesAreBoolean);
}
const writeStates = variable.references
.filter(reference => reference.writeExpr || ['ForInStatement', 'UpdateExpression'].includes(reference.identifier.parent.type))
.map(reference => getReferenceWriteBooleanState(reference, context, visitedVariables, functionValuesAreBoolean));
if (writeStates.length > 0) {
result = combineVariableBooleanStates([
result,
...writeStates,
]);
}
if (
result === unknown
&& functionValuesAreBoolean
&& isBooleanVariable(variable, context)
) {
result = boolean;
}
visitedVariables.delete(variable);
return result;
}
function getBooleanPropertyName(node, sourceCode) {
const propertyNameNode = node.type === 'TSParameterProperty' ? getParameterPropertyNameNode(node) : node.key;
if (
!node.computed
&& [
'Identifier',
'PrivateIdentifier',
].includes(propertyNameNode?.type)
) {
return propertyNameNode.name;
}
if (
propertyNameNode?.type === 'Literal'
&& typeof propertyNameNode.value === 'string'
) {
return propertyNameNode.value;
}
const name = getPropertyName(node, sourceCode.getScope(node));
return typeof name === 'string' ? name : undefined;
}
function getTypeScriptNameParts(node) {
if (node?.type === 'Identifier') {
return [node.name];
}
if (node?.type === 'Literal' && typeof node.value === 'string') {
return [node.value];
}
return node?.type === 'TSQualifiedName'
? [...getTypeScriptNameParts(node.left), ...getTypeScriptNameParts(node.right)]
: [];
}
function getTypeScriptIdentityPrefix(node) {
if (node.global) {
return 'global';
}
if (node.id.type === 'Literal') {
return 'module';
}
return 'namespace';
}
function getTypeScriptModuleIdentity(node) {
const namespaceNames = [];
const namespaceIdentityParts = [];
let isAmbient = false;
let isExternalModule = false;
let isGlobal = false;
for (let current = node; current; current = current.parent) {
if (current.type !== 'TSModuleDeclaration') {
continue;
}
isAmbient ||= current.declare || current.global || current.id.type === 'Literal';
isExternalModule ||= current.id.type === 'Literal';
isGlobal ||= current.global;
const names = getTypeScriptNameParts(current.id);
const identityPrefix = getTypeScriptIdentityPrefix(current);
namespaceNames.unshift(...names);
namespaceIdentityParts.unshift(...names.map(name => `${identityPrefix}:${name}`));
}
return {
namespaceNames,
namespaceIdentityParts,
isAmbient,
isExternalModule,
isGlobal,
};
}
function getMemberReportIdentity(node, sourceCode) {
if (typeScriptMemberTypes.has(node.type)) {
const interfaceNode = node.parent?.parent;
const interfaceName = interfaceNode?.type === 'TSInterfaceDeclaration' ? interfaceNode.id.name : undefined;
if (interfaceName) {
const {
namespaceNames,
namespaceIdentityParts,
isAmbient,
isExternalModule,
isGlobal,
} = getTypeScriptModuleIdentity(interfaceNode.parent);
const isExportedInterface = interfaceNode.parent?.type === 'ExportNamedDeclaration';
if (namespaceNames.length > 0 && !isAmbient && !isExportedInterface) {
return {owner: interfaceNode.parent, name: interfaceName};
}
const ownerName = namespaceNames[0] ?? interfaceName;
const namespaceIdentity = namespaceIdentityParts.join('/');
let owner = isExternalModule || isGlobal
? namespaceIdentity
: resolveVariableName(ownerName, sourceCode.getScope(node));
if (!owner && namespaceNames.length > 0) {
owner = namespaceIdentity;
}
if (owner) {
return {owner, name: [...namespaceNames, interfaceName].join('.')};
}
}
}
return {owner: node.parent};
}
const getMemberReportKey = node => [
node.static ? 'static' : 'instance',
node.key?.type === 'PrivateIdentifier' ? 'private' : 'public',
].join(':');
function getPromisedReturnTypeBooleanState(node, context, scope) {
if (hasUnresolvedTypeParameterReference(node, getTypeState(), scope, false)) {
return unknown;
}
const state = getPromisedTypeAnnotationBooleanState(node, context, scope, {functionTypesAreBoolean: false, allowNullish: false});
if (state !== unknown) {
return state;
}
return getPromisedTypeAnnotationBooleanState(node, context, scope, {functionTypesAreBoolean: false}) === nonBoolean
? nonBoolean
: unknown;
}
function getParameterPropertyNameNode(node) {
const parameter = unwrapParameter(node.parameter);
return parameter.type === 'AssignmentPattern' ? parameter.left : parameter;
}
function getShorthandVariable(node, sourceCode) {
if (
node.type !== 'Property'
|| !node.shorthand
|| node.parent.type !== 'ObjectExpression'
|| node.key.type !== 'Identifier'
) {
return;
}
return findVariable(sourceCode.getScope(node), node.key);
}
function isBooleanProperty(node, context) {
const {sourceCode} = context;
if (node.type === 'TSParameterProperty') {
return isBooleanVariable(findVariable(sourceCode.getScope(node), getParameterPropertyNameNode(node)), context);
}
if (node.type === 'Property') {
if (
node.parent.type !== 'ObjectExpression'
|| node.kind === 'set'
) {
return false;
}
return isBooleanValue(node.value, context);
}
if (methodDefinitionTypes.has(node.type)) {
return !['constructor', 'set'].includes(node.kind) && isBooleanFunctionValue(node.value, context);
}
if (propertyDefinitionTypes.has(node.type)) {
return isBooleanTypeAnnotatedValue(node, context)
|| isBooleanValue(node.value, context);
}
if (node.type === 'TSPropertySignature') {
return isBooleanTypeAnnotatedValue(node, context);
}
if (node.type === 'TSMethodSignature') {
if (isSetter(node)) {
return false;
}
const scope = sourceCode.getScope(node);
return getDirectTypeAnnotationBooleanState(node.returnType, context, scope, {functionTypesAreBoolean: false, allowNullish: false}) === boolean
|| getPromisedReturnTypeBooleanState(node.returnType, context, scope) === boolean;
}
return false;
}
function getExplicitPropertyBooleanState(node, context) {
const {sourceCode} = context;
if (node.type === 'TSParameterProperty') {
return getVariableBooleanState(findVariable(sourceCode.getScope(node), getParameterPropertyNameNode(node)), context);
}
if (node.type === 'Property') {
if (
node.parent.type !== 'ObjectExpression'
|| node.kind === 'set'
) {
return unknown;
}
if (node.shorthand) {
return getVariableBooleanState(getShorthandVariable(node, sourceCode), context);
}
return getExpressionBooleanState(node.value, context);
}
if (methodDefinitionTypes.has(node.type)) {
return ['constructor', 'set'].includes(node.kind) ? unknown : getFunctionBooleanState(node.value, context);
}
if (propertyDefinitionTypes.has(node.type)) {
const scope = sourceCode.getScope(node);
const stateFromTypeAnnotation = getDirectTypeAnnotationBooleanState(node.typeAnnotation, context, scope);
if (stateFromTypeAnnotation !== unknown) {
return stateFromTypeAnnotation;
}
return getExpressionBooleanState(node.value, context);
}
if (node.type === 'TSPropertySignature') {
return getDirectTypeAnnotationBooleanState(node.typeAnnotation, context, sourceCode.getScope(node), {allowNullish: false});
}
if (node.type === 'TSMethodSignature') {
if (isSetter(node)) {
return unknown;
}
const scope = sourceCode.getScope(node);
const hasUnresolvedReturnType = hasUnresolvedTypeParameterReference(node.returnType, getTypeState(), scope, false);
const stateFromPromisedReturnType = getPromisedReturnTypeBooleanState(node.returnType, context, scope);
return stateFromPromisedReturnType === unknown
&& !hasUnresolvedReturnType
&& !isPromisedTypeAnnotation(node.returnType, context, scope)
? getTypeAnnotationBooleanState(node.returnType, context, scope, {functionTypesAreBoolean: false, allowNullish: false})
: stateFromPromisedReturnType;
}
return unknown;
}
function getPropertyBooleanState(node, context) {
const state = getExplicitPropertyBooleanState(node, context);
return state === unknown && isBooleanProperty(node, context) ? boolean : state;
}
function getSuggestions(variable, prefixes, context, nameForPrefixCheck) {
const [definition] = variable.defs;
if (
!shouldSuggestRename(variable)
|| variable.references.some(reference => reference.vueUsedInTemplate)
|| (definition && isParameterPropertyDefinition(definition))
) {
return;
}
const scopes = [
...variable.references.map(reference => reference.from),
variable.scope,
];
const usedReplacements = new Set();
const suggestions = [];
for (const prefix of prefixes) {
const replacement = getAvailableVariableName(getReactHookReplacementName({name: variable.name, nameForPrefixCheck}, prefix), scopes);
if (!replacement || usedReplacements.has(replacement)) {
continue;
}
usedReplacements.add(replacement);
suggestions.push({
messageId: MESSAGE_ID_SUGGESTION,
data: {replacement},
fix: fixer => renameVariable(variable, replacement, context, fixer),
});
}
return suggestions.length > 0 ? suggestions : undefined;
}
function isAutofixableVariable(variable, context) {
const [definition] = variable.defs;
if (
variable.scope.type === 'global'
|| definition?.type !== 'Variable'
|| isInDeclareContext(definition.node)
|| isFunction(definition.node.init)
|| getVariableIdentifiers(variable).some(identifier =>
isExportedIdentifier(identifier)
|| isExportDefaultIdentifier(identifier)
|| isExportSpecifierLocal(identifier),
)
) {
return false;
}
const {sourceCode} = context;
const scope = sourceCode.getScope(definition.name);
return !isBooleanFunctionLikeTypeAnnotation(definition.name.typeAnnotation, context, scope)
&& !isBooleanFunctionReference(definition.node.init, context)
&& !isBooleanAsyncFunctionReference(definition.node.init, context);
}
function isInDeclareContext(node) {
for (let currentNode = node; currentNode; currentNode = currentNode.parent) {
if (currentNode.declare) {
return true;
}
}
return false;
}
function getAutofix({
variable,
prefixes,
context,
suggestions,
nameForPrefixCheck,
}) {
if (
!suggestions
|| !isAutofixableVariable(variable, context)
) {
return;
}
const [prefix] = prefixes;
const replacement = getReactHookReplacementName({name: variable.name, nameForPrefixCheck}, prefix);
const suggestion = suggestions.find(suggestion => suggestion.data.replacement === replacement);
return suggestion?.fix;
}
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const {
checkVariables,
checkArguments,
checkFunctions,
checkMethods,
checkFields,
prefixes,
ignore,
wrappers,
} = prepareOptions(context.options[0]);
if (prefixes.length === 0) {
return;
}
const checkVariable = variable => {
if (
isIgnoredName(variable.name, ignore)
|| isDestructuredVariable(variable)
) {
return;
}
const option = getVariableOption(variable);
const mode = variableModes[option];
if (!option || mode === NEVER) {
return;
}
const nameForPrefixCheck = getNameForPrefixCheck(variable, context);
const booleanPrefix = getBooleanPrefix(nameForPrefixCheck, prefixes);
if (booleanPrefix) {
const booleanState = getVariableBooleanState(variable, context);
const booleanWrapperState = wrappers.size === 0 || booleanState === boolean
? unknown
: getBooleanWrapperVariableState({
variable,
definition: getSupportedVariableDefinition(variable),
context,
wrappers,
});
const effectiveBooleanState = booleanWrapperState === unknown ? booleanState : booleanWrapperState;
if (
effectiveBooleanState === nonBoolean
&& !isBooleanReactReferenceVariable(variable, context)
&& !isBooleanVueReferenceVariable(variable, context)
) {
const [definition] = variable.defs;
context.report({
node: definition.name,
messageId: MESSAGE_ID_NON_BOOLEAN_PREFIX,
data: {
name: nameForPrefixCheck,
prefix: booleanPrefix,
},
});
}
return;
}
// For names without a boolean prefix, only structurally-boolean values are flagged.
// `isBooleanVariable` is a cheaper check than the full `getVariableBooleanState` analysis,
// so bail out on the common non-boolean case before running the expensive check.
if (mode !== ALWAYS) {
return;
}
if (
!isBooleanVariable(variable, context)
|| getVariableBooleanState(variable, context) === nonBoolean
) {
return;
}
const [definition] = variable.defs;
const suggest = getSuggestions(variable, prefixes, context, nameForPrefixCheck);
context.report({
node: definition.name,
messageId: MESSAGE_ID,
data: {
name: nameForPrefixCheck,
prefixes: formatPrefixes(prefixes),
},
fix: getAutofix({
variable,
prefixes,
context,
suggestions: suggest,
nameForPrefixCheck,
}),
suggest,
});
};
const variableModes = {checkVariables, checkArguments, checkFunctions};
const memberReports = new Map();
const getMemberReport = (node, name) => {
const {owner, name: qualifiedName} = getMemberReportIdentity(node, context.sourceCode);
let reports = memberReports.get(owner);
if (!reports) {
reports = new Map();
memberReports.set(owner, reports);
}
const key = `${qualifiedName ?? ''}:${name}:${getMemberReportKey(node)}`;
let memberReport = reports.get(key);
if (!memberReport) {
memberReport = {
reported: false,
states: [],
};
reports.set(key, memberReport);
}
return memberReport;
};
context.on('Program', node => {
for (const scope of getScopes(context.sourceCode.getScope(node))) {
for (const variable of scope.variables) {
checkVariable(variable);
}
}
});
const checkProperty = (node, mode) => {
if (mode === NEVER) {
return;
}
const name = getBooleanPropertyName(node, context.sourceCode);
if (
!name
|| isIgnoredName(name, ignore)
) {
return;
}
if (isSetter(node)) {
return;
}
const booleanPrefix = getBooleanPrefix(name, prefixes);
const reportNode = node.key ?? getParameterPropertyNameNode(node);
const shouldDeduplicate = methodDefinitionTypes.has(node.type)
|| typeScriptMemberTypes.has(node.type)
|| (node.type === 'Property' && (node.method || node.kind === 'get'));
const report = problem => {
if (!shouldDeduplicate) {
context.report(problem);
return;
}
const memberReport = getMemberReport(node, name);
if (memberReport.reported) {
return;
}
memberReport.reported = true;
context.report(problem);
};
if (booleanPrefix) {
const booleanState = getPropertyBooleanState(node, context);
if (booleanState === nonBoolean) {
report({
node: reportNode,
messageId: MESSAGE_ID_NON_BOOLEAN_PREFIX,
data: {
name,
prefix: booleanPrefix,
},
});
}
return;
}
if (mode !== ALWAYS) {
return;
}
if (shouldDeduplicate) {
const memberReport = getMemberReport(node, name);
memberReport.states.push({
node,
state: getPropertyBooleanState(node, context),
});
memberReport.problem ??= {
node: reportNode,
messageId: MESSAGE_ID,
data: {
name,
prefixes: formatPrefixes(prefixes),
},
};
return;
}
// For names without a boolean prefix, only structurally-boolean values are flagged.
// `isBooleanProperty` is a cheaper check than the full `getPropertyBooleanState` analysis,
// so bail out on the common non-boolean case before running the expensive check.
if (
!isBooleanProperty(node, context)
) {
return;
}
if (getExplicitPropertyBooleanState(node, context) === nonBoolean) {
return;
}
report({
node: reportNode,
messageId: MESSAGE_ID,
data: {
name,
prefixes: formatPrefixes(prefixes),
},
});
};
context.onExit('Program', () => {
for (const reports of memberReports.values()) {
for (const {problem, states} of reports.values()) {
const hasOverloadSignature = states.some(({node}) =>
methodDefinitionTypes.has(node.type)
&& !node.value?.body,
);
const statesToCombine = states
.filter(({node}) =>
!hasOverloadSignature
|| !methodDefinitionTypes.has(node.type)
|| !node.value?.body,
)
.map(({state}) => state);
if (problem && combineBooleanStates(statesToCombine) === boolean) {
context.report(problem);
}
}
}
});
const checkClassBody = node => {
for (const element of node.body) {
const mode = methodDefinitionTypes.has(element.type) ? checkMethods : checkFields;
checkProperty(element, mode);
}
};
if (checkMethods !== NEVER || checkFields !== NEVER) {
context.on('Property', node => {
const mode = node.method || ['get', 'set'].includes(node.kind) ? checkMethods : checkFields;
checkProperty(node, mode);
});
context.on('ClassBody', checkClassBody);
}
if (checkFields !== NEVER) {
context.on('TSParameterProperty', node => checkProperty(node, checkFields));
context.on('TSPropertySignature', node => checkProperty(node, checkFields));
}
if (checkMethods !== NEVER) {
context.on('TSMethodSignature', node => checkProperty(node, checkMethods));
}
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Enforce consistent naming for boolean names.',
recommended: true,
},
fixable: 'code',
hasSuggestions: true,
schema: [
{
type: 'object',
description: 'Rule options.',
additionalProperties: false,
patternProperties: {
'^checkProperties$': {},
},
properties: {
checkVariables: {
enum: [ALWAYS, PROHIBIT, NEVER],
description: 'How to check variable names.',
},
checkArguments: {
enum: [ALWAYS, PROHIBIT, NEVER],
description: 'How to check parameter names.',
},
checkFunctions: {
enum: [ALWAYS, PROHIBIT, NEVER],
description: 'How to check function names.',
},
checkMethods: {
enum: [ALWAYS, PROHIBIT, NEVER],
description: 'How to check object and class methods, getters, and TypeScript method signatures. Setter names are ignored because setters do not return values.',
},
checkFields: {
enum: [ALWAYS, PROHIBIT, NEVER],
description: 'How to check object properties, class fields, TypeScript property signatures, and constructor parameter properties.',
},
prefixes: {
type: 'object',
description: 'Boolean name prefixes to allow or disallow.',
additionalProperties: {
type: 'boolean',
description: 'Whether the prefix is allowed.',
},
propertyNames: {
description: 'Prefix name.',
pattern: '^[a-z][a-zA-Z0-9]*$',
},
},
wrappers: {
type: 'object',
description: 'Wrapper type names and their boolean-like value members.',
additionalProperties: {
type: 'string',
description: 'The property or method that provides the wrapped value.',
minLength: 1,
},
},
ignore: {
type: 'array',
uniqueItems: true,
description: 'Patterns to ignore.',
},
},
},
],
defaultOptions: [{
checkVariables: ALWAYS,
checkArguments: ALWAYS,
checkFunctions: ALWAYS,
checkMethods: NEVER,
checkFields: NEVER,
ignore: [],
wrappers: {},
}],
messages,
languages: [
'js/js',
],
},
};
export default config;