eslint-plugin-sonarjs
Version:
535 lines (534 loc) • 20 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UTILITY_TYPES = void 0;
exports.isArray = isArray;
exports.isTypedArray = isTypedArray;
exports.isString = isString;
exports.isNumber = isNumber;
exports.isBigIntType = isBigIntType;
exports.isNumberType = isNumberType;
exports.isStringType = isStringType;
exports.isFunction = isFunction;
exports.isUnion = isUnion;
exports.isUndefinedOrNull = isUndefinedOrNull;
exports.isThenable = isThenable;
exports.isThenableOrVoidUnion = isThenableOrVoidUnion;
exports.isThenableOrGuardUnion = isThenableOrGuardUnion;
exports.isAny = isAny;
exports.areMutuallyAssignableTypes = areMutuallyAssignableTypes;
exports.areSameTypeDeclarations = areSameTypeDeclarations;
exports.isAnyOrUnknownType = isAnyOrUnknownType;
exports.isGenericType = isGenericType;
exports.getTypeFromTreeNode = getTypeFromTreeNode;
exports.getTypeAsString = getTypeAsString;
exports.getSymbolAtLocation = getSymbolAtLocation;
exports.getSignatureFromCallee = getSignatureFromCallee;
exports.isArrayLikeType = isArrayLikeType;
exports.isStringArray = isStringArray;
exports.isNumberArray = isNumberArray;
exports.isBigIntArray = isBigIntArray;
exports.isBooleanArray = isBooleanArray;
exports.isTypeAlias = isTypeAlias;
exports.isBooleanTrueType = isBooleanTrueType;
exports.isBooleanType = isBooleanType;
exports.isNullOrUndefinedType = isNullOrUndefinedType;
exports.isObjectType = isObjectType;
exports.typeHasMethod = typeHasMethod;
exports.isIterable = isIterable;
exports.getFunctionParameterCount = getFunctionParameterCount;
const typescript_1 = __importDefault(require("typescript"));
const ast_js_1 = require("./ast.js");
/**
* Returns true when `node` resolves to the built-in `Array` type.
*/
function isArray(node, services) {
const type = getTypeFromTreeNode(node, services);
return type.symbol?.name === 'Array';
}
/**
* TypeScript provides a set of utility types to facilitate type transformations.
* @see https://www.typescriptlang.org/docs/handbook/utility-types.html
*/
exports.UTILITY_TYPES = new Set([
'Awaited',
'Partial',
'Required',
'Readonly',
'Record',
'Pick',
'Omit',
'Exclude',
'Extract',
'NonNullable',
'Parameters',
'ConstructorParameters',
'ReturnType',
'InstanceType',
'ThisParameterType',
'OmitThisParameter',
'ThisType',
'Uppercase',
'Lowercase',
'Capitalize',
'Uncapitalize',
]);
/**
* JavaScript typed arrays
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Typed_arrays
*/
const TYPED_ARRAY_TYPES = new Set([
'Int8Array',
'Uint8Array',
'Uint8ClampedArray',
'Int16Array',
'Uint16Array',
'Int32Array',
'Uint32Array',
'Float32Array',
'Float64Array',
'BigInt64Array',
'BigUint64Array',
]);
/**
* Checks if the provided node is a JS typed array like "BigInt64Array". See TYPED_ARRAY_TYPES
*
* @param node
* @param services
* @returns
*/
function isTypedArray(node, services) {
const type = getTypeFromTreeNode(node, services);
return TYPED_ARRAY_TYPES.has(type.symbol?.name);
}
/**
* Returns true when `node` resolves to a string-like type.
*/
function isString(node, services) {
const checker = services.program.getTypeChecker();
const typ = checker.getTypeAtLocation(services.esTreeNodeToTSNodeMap.get(node));
return (typ.getFlags() & typescript_1.default.TypeFlags.StringLike) !== 0;
}
/**
* Returns true when `node` resolves to a number-like type.
*/
function isNumber(node, services) {
const checker = services.program.getTypeChecker();
const typ = checker.getTypeAtLocation(services.esTreeNodeToTSNodeMap.get(node));
return (typ.getFlags() & typescript_1.default.TypeFlags.NumberLike) !== 0;
}
/**
* Returns true when `type` is bigint-like.
*/
function isBigIntType(type) {
return (type.getFlags() & typescript_1.default.TypeFlags.BigIntLike) !== 0;
}
/**
* Returns true when `type` is number-like.
*/
function isNumberType(type) {
return (type.getFlags() & typescript_1.default.TypeFlags.NumberLike) !== 0;
}
/**
* Returns true when `type` is string-like.
*/
function isStringType(type) {
return (type.flags & typescript_1.default.TypeFlags.StringLike) > 0 || type.symbol?.name === 'String';
}
/**
* Returns true when `node` resolves to a function type.
*/
function isFunction(node, services) {
const checker = services.program.getTypeChecker();
const type = checker.getTypeAtLocation(services.esTreeNodeToTSNodeMap.get(node));
return type.symbol && (type.symbol.flags & typescript_1.default.SymbolFlags.Function) !== 0;
}
/**
* Returns true when `node` resolves to a union type.
*/
function isUnion(node, services) {
const checker = services.program.getTypeChecker();
const type = checker.getTypeAtLocation(services.esTreeNodeToTSNodeMap.get(node));
return type.isUnion();
}
/**
* Returns an array of the union types if the provided type is a union.
* Otherwise, returns an array containing the provided type as its unique element.
* @param type A TypeScript type.
* @return An array of types. It's never empty.
*/
function getUnionTypes(type) {
return type.isUnion() ? type.types : [type];
}
/**
* Returns true when `node` resolves to `undefined`, `null`, or a union containing either.
*/
function isUndefinedOrNull(node, services) {
const checker = services.program.getTypeChecker();
const typ = checker.getTypeAtLocation(services.esTreeNodeToTSNodeMap.get(node));
return ((typ.getFlags() & typescript_1.default.TypeFlags.Undefined) !== 0 || (typ.getFlags() & typescript_1.default.TypeFlags.Null) !== 0);
}
/**
* Returns true when `node` resolves to a thenable type.
*/
function isThenable(node, services) {
const mapped = services.esTreeNodeToTSNodeMap.get(node);
const checker = services.program.getTypeChecker();
return hasThenMethod(checker.getTypeAtLocation(mapped), checker);
}
const NOTHING_TYPE_FLAGS = typescript_1.default.TypeFlags.Void | typescript_1.default.TypeFlags.Undefined | typescript_1.default.TypeFlags.Null;
/**
* Checks if a node's type is either:
* - Thenable (Promise-like), OR
* - A union where ALL members are either thenable or "nothing" types (void, undefined, null)
*
* This is useful for rules like S3735 that allow voiding Promise operations,
* including cases like optional chaining (Promise<T> | undefined) or
* optional async callbacks (() => void | Promise<void>).
*/
function isThenableOrVoidUnion(node, services) {
return isThenableUnionWithGuards(node, services, NOTHING_TYPE_FLAGS);
}
/**
* Like {@link isThenableOrVoidUnion}, but also accepts boolean members as guards.
*
* Short-circuit expressions such as `skipCheck || checkPromise` resolve to
* `boolean | Promise<void>`, where the boolean is only a guard deciding whether
* the promise runs. Voiding such an expression is the `no-floating-promises`
* idiom and must not be flagged.
*/
function isThenableOrGuardUnion(node, services) {
return isThenableUnionWithGuards(node, services, NOTHING_TYPE_FLAGS | typescript_1.default.TypeFlags.BooleanLike);
}
function isThenableUnionWithGuards(node, services, guardFlags) {
const checker = services.program.getTypeChecker();
const type = getTypeFromTreeNode(node, services);
const unionTypes = type.isUnion() ? type.types : [type];
let hasThenable = false;
let allThenableOrGuard = true;
for (const unionType of unionTypes) {
const isThenable = hasThenMethod(unionType, checker);
const isGuard = (unionType.flags & guardFlags) !== 0;
hasThenable ||= isThenable;
allThenableOrGuard &&= isThenable || isGuard;
}
return hasThenable && allThenableOrGuard;
}
function hasThenMethod(type, checker) {
const thenProperty = type.getProperty('then');
if (!thenProperty) {
return false;
}
const thenType = checker.getTypeOfSymbol(thenProperty);
return thenType.getCallSignatures().length > 0;
}
/**
* Returns true when `type` is exactly `any`.
*/
function isAny(type) {
return type.flags === typescript_1.default.TypeFlags.Any;
}
/**
* Returns true when both types are specific enough to compare (neither `any`
* nor `unknown`) and are mutually assignable.
*
* Mutual assignability is stricter than symbol equality for generic aliases:
* `Props<User>` and `Props<Product>` share the same alias symbol, but they are
* not mutually assignable and therefore do not represent the same effective type.
*/
function areMutuallyAssignableTypes(checker, left, right) {
if (!left || !right || isAnyOrUnknownType(left) || isAnyOrUnknownType(right)) {
return false;
}
// @ts-ignore -- isTypeAssignableTo is a private TypeScript API
return checker.isTypeAssignableTo(left, right) && checker.isTypeAssignableTo(right, left);
}
/**
* Returns true when both types come from the same named declaration and, for
* generic declarations, are instantiated with the same type arguments.
*
* The comparison has two steps:
* 1. the declarations must resolve to the same symbol
* 2. each type argument must match through `areSameTypeArguments(...)`
*
* That helper can call back into this function for named type arguments, so
* generic comparisons recurse through nested declarations.
*
* This is intentionally stricter than structural equality: `Props<User>` and
* `Props<Product>` are different, and `Props<User>` also stays different from
* `Props<{ id: string }>` even if the shapes happen to match.
*/
function areSameTypeDeclarations(checker, left, right) {
if (!left || !right || isAnyOrUnknownType(left) || isAnyOrUnknownType(right)) {
return false;
}
const leftSymbol = getComparableDeclaredTypeSymbol(left);
const rightSymbol = getComparableDeclaredTypeSymbol(right);
if (!leftSymbol || leftSymbol !== rightSymbol) {
return false;
}
const leftArgs = getDeclaredTypeArguments(left, checker);
const rightArgs = getDeclaredTypeArguments(right, checker);
return (leftArgs.length === rightArgs.length &&
leftArgs.every((argument, index) => areSameTypeArguments(checker, argument, rightArgs[index])));
}
function areSameTypeArguments(checker, left, right) {
if (!left || !right) {
return false;
}
if (isAnyOrUnknownType(left) || isAnyOrUnknownType(right)) {
return (isAnyOrUnknownType(left) &&
isAnyOrUnknownType(right) &&
checker.typeToString(left) === checker.typeToString(right));
}
const leftSymbol = getComparableDeclaredTypeSymbol(left);
const rightSymbol = getComparableDeclaredTypeSymbol(right);
if (leftSymbol || rightSymbol) {
// Keep named type arguments distinct from anonymous structural literals.
return leftSymbol !== undefined && leftSymbol === rightSymbol
? areSameTypeDeclarations(checker, left, right)
: false;
}
// Primitives, unions, tuples, and anonymous object literals do not have a
// stable declared type symbol, so compare their effective instantiated form.
return checker.typeToString(left) === checker.typeToString(right);
}
function isAnyOrUnknownType(type) {
return (type.flags & (typescript_1.default.TypeFlags.Any | typescript_1.default.TypeFlags.Unknown)) !== 0;
}
/**
* Returns the symbol to use for declaration-based comparisons.
*
* Anonymous type literals such as `{ id: string }` are intentionally excluded:
* their `TypeLiteral` symbols do not represent stable declaration identities.
* Named interfaces and type aliases are still kept because we prefer
* `aliasSymbol` over the underlying `TypeLiteral` symbol.
*/
function getComparableDeclaredTypeSymbol(type) {
const symbol = type.aliasSymbol ?? type.symbol;
return symbol && (symbol.flags & typescript_1.default.SymbolFlags.TypeLiteral) === 0 ? symbol : undefined;
}
function getDeclaredTypeArguments(type, checker) {
if (type.aliasTypeArguments?.length) {
return type.aliasTypeArguments;
}
return isTypeReference(type) ? checker.getTypeArguments(type) : [];
}
function isTypeReference(type) {
return ((type.flags & typescript_1.default.TypeFlags.Object) !== 0 &&
(type.objectFlags & typescript_1.default.ObjectFlags.Reference) !== 0);
}
/**
* Checks if a node has a generic type like:
*
* function foo<T> (bar: T) {
* bar // is generic
* }
*
* @param node TSESTree.Node
* @param services RuleContext.parserServices
* @returns
*/
function isGenericType(node, services) {
const type = getTypeFromTreeNode(node, services);
return type.isTypeParameter();
}
/**
* Returns the TypeScript type resolved at `node`.
*/
function getTypeFromTreeNode(node, services) {
const checker = services.program.getTypeChecker();
return checker.getTypeAtLocation(services.esTreeNodeToTSNodeMap.get(node));
}
/**
* Returns the widened string representation of the type resolved at `node`.
*/
function getTypeAsString(node, services) {
const { typeToString, getBaseTypeOfLiteralType } = services.program.getTypeChecker();
return typeToString(getBaseTypeOfLiteralType(getTypeFromTreeNode(node, services)));
}
/**
* Returns the symbol resolved at `node`, if any.
*/
function getSymbolAtLocation(node, services) {
const checker = services.program.getTypeChecker();
return checker.getSymbolAtLocation(services.esTreeNodeToTSNodeMap.get(node));
}
/**
* Returns the resolved call signature for the call-like expression at `node`.
*/
function getSignatureFromCallee(node, services) {
const checker = services.program.getTypeChecker();
return checker.getResolvedSignature(services.esTreeNodeToTSNodeMap.get(node));
}
/**
* This function checks if a type may correspond to an array type. Beyond simple array types, it will also
* consider the union of array types and generic types extending an array type.
* @param type A type to check
* @param services The services used to get access to the TypeScript type checker
*/
function isArrayLikeType(type, services) {
const checker = services.program.getTypeChecker();
const constrained = checker.getBaseConstraintOfType(type);
return isArrayOrUnionOfArrayType(constrained ?? type, services);
}
function isArrayOrUnionOfArrayType(type, services) {
for (const part of getUnionTypes(type)) {
if (!isArrayType(part, services)) {
return false;
}
}
return true;
}
/**
* Test if the provided type is an array of strings.
* @param type A TypeScript type.
* @param services The services used to get access to the TypeScript type checker
*/
function isStringArray(type, services) {
return isArrayElementTypeMatching(type, services, isStringType);
}
/**
* Test if the provided type is an array of numbers.
* @param type A TypeScript type.
* @param services The services used to get access to the TypeScript type checker
*/
function isNumberArray(type, services) {
return isArrayElementTypeMatching(type, services, isNumberType);
}
/**
* Test if the provided type is an array of big integers.
* @param type A TypeScript type.
* @param services The services used to get access to the TypeScript type checker
*/
function isBigIntArray(type, services) {
return isArrayElementTypeMatching(type, services, isBigIntType);
}
/**
* Test if the provided type is an array of booleans.
* @param type A TypeScript type.
* @param services The services used to get access to the TypeScript type checker
*/
function isBooleanArray(type, services) {
return isArrayElementTypeMatching(type, services, isBooleanType);
}
function isArrayElementTypeMatching(type, services, predicate) {
const checker = services.program.getTypeChecker();
if (!isArrayType(type, services)) {
return false;
}
const [elementType] = checker.getTypeArguments(type);
return elementType && predicate(elementType);
}
// Internal TS API
function isArrayType(type, services) {
const checker = services.program.getTypeChecker();
return ('isArrayType' in checker &&
typeof checker.isArrayType === 'function' &&
checker.isArrayType(type));
}
/**
* Checks whether a TypeScript type node denotes a type alias.
* @param node a type node to check
* @param context the rule context
*/
function isTypeAlias(node, context) {
if (node.type !== 'TSTypeReference' ||
node.typeName.type !== 'Identifier' ||
node.typeArguments) {
return false;
}
const scope = context.sourceCode.getScope(node);
const variable = (0, ast_js_1.getVariableFromScope)(scope, node.typeName.name);
return variable?.defs.some(def => def.node.type === 'TSTypeAliasDeclaration');
}
function isBooleanLiteralType(type) {
return type.flags === typescript_1.default.TypeFlags.BooleanLiteral;
}
/**
* Returns true when `type` is the boolean literal `true`.
*/
function isBooleanTrueType(type) {
return isBooleanLiteralType(type) && type.intrinsicName === 'true';
}
/**
* Returns true when `type` is boolean-like.
*/
function isBooleanType({ flags }) {
return (flags & typescript_1.default.TypeFlags.BooleanLike) !== 0;
}
/**
* Returns true when `type` is `null`, `undefined`, or a union containing either.
*/
function isNullOrUndefinedType({ flags }) {
return flags & typescript_1.default.TypeFlags.Null || flags & typescript_1.default.TypeFlags.Undefined;
}
/**
* Returns true when `type` is object-like.
*/
function isObjectType({ flags }) {
return flags & typescript_1.default.TypeFlags.Object;
}
/**
* Returns true when `node` exposes a callable member named `methodName`.
*/
function typeHasMethod(node, methodName, services) {
const type = getTypeFromTreeNode(node, services);
const property = type.getProperty(methodName);
if (!property) {
return false;
}
// Check if it's a method by examining the symbol flags
if (property.flags & typescript_1.default.SymbolFlags.Method) {
return true;
}
// Also check if it's a property that contains a function
if (property.flags & typescript_1.default.SymbolFlags.Property) {
const typeChecker = services.program.getTypeChecker();
const propertyType = property.valueDeclaration
? typeChecker.getTypeOfSymbolAtLocation(property, property.valueDeclaration)
: typeChecker.getTypeOfSymbol(property);
// Check if the property type is callable (has call signatures)
return propertyType.getCallSignatures().length > 0;
}
return false;
}
/**
* Checks if a type is iterable (can be used in for-of loops).
* @param node The node to check
* @param services The parser services
* @returns true if the type is iterable, false otherwise
*/
function isIterable(node, services) {
const type = getTypeFromTreeNode(node, services);
const properties = type.getProperties();
return properties.some(prop => prop.name.startsWith('__@iterator@'));
}
/**
* Gets the number of parameters for a function-typed node using TypeScript type information.
* Returns null if the parameter count cannot be determined.
*
* For functions with multiple call signatures (overloads), returns the maximum
* parameter count across all signatures.
*
* @param node The node to get parameter count for
* @param services TypeScript parser services
* @returns The parameter count, or null if it cannot be determined
*/
function getFunctionParameterCount(node, services) {
try {
const type = getTypeFromTreeNode(node, services);
const signatures = type.getCallSignatures();
if (signatures.length === 0) {
return null;
}
// Return the maximum parameter count across all signatures
// (handles function overloads)
return Math.max(...signatures.map(sig => sig.parameters.length));
}
catch {
return null;
}
}