• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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