• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import unescape from 'lodash/unescape';
2import * as ts from 'typescript';
3import { AST_NODE_TYPES, AST_TOKEN_TYPES, TSESTree } from './ts-estree';
4
5const SyntaxKind = ts.SyntaxKind;
6
7const LOGICAL_OPERATORS: (
8  | ts.LogicalOperator
9  | ts.SyntaxKind.QuestionQuestionToken
10)[] = [
11  SyntaxKind.BarBarToken,
12  SyntaxKind.AmpersandAmpersandToken,
13  SyntaxKind.QuestionQuestionToken,
14];
15
16interface TokenToText {
17  [SyntaxKind.OpenBraceToken]: '{';
18  [SyntaxKind.CloseBraceToken]: '}';
19  [SyntaxKind.OpenParenToken]: '(';
20  [SyntaxKind.CloseParenToken]: ')';
21  [SyntaxKind.OpenBracketToken]: '[';
22  [SyntaxKind.CloseBracketToken]: ']';
23  [SyntaxKind.DotToken]: '.';
24  [SyntaxKind.DotDotDotToken]: '...';
25  [SyntaxKind.SemicolonToken]: ';';
26  [SyntaxKind.CommaToken]: ',';
27  [SyntaxKind.LessThanToken]: '<';
28  [SyntaxKind.GreaterThanToken]: '>';
29  [SyntaxKind.LessThanEqualsToken]: '<=';
30  [SyntaxKind.GreaterThanEqualsToken]: '>=';
31  [SyntaxKind.EqualsEqualsToken]: '==';
32  [SyntaxKind.ExclamationEqualsToken]: '!=';
33  [SyntaxKind.EqualsEqualsEqualsToken]: '===';
34  [SyntaxKind.InstanceOfKeyword]: 'instanceof';
35  [SyntaxKind.ExclamationEqualsEqualsToken]: '!==';
36  [SyntaxKind.EqualsGreaterThanToken]: '=>';
37  [SyntaxKind.PlusToken]: '+';
38  [SyntaxKind.MinusToken]: '-';
39  [SyntaxKind.AsteriskToken]: '*';
40  [SyntaxKind.AsteriskAsteriskToken]: '**';
41  [SyntaxKind.SlashToken]: '/';
42  [SyntaxKind.PercentToken]: '%';
43  [SyntaxKind.PlusPlusToken]: '++';
44  [SyntaxKind.MinusMinusToken]: '--';
45  [SyntaxKind.LessThanLessThanToken]: '<<';
46  [SyntaxKind.LessThanSlashToken]: '</';
47  [SyntaxKind.GreaterThanGreaterThanToken]: '>>';
48  [SyntaxKind.GreaterThanGreaterThanGreaterThanToken]: '>>>';
49  [SyntaxKind.AmpersandToken]: '&';
50  [SyntaxKind.BarToken]: '|';
51  [SyntaxKind.CaretToken]: '^';
52  [SyntaxKind.ExclamationToken]: '!';
53  [SyntaxKind.TildeToken]: '~';
54  [SyntaxKind.AmpersandAmpersandToken]: '&&';
55  [SyntaxKind.BarBarToken]: '||';
56  [SyntaxKind.QuestionToken]: '?';
57  [SyntaxKind.ColonToken]: ':';
58  [SyntaxKind.EqualsToken]: '=';
59  [SyntaxKind.PlusEqualsToken]: '+=';
60  [SyntaxKind.MinusEqualsToken]: '-=';
61  [SyntaxKind.AsteriskEqualsToken]: '*=';
62  [SyntaxKind.AsteriskAsteriskEqualsToken]: '**=';
63  [SyntaxKind.SlashEqualsToken]: '/=';
64  [SyntaxKind.PercentEqualsToken]: '%=';
65  [SyntaxKind.LessThanLessThanEqualsToken]: '<<=';
66  [SyntaxKind.GreaterThanGreaterThanEqualsToken]: '>>=';
67  [SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken]: '>>>=';
68  [SyntaxKind.AmpersandEqualsToken]: '&=';
69  [SyntaxKind.AmpersandAmpersandEqualsToken]: '&&=';
70  [SyntaxKind.BarEqualsToken]: '|=';
71  [SyntaxKind.BarBarEqualsToken]: '||=';
72  [SyntaxKind.CaretEqualsToken]: '^=';
73  [SyntaxKind.QuestionQuestionEqualsToken]: '??=';
74  [SyntaxKind.AtToken]: '@';
75  [SyntaxKind.InKeyword]: 'in';
76  [SyntaxKind.UniqueKeyword]: 'unique';
77  [SyntaxKind.KeyOfKeyword]: 'keyof';
78  [SyntaxKind.NewKeyword]: 'new';
79  [SyntaxKind.ImportKeyword]: 'import';
80  [SyntaxKind.ReadonlyKeyword]: 'readonly';
81  [SyntaxKind.QuestionQuestionToken]: '??';
82  [SyntaxKind.QuestionDotToken]: '?.';
83}
84
85/**
86 * Returns true if the given ts.Token is the assignment operator
87 * @param operator the operator token
88 * @returns is assignment
89 */
90export function isAssignmentOperator<T extends ts.SyntaxKind>(
91  operator: ts.Token<T>,
92): boolean {
93  return (
94    operator.kind >= SyntaxKind.FirstAssignment &&
95    operator.kind <= SyntaxKind.LastAssignment
96  );
97}
98
99/**
100 * Returns true if the given ts.Token is a logical operator
101 * @param operator the operator token
102 * @returns is a logical operator
103 */
104export function isLogicalOperator<T extends ts.SyntaxKind>(
105  operator: ts.Token<T>,
106): boolean {
107  return (LOGICAL_OPERATORS as ts.SyntaxKind[]).includes(operator.kind);
108}
109
110/**
111 * Returns the string form of the given TSToken SyntaxKind
112 * @param kind the token's SyntaxKind
113 * @returns the token applicable token as a string
114 */
115export function getTextForTokenKind<T extends ts.SyntaxKind>(
116  kind: T,
117): T extends keyof TokenToText ? TokenToText[T] : string | undefined {
118  return ts.tokenToString(kind) as T extends keyof TokenToText
119    ? TokenToText[T]
120    : string | undefined;
121}
122
123/**
124 * Returns true if the given ts.Node is a valid ESTree class member
125 * @param node TypeScript AST node
126 * @returns is valid ESTree class member
127 */
128export function isESTreeClassMember(node: ts.Node): boolean {
129  return node.kind !== SyntaxKind.SemicolonClassElement;
130}
131
132/**
133 * Checks if a ts.Node has a modifier
134 * @param modifierKind TypeScript SyntaxKind modifier
135 * @param node TypeScript AST node
136 * @returns has the modifier specified
137 */
138export function hasModifier(
139  modifierKind: ts.KeywordSyntaxKind,
140  node: ts.Node,
141): boolean {
142  return (
143    !!node.modifiers &&
144    !!node.modifiers.length &&
145    node.modifiers.some(modifier => modifier.kind === modifierKind)
146  );
147}
148
149/**
150 * Get last last modifier in ast
151 * @param node TypeScript AST node
152 * @returns returns last modifier if present or null
153 */
154export function getLastModifier(node: ts.Node): ts.Modifier | null {
155  return (
156    (!!node.modifiers &&
157      !!node.modifiers.length &&
158      node.modifiers[node.modifiers.length - 1]) ||
159    null
160  );
161}
162
163/**
164 * Returns true if the given ts.Token is a comma
165 * @param token the TypeScript token
166 * @returns is comma
167 */
168export function isComma(token: ts.Node): boolean {
169  return token.kind === SyntaxKind.CommaToken;
170}
171
172/**
173 * Returns true if the given ts.Node is a comment
174 * @param node the TypeScript node
175 * @returns is comment
176 */
177export function isComment(node: ts.Node): boolean {
178  return (
179    node.kind === SyntaxKind.SingleLineCommentTrivia ||
180    node.kind === SyntaxKind.MultiLineCommentTrivia
181  );
182}
183
184/**
185 * Returns true if the given ts.Node is a JSDoc comment
186 * @param node the TypeScript node
187 * @returns is JSDoc comment
188 */
189export function isJSDocComment(node: ts.Node): boolean {
190  return node.kind === SyntaxKind.JSDocComment;
191}
192
193/**
194 * Returns the binary expression type of the given ts.Token
195 * @param operator the operator token
196 * @returns the binary expression type
197 */
198export function getBinaryExpressionType<T extends ts.SyntaxKind>(
199  operator: ts.Token<T>,
200):
201  | AST_NODE_TYPES.AssignmentExpression
202  | AST_NODE_TYPES.LogicalExpression
203  | AST_NODE_TYPES.BinaryExpression {
204  if (isAssignmentOperator(operator)) {
205    return AST_NODE_TYPES.AssignmentExpression;
206  } else if (isLogicalOperator(operator)) {
207    return AST_NODE_TYPES.LogicalExpression;
208  }
209  return AST_NODE_TYPES.BinaryExpression;
210}
211
212/**
213 * Returns line and column data for the given positions,
214 * @param pos position to check
215 * @param ast the AST object
216 * @returns line and column
217 */
218export function getLineAndCharacterFor(
219  pos: number,
220  ast: ts.SourceFile,
221): TSESTree.LineAndColumnData {
222  const loc = ast.getLineAndCharacterOfPosition(pos);
223  return {
224    line: loc.line + 1,
225    column: loc.character,
226  };
227}
228
229/**
230 * Returns line and column data for the given start and end positions,
231 * for the given AST
232 * @param start start data
233 * @param end   end data
234 * @param ast   the AST object
235 * @returns the loc data
236 */
237export function getLocFor(
238  start: number,
239  end: number,
240  ast: ts.SourceFile,
241): TSESTree.SourceLocation {
242  return {
243    start: getLineAndCharacterFor(start, ast),
244    end: getLineAndCharacterFor(end, ast),
245  };
246}
247
248/**
249 * Check whatever node can contain directive
250 * @param node
251 * @returns returns true if node can contain directive
252 */
253export function canContainDirective(
254  node: ts.SourceFile | ts.Block | ts.ModuleBlock,
255): boolean {
256  if (node.kind === ts.SyntaxKind.Block) {
257    switch (node.parent.kind) {
258      case ts.SyntaxKind.Constructor:
259      case ts.SyntaxKind.GetAccessor:
260      case ts.SyntaxKind.SetAccessor:
261      case ts.SyntaxKind.ArrowFunction:
262      case ts.SyntaxKind.FunctionExpression:
263      case ts.SyntaxKind.FunctionDeclaration:
264      case ts.SyntaxKind.MethodDeclaration:
265        return true;
266      default:
267        return false;
268    }
269  }
270  return true;
271}
272
273/**
274 * Returns range for the given ts.Node
275 * @param node the ts.Node or ts.Token
276 * @param ast the AST object
277 * @returns the range data
278 */
279export function getRange(node: ts.Node, ast: ts.SourceFile): [number, number] {
280  return [node.getStart(ast), node.getEnd()];
281}
282
283/**
284 * Returns true if a given ts.Node is a token
285 * @param node the ts.Node
286 * @returns is a token
287 */
288export function isToken(node: ts.Node): boolean {
289  return (
290    node.kind >= SyntaxKind.FirstToken && node.kind <= SyntaxKind.LastToken
291  );
292}
293
294/**
295 * Returns true if a given ts.Node is a JSX token
296 * @param node ts.Node to be checked
297 * @returns is a JSX token
298 */
299export function isJSXToken(node: ts.Node): boolean {
300  return (
301    node.kind >= SyntaxKind.JsxElement && node.kind <= SyntaxKind.JsxAttribute
302  );
303}
304
305/**
306 * Returns the declaration kind of the given ts.Node
307 * @param node TypeScript AST node
308 * @returns declaration kind
309 */
310export function getDeclarationKind(
311  node: ts.VariableDeclarationList,
312): 'let' | 'const' | 'var' {
313  if (node.flags & ts.NodeFlags.Let) {
314    return 'let';
315  }
316  if (node.flags & ts.NodeFlags.Const) {
317    return 'const';
318  }
319  return 'var';
320}
321
322/**
323 * Gets a ts.Node's accessibility level
324 * @param node The ts.Node
325 * @returns accessibility "public", "protected", "private", or null
326 */
327export function getTSNodeAccessibility(
328  node: ts.Node,
329): 'public' | 'protected' | 'private' | null {
330  const modifiers = node.modifiers;
331  if (!modifiers) {
332    return null;
333  }
334  for (let i = 0; i < modifiers.length; i++) {
335    const modifier = modifiers[i];
336    switch (modifier.kind) {
337      case SyntaxKind.PublicKeyword:
338        return 'public';
339      case SyntaxKind.ProtectedKeyword:
340        return 'protected';
341      case SyntaxKind.PrivateKeyword:
342        return 'private';
343      default:
344        break;
345    }
346  }
347  return null;
348}
349
350/**
351 * Finds the next token based on the previous one and its parent
352 * Had to copy this from TS instead of using TS's version because theirs doesn't pass the ast to getChildren
353 * @param previousToken The previous TSToken
354 * @param parent The parent TSNode
355 * @param ast The TS AST
356 * @returns the next TSToken
357 */
358export function findNextToken(
359  previousToken: ts.TextRange,
360  parent: ts.Node,
361  ast: ts.SourceFile,
362): ts.Node | undefined {
363  return find(parent);
364
365  function find(n: ts.Node): ts.Node | undefined {
366    if (ts.isToken(n) && n.pos === previousToken.end) {
367      // this is token that starts at the end of previous token - return it
368      return n;
369    }
370    return firstDefined(n.getChildren(ast), (child: ts.Node) => {
371      const shouldDiveInChildNode =
372        // previous token is enclosed somewhere in the child
373        (child.pos <= previousToken.pos && child.end > previousToken.end) ||
374        // previous token ends exactly at the beginning of child
375        child.pos === previousToken.end;
376      return shouldDiveInChildNode && nodeHasTokens(child, ast)
377        ? find(child)
378        : undefined;
379    });
380  }
381}
382
383/**
384 * Find the first matching ancestor based on the given predicate function.
385 * @param node The current ts.Node
386 * @param predicate The predicate function to apply to each checked ancestor
387 * @returns a matching parent ts.Node
388 */
389export function findFirstMatchingAncestor(
390  node: ts.Node,
391  predicate: (node: ts.Node) => boolean,
392): ts.Node | undefined {
393  while (node) {
394    if (predicate(node)) {
395      return node;
396    }
397    node = node.parent;
398  }
399  return undefined;
400}
401
402/**
403 * Returns true if a given ts.Node has a JSX token within its hierarchy
404 * @param node ts.Node to be checked
405 * @returns has JSX ancestor
406 */
407export function hasJSXAncestor(node: ts.Node): boolean {
408  return !!findFirstMatchingAncestor(node, isJSXToken);
409}
410
411/**
412 * Unescape the text content of string literals, e.g. &amp; -> &
413 * @param text The escaped string literal text.
414 * @returns The unescaped string literal text.
415 */
416export function unescapeStringLiteralText(text: string): string {
417  return unescape(text);
418}
419
420/**
421 * Returns true if a given ts.Node is a computed property
422 * @param node ts.Node to be checked
423 * @returns is Computed Property
424 */
425export function isComputedProperty(node: ts.Node): boolean {
426  return node.kind === SyntaxKind.ComputedPropertyName;
427}
428
429/**
430 * Returns true if a given ts.Node is optional (has QuestionToken)
431 * @param node ts.Node to be checked
432 * @returns is Optional
433 */
434export function isOptional(node: {
435  questionToken?: ts.QuestionToken;
436}): boolean {
437  return node.questionToken
438    ? node.questionToken.kind === SyntaxKind.QuestionToken
439    : false;
440}
441
442/**
443 * Returns true if the node is an optional chain node
444 */
445export function isChainExpression(
446  node: TSESTree.Node,
447): node is TSESTree.ChainExpression {
448  return node.type === AST_NODE_TYPES.ChainExpression;
449}
450
451/**
452 * Returns true of the child of property access expression is an optional chain
453 */
454export function isChildUnwrappableOptionalChain(
455  node:
456    | ts.PropertyAccessExpression
457    | ts.ElementAccessExpression
458    | ts.CallExpression
459    | ts.NonNullExpression,
460  child: TSESTree.Node,
461): boolean {
462  if (
463    isChainExpression(child) &&
464    // (x?.y).z is semantically different, and as such .z is no longer optional
465    node.expression.kind !== ts.SyntaxKind.ParenthesizedExpression
466  ) {
467    return true;
468  }
469
470  return false;
471}
472
473/**
474 * Returns the type of a given ts.Token
475 * @param token the ts.Token
476 * @returns the token type
477 */
478export function getTokenType(
479  token: ts.Identifier | ts.Token<ts.SyntaxKind>,
480): Exclude<AST_TOKEN_TYPES, AST_TOKEN_TYPES.Line | AST_TOKEN_TYPES.Block> {
481  if ('originalKeywordKind' in token && token.originalKeywordKind) {
482    if (token.originalKeywordKind === SyntaxKind.NullKeyword) {
483      return AST_TOKEN_TYPES.Null;
484    } else if (
485      token.originalKeywordKind >= SyntaxKind.FirstFutureReservedWord &&
486      token.originalKeywordKind <= SyntaxKind.LastKeyword
487    ) {
488      return AST_TOKEN_TYPES.Identifier;
489    }
490    return AST_TOKEN_TYPES.Keyword;
491  }
492
493  if (
494    token.kind >= SyntaxKind.FirstKeyword &&
495    token.kind <= SyntaxKind.LastFutureReservedWord
496  ) {
497    if (
498      token.kind === SyntaxKind.FalseKeyword ||
499      token.kind === SyntaxKind.TrueKeyword
500    ) {
501      return AST_TOKEN_TYPES.Boolean;
502    }
503
504    return AST_TOKEN_TYPES.Keyword;
505  }
506
507  if (
508    token.kind >= SyntaxKind.FirstPunctuation &&
509    token.kind <= SyntaxKind.LastBinaryOperator
510  ) {
511    return AST_TOKEN_TYPES.Punctuator;
512  }
513
514  if (
515    token.kind >= SyntaxKind.NoSubstitutionTemplateLiteral &&
516    token.kind <= SyntaxKind.TemplateTail
517  ) {
518    return AST_TOKEN_TYPES.Template;
519  }
520
521  switch (token.kind) {
522    case SyntaxKind.NumericLiteral:
523      return AST_TOKEN_TYPES.Numeric;
524
525    case SyntaxKind.JsxText:
526      return AST_TOKEN_TYPES.JSXText;
527
528    case SyntaxKind.StringLiteral:
529      // A TypeScript-StringLiteral token with a TypeScript-JsxAttribute or TypeScript-JsxElement parent,
530      // must actually be an ESTree-JSXText token
531      if (
532        token.parent &&
533        (token.parent.kind === SyntaxKind.JsxAttribute ||
534          token.parent.kind === SyntaxKind.JsxElement)
535      ) {
536        return AST_TOKEN_TYPES.JSXText;
537      }
538
539      return AST_TOKEN_TYPES.String;
540
541    case SyntaxKind.RegularExpressionLiteral:
542      return AST_TOKEN_TYPES.RegularExpression;
543
544    case SyntaxKind.Identifier:
545    case SyntaxKind.ConstructorKeyword:
546    case SyntaxKind.GetKeyword:
547    case SyntaxKind.SetKeyword:
548
549    // falls through
550    default:
551  }
552
553  // Some JSX tokens have to be determined based on their parent
554  if (token.parent && token.kind === SyntaxKind.Identifier) {
555    if (isJSXToken(token.parent)) {
556      return AST_TOKEN_TYPES.JSXIdentifier;
557    }
558
559    if (
560      token.parent.kind === SyntaxKind.PropertyAccessExpression &&
561      hasJSXAncestor(token)
562    ) {
563      return AST_TOKEN_TYPES.JSXIdentifier;
564    }
565  }
566
567  return AST_TOKEN_TYPES.Identifier;
568}
569
570/**
571 * Extends and formats a given ts.Token, for a given AST
572 * @param token the ts.Token
573 * @param ast   the AST object
574 * @returns the converted Token
575 */
576export function convertToken(
577  token: ts.Node,
578  ast: ts.SourceFile,
579): TSESTree.Token {
580  const start =
581    token.kind === SyntaxKind.JsxText
582      ? token.getFullStart()
583      : token.getStart(ast);
584  const end = token.getEnd();
585  const value = ast.text.slice(start, end);
586  const tokenType = getTokenType(token);
587
588  if (tokenType === AST_TOKEN_TYPES.RegularExpression) {
589    return {
590      type: tokenType,
591      value,
592      range: [start, end],
593      loc: getLocFor(start, end, ast),
594      regex: {
595        pattern: value.slice(1, value.lastIndexOf('/')),
596        flags: value.slice(value.lastIndexOf('/') + 1),
597      },
598    };
599  } else {
600    return {
601      type: tokenType,
602      value,
603      range: [start, end],
604      loc: getLocFor(start, end, ast),
605    };
606  }
607}
608
609/**
610 * Converts all tokens for the given AST
611 * @param ast the AST object
612 * @returns the converted Tokens
613 */
614export function convertTokens(ast: ts.SourceFile): TSESTree.Token[] {
615  const result: TSESTree.Token[] = [];
616  /**
617   * @param node the ts.Node
618   */
619  function walk(node: ts.Node): void {
620    // TypeScript generates tokens for types in JSDoc blocks. Comment tokens
621    // and their children should not be walked or added to the resulting tokens list.
622    if (isComment(node) || isJSDocComment(node)) {
623      return;
624    }
625
626    if (isToken(node) && node.kind !== SyntaxKind.EndOfFileToken) {
627      const converted = convertToken(node, ast);
628
629      if (converted) {
630        result.push(converted);
631      }
632    } else {
633      node.getChildren(ast).forEach(walk);
634    }
635  }
636  walk(ast);
637  return result;
638}
639
640export interface TSError {
641  index: number;
642  lineNumber: number;
643  column: number;
644  message: string;
645}
646
647/**
648 * @param ast     the AST object
649 * @param start      the index at which the error starts
650 * @param message the error message
651 * @returns converted error object
652 */
653export function createError(
654  ast: ts.SourceFile,
655  start: number,
656  message: string,
657): TSError {
658  const loc = ast.getLineAndCharacterOfPosition(start);
659  return {
660    index: start,
661    lineNumber: loc.line + 1,
662    column: loc.character,
663    message,
664  };
665}
666
667/**
668 * @param n the TSNode
669 * @param ast the TS AST
670 */
671export function nodeHasTokens(n: ts.Node, ast: ts.SourceFile): boolean {
672  // If we have a token or node that has a non-zero width, it must have tokens.
673  // Note: getWidth() does not take trivia into account.
674  return n.kind === SyntaxKind.EndOfFileToken
675    ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
676      !!(n as any).jsDoc
677    : n.getWidth(ast) !== 0;
678}
679
680/**
681 * Like `forEach`, but suitable for use with numbers and strings (which may be falsy).
682 * @template T
683 * @template U
684 * @param array
685 * @param callback
686 */
687export function firstDefined<T, U>(
688  array: readonly T[] | undefined,
689  callback: (element: T, index: number) => U | undefined,
690): U | undefined {
691  if (array === undefined) {
692    return undefined;
693  }
694
695  for (let i = 0; i < array.length; i++) {
696    const result = callback(array[i], i);
697    if (result !== undefined) {
698      return result;
699    }
700  }
701  return undefined;
702}
703