1import debug from 'debug'; 2import { 3 isCallExpression, 4 isJsxExpression, 5 isIdentifier, 6 isNewExpression, 7 isParameterDeclaration, 8 isPropertyDeclaration, 9 isTypeReference, 10 isUnionOrIntersectionType, 11 isVariableDeclaration, 12 unionTypeParts, 13 isPropertyAssignment, 14} from 'tsutils'; 15import * as ts from 'typescript'; 16 17const log = debug('typescript-eslint:eslint-plugin:utils:types'); 18 19/** 20 * Checks if the given type is either an array type, 21 * or a union made up solely of array types. 22 */ 23export function isTypeArrayTypeOrUnionOfArrayTypes( 24 type: ts.Type, 25 checker: ts.TypeChecker, 26): boolean { 27 for (const t of unionTypeParts(type)) { 28 if (!checker.isArrayType(t)) { 29 return false; 30 } 31 } 32 33 return true; 34} 35 36/** 37 * @param type Type being checked by name. 38 * @param allowedNames Symbol names checking on the type. 39 * @returns Whether the type is, extends, or contains all of the allowed names. 40 */ 41export function containsAllTypesByName( 42 type: ts.Type, 43 allowAny: boolean, 44 allowedNames: Set<string>, 45): boolean { 46 if (isTypeFlagSet(type, ts.TypeFlags.Any | ts.TypeFlags.Unknown)) { 47 return !allowAny; 48 } 49 50 if (isTypeReference(type)) { 51 type = type.target; 52 } 53 54 const symbol = type.getSymbol(); 55 if (symbol && allowedNames.has(symbol.name)) { 56 return true; 57 } 58 59 if (isUnionOrIntersectionType(type)) { 60 return type.types.every(t => 61 containsAllTypesByName(t, allowAny, allowedNames), 62 ); 63 } 64 65 const bases = type.getBaseTypes(); 66 return ( 67 typeof bases !== 'undefined' && 68 bases.length > 0 && 69 bases.every(t => containsAllTypesByName(t, allowAny, allowedNames)) 70 ); 71} 72 73/** 74 * Get the type name of a given type. 75 * @param typeChecker The context sensitive TypeScript TypeChecker. 76 * @param type The type to get the name of. 77 */ 78export function getTypeName( 79 typeChecker: ts.TypeChecker, 80 type: ts.Type, 81): string { 82 // It handles `string` and string literal types as string. 83 if ((type.flags & ts.TypeFlags.StringLike) !== 0) { 84 return 'string'; 85 } 86 87 // If the type is a type parameter which extends primitive string types, 88 // but it was not recognized as a string like. So check the constraint 89 // type of the type parameter. 90 if ((type.flags & ts.TypeFlags.TypeParameter) !== 0) { 91 // `type.getConstraint()` method doesn't return the constraint type of 92 // the type parameter for some reason. So this gets the constraint type 93 // via AST. 94 const symbol = type.getSymbol(); 95 const decls = symbol?.getDeclarations(); 96 const typeParamDecl = decls?.[0] as ts.TypeParameterDeclaration; 97 if ( 98 ts.isTypeParameterDeclaration(typeParamDecl) && 99 typeParamDecl.constraint != null 100 ) { 101 return getTypeName( 102 typeChecker, 103 typeChecker.getTypeFromTypeNode(typeParamDecl.constraint), 104 ); 105 } 106 } 107 108 // If the type is a union and all types in the union are string like, 109 // return `string`. For example: 110 // - `"a" | "b"` is string. 111 // - `string | string[]` is not string. 112 if ( 113 type.isUnion() && 114 type.types 115 .map(value => getTypeName(typeChecker, value)) 116 .every(t => t === 'string') 117 ) { 118 return 'string'; 119 } 120 121 // If the type is an intersection and a type in the intersection is string 122 // like, return `string`. For example: `string & {__htmlEscaped: void}` 123 if ( 124 type.isIntersection() && 125 type.types 126 .map(value => getTypeName(typeChecker, value)) 127 .some(t => t === 'string') 128 ) { 129 return 'string'; 130 } 131 132 return typeChecker.typeToString(type); 133} 134 135/** 136 * Resolves the given node's type. Will resolve to the type's generic constraint, if it has one. 137 */ 138export function getConstrainedTypeAtLocation( 139 checker: ts.TypeChecker, 140 node: ts.Node, 141): ts.Type { 142 const nodeType = checker.getTypeAtLocation(node); 143 const constrained = checker.getBaseConstraintOfType(nodeType); 144 145 return constrained ?? nodeType; 146} 147 148/** 149 * Checks if the given type is (or accepts) nullable 150 * @param isReceiver true if the type is a receiving type (i.e. the type of a called function's parameter) 151 */ 152export function isNullableType( 153 type: ts.Type, 154 { 155 isReceiver = false, 156 allowUndefined = true, 157 }: { isReceiver?: boolean; allowUndefined?: boolean } = {}, 158): boolean { 159 const flags = getTypeFlags(type); 160 161 if (isReceiver && flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) { 162 return true; 163 } 164 165 if (allowUndefined) { 166 return (flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0; 167 } else { 168 return (flags & ts.TypeFlags.Null) !== 0; 169 } 170} 171 172/** 173 * Gets the declaration for the given variable 174 */ 175export function getDeclaration( 176 checker: ts.TypeChecker, 177 node: ts.Expression, 178): ts.Declaration | null { 179 const symbol = checker.getSymbolAtLocation(node); 180 if (!symbol) { 181 return null; 182 } 183 const declarations = symbol.getDeclarations(); 184 return declarations?.[0] ?? null; 185} 186 187/** 188 * Gets all of the type flags in a type, iterating through unions automatically 189 */ 190export function getTypeFlags(type: ts.Type): ts.TypeFlags { 191 let flags: ts.TypeFlags = 0; 192 for (const t of unionTypeParts(type)) { 193 flags |= t.flags; 194 } 195 return flags; 196} 197 198/** 199 * Checks if the given type is (or accepts) the given flags 200 * @param isReceiver true if the type is a receiving type (i.e. the type of a called function's parameter) 201 */ 202export function isTypeFlagSet( 203 type: ts.Type, 204 flagsToCheck: ts.TypeFlags, 205 isReceiver?: boolean, 206): boolean { 207 const flags = getTypeFlags(type); 208 209 if (isReceiver && flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) { 210 return true; 211 } 212 213 return (flags & flagsToCheck) !== 0; 214} 215 216/** 217 * @returns Whether a type is an instance of the parent type, including for the parent's base types. 218 */ 219export function typeIsOrHasBaseType( 220 type: ts.Type, 221 parentType: ts.Type, 222): boolean { 223 const parentSymbol = parentType.getSymbol(); 224 if (!type.getSymbol() || !parentSymbol) { 225 return false; 226 } 227 228 const typeAndBaseTypes = [type]; 229 const ancestorTypes = type.getBaseTypes(); 230 231 if (ancestorTypes) { 232 typeAndBaseTypes.push(...ancestorTypes); 233 } 234 235 for (const baseType of typeAndBaseTypes) { 236 const baseSymbol = baseType.getSymbol(); 237 if (baseSymbol && baseSymbol.name === parentSymbol.name) { 238 return true; 239 } 240 } 241 242 return false; 243} 244 245/** 246 * Gets the source file for a given node 247 */ 248export function getSourceFileOfNode(node: ts.Node): ts.SourceFile { 249 while (node && node.kind !== ts.SyntaxKind.SourceFile) { 250 node = node.parent; 251 } 252 return node as ts.SourceFile; 253} 254 255export function getTokenAtPosition( 256 sourceFile: ts.SourceFile, 257 position: number, 258): ts.Node { 259 const queue: ts.Node[] = [sourceFile]; 260 let current: ts.Node; 261 while (queue.length > 0) { 262 current = queue.shift()!; 263 // find the child that contains 'position' 264 for (const child of current.getChildren(sourceFile)) { 265 const start = child.getFullStart(); 266 if (start > position) { 267 // If this child begins after position, then all subsequent children will as well. 268 return current; 269 } 270 271 const end = child.getEnd(); 272 if ( 273 position < end || 274 (position === end && child.kind === ts.SyntaxKind.EndOfFileToken) 275 ) { 276 queue.push(child); 277 break; 278 } 279 } 280 } 281 return current!; 282} 283 284export interface EqualsKind { 285 isPositive: boolean; 286 isStrict: boolean; 287} 288 289export function getEqualsKind(operator: string): EqualsKind | undefined { 290 switch (operator) { 291 case '==': 292 return { 293 isPositive: true, 294 isStrict: false, 295 }; 296 297 case '===': 298 return { 299 isPositive: true, 300 isStrict: true, 301 }; 302 303 case '!=': 304 return { 305 isPositive: false, 306 isStrict: false, 307 }; 308 309 case '!==': 310 return { 311 isPositive: false, 312 isStrict: true, 313 }; 314 315 default: 316 return undefined; 317 } 318} 319 320export function getTypeArguments( 321 type: ts.TypeReference, 322 checker: ts.TypeChecker, 323): readonly ts.Type[] { 324 // getTypeArguments was only added in TS3.7 325 if (checker.getTypeArguments) { 326 return checker.getTypeArguments(type); 327 } 328 329 return type.typeArguments ?? []; 330} 331 332/** 333 * @returns true if the type is `unknown` 334 */ 335export function isTypeUnknownType(type: ts.Type): boolean { 336 return isTypeFlagSet(type, ts.TypeFlags.Unknown); 337} 338 339/** 340 * @returns true if the type is `any` 341 */ 342export function isTypeAnyType(type: ts.Type): boolean { 343 if (isTypeFlagSet(type, ts.TypeFlags.Any)) { 344 if (type.intrinsicName === 'error') { 345 log('Found an "error" any type'); 346 } 347 return true; 348 } 349 return false; 350} 351 352/** 353 * @returns true if the type is `any[]` 354 */ 355export function isTypeAnyArrayType( 356 type: ts.Type, 357 checker: ts.TypeChecker, 358): boolean { 359 return ( 360 checker.isArrayType(type) && 361 isTypeAnyType( 362 // getTypeArguments was only added in TS3.7 363 getTypeArguments(type, checker)[0], 364 ) 365 ); 366} 367 368/** 369 * @returns true if the type is `unknown[]` 370 */ 371export function isTypeUnknownArrayType( 372 type: ts.Type, 373 checker: ts.TypeChecker, 374): boolean { 375 return ( 376 checker.isArrayType(type) && 377 isTypeUnknownType( 378 // getTypeArguments was only added in TS3.7 379 getTypeArguments(type, checker)[0], 380 ) 381 ); 382} 383 384export const enum AnyType { 385 Any, 386 AnyArray, 387 Safe, 388} 389/** 390 * @returns `AnyType.Any` if the type is `any`, `AnyType.AnyArray` if the type is `any[]` or `readonly any[]`, 391 * otherwise it returns `AnyType.Safe`. 392 */ 393export function isAnyOrAnyArrayTypeDiscriminated( 394 node: ts.Node, 395 checker: ts.TypeChecker, 396): AnyType { 397 const type = checker.getTypeAtLocation(node); 398 if (isTypeAnyType(type)) { 399 return AnyType.Any; 400 } 401 if (isTypeAnyArrayType(type, checker)) { 402 return AnyType.AnyArray; 403 } 404 return AnyType.Safe; 405} 406 407/** 408 * Does a simple check to see if there is an any being assigned to a non-any type. 409 * 410 * This also checks generic positions to ensure there's no unsafe sub-assignments. 411 * Note: in the case of generic positions, it makes the assumption that the two types are the same. 412 * 413 * @example See tests for examples 414 * 415 * @returns false if it's safe, or an object with the two types if it's unsafe 416 */ 417export function isUnsafeAssignment( 418 type: ts.Type, 419 receiver: ts.Type, 420 checker: ts.TypeChecker, 421): false | { sender: ts.Type; receiver: ts.Type } { 422 if (isTypeAnyType(type)) { 423 // Allow assignment of any ==> unknown. 424 if (isTypeUnknownType(receiver)) { 425 return false; 426 } 427 428 if (!isTypeAnyType(receiver)) { 429 return { sender: type, receiver }; 430 } 431 } 432 433 if (isTypeReference(type) && isTypeReference(receiver)) { 434 // TODO - figure out how to handle cases like this, 435 // where the types are assignable, but not the same type 436 /* 437 function foo(): ReadonlySet<number> { return new Set<any>(); } 438 439 // and 440 441 type Test<T> = { prop: T } 442 type Test2 = { prop: string } 443 declare const a: Test<any>; 444 const b: Test2 = a; 445 */ 446 447 if (type.target !== receiver.target) { 448 // if the type references are different, assume safe, as we won't know how to compare the two types 449 // the generic positions might not be equivalent for both types 450 return false; 451 } 452 453 const typeArguments = type.typeArguments ?? []; 454 const receiverTypeArguments = receiver.typeArguments ?? []; 455 456 for (let i = 0; i < typeArguments.length; i += 1) { 457 const arg = typeArguments[i]; 458 const receiverArg = receiverTypeArguments[i]; 459 460 const unsafe = isUnsafeAssignment(arg, receiverArg, checker); 461 if (unsafe) { 462 return { sender: type, receiver }; 463 } 464 } 465 466 return false; 467 } 468 469 return false; 470} 471 472/** 473 * Returns the contextual type of a given node. 474 * Contextual type is the type of the target the node is going into. 475 * i.e. the type of a called function's parameter, or the defined type of a variable declaration 476 */ 477export function getContextualType( 478 checker: ts.TypeChecker, 479 node: ts.Expression, 480): ts.Type | undefined { 481 const parent = node.parent; 482 if (!parent) { 483 return; 484 } 485 486 if (isCallExpression(parent) || isNewExpression(parent)) { 487 if (node === parent.expression) { 488 // is the callee, so has no contextual type 489 return; 490 } 491 } else if ( 492 isVariableDeclaration(parent) || 493 isPropertyDeclaration(parent) || 494 isParameterDeclaration(parent) 495 ) { 496 return parent.type ? checker.getTypeFromTypeNode(parent.type) : undefined; 497 } else if (isJsxExpression(parent)) { 498 return checker.getContextualType(parent); 499 } else if (isPropertyAssignment(parent) && isIdentifier(node)) { 500 return checker.getContextualType(node); 501 } else if ( 502 ![ts.SyntaxKind.TemplateSpan, ts.SyntaxKind.JsxExpression].includes( 503 parent.kind, 504 ) 505 ) { 506 // parent is not something we know we can get the contextual type of 507 return; 508 } 509 // TODO - support return statement checking 510 511 return checker.getContextualType(node); 512} 513