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. & -> & 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