kythe.io@v0.0.68-0.20240422202219-7225dbc01741/kythe/typescript/indexer.ts (about) 1 /* 2 * Copyright 2017 The Kythe Authors. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 import 'source-map-support/register'; 18 19 import * as fs from 'fs'; 20 import * as path from 'path'; 21 import * as ts from 'typescript'; 22 23 import {EdgeKind, FactName, JSONEdge, JSONFact, JSONMarkedSource, makeOrdinalEdge, MarkedSourceKind, NodeKind, OrdinalEdge, Subkind, VName} from './kythe'; 24 import * as utf8 from './utf8'; 25 import {CompilationUnit, Context, IndexerHost, IndexingOptions, Plugin, TSNamespace} from './plugin_api'; 26 27 const LANGUAGE = 'typescript'; 28 29 enum RefType { 30 READ, 31 WRITE, 32 READ_WRITE 33 } 34 35 /** 36 * toArray converts an Iterator to an array of its values. 37 * It's necessary when running in ES5 environments where for-of loops 38 * don't iterate through Iterators. 39 */ 40 function toArray<T>(it: Iterator<T>): T[] { 41 const array: T[] = []; 42 for (let next = it.next(); !next.done; next = it.next()) { 43 array.push(next.value); 44 } 45 return array; 46 } 47 48 /** 49 * stripExtension strips the .d.ts, .ts or .tsx extension from a path. 50 * It's used to map a file path to the module name. 51 */ 52 function stripExtension(path: string): string { 53 return path.replace(/\.(d\.)?tsx?$/, ''); 54 } 55 56 /** 57 * Determines if a node is a variable-like declaration. 58 * 59 * TODO(https://github.com/microsoft/TypeScript/issues/33115): Replace this with 60 * a native `ts.isHasExpressionInitializer` if TypeScript ever adds it. 61 */ 62 function hasExpressionInitializer(node: ts.Node): 63 node is ts.HasExpressionInitializer { 64 return ts.isVariableDeclaration(node) || ts.isParameter(node) || 65 ts.isBindingElement(node) || ts.isPropertySignature(node) || 66 ts.isPropertyDeclaration(node) || ts.isPropertyAssignment(node) || 67 ts.isEnumMember(node); 68 } 69 70 /** 71 * Determines if a node is a static member of a class. 72 */ 73 function isStaticMember(node: ts.Node, klass: ts.Declaration): boolean { 74 return ts.isPropertyDeclaration(node) && node.parent === klass && 75 ((ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Static) > 0); 76 } 77 78 function todo(sourceRoot: string, node: ts.Node, message: string) { 79 const sourceFile = node.getSourceFile(); 80 const file = path.relative(sourceRoot, sourceFile.fileName); 81 const {line, character} = 82 ts.getLineAndCharacterOfPosition(sourceFile, node.getStart()); 83 console.warn(`TODO: ${file}:${line}:${character}: ${message}`); 84 } 85 86 /** 87 * Convert VName to a string that can be used as key in Maps. 88 */ 89 function vnameToString(vname: VName): string { 90 return `(${vname.corpus},${vname.language},${vname.path},${vname.root},${ 91 vname.signature})`; 92 } 93 94 type NamespaceAndContext = string&{__brand: 'nsctx'}; 95 /** 96 * A SymbolVNameStore stores a mapping of symbols to the (many) VNames it may 97 * have. Each TypeScript symbol can be be of a different TypeScript namespace 98 * and be declared in a unique context, leading to a total (`TSNamespace` * 99 * `Context`) number of possible VNames for the symbol. 100 * 101 * TSNamespace + Context 102 * ----------- ------- 103 * TYPE Any 104 * ts.Symbol -> VALUE Getter -> VName 105 * NAMESPACE Setter 106 * ... 107 * 108 * The `Any` context makes no guarantee of symbol declaration disambiguation. 109 * As a result, unless explicitly set for a given symbol and namespace, the 110 * VName of an `Any` context is lazily set to the VName of an arbitrary context. 111 */ 112 class SymbolVNameStore { 113 private readonly store = 114 new Map<ts.Symbol, Map<NamespaceAndContext, Readonly<VName>>>(); 115 116 /** 117 * Serializes a namespace and context as a string to lookup in the store. 118 * 119 * Each instance of a JavaScript object is unique, so using one as a key fails 120 * because a new object would be generated every time the store is queried. 121 */ 122 private serialize(ns: TSNamespace, context: Context): NamespaceAndContext { 123 return `${ns}${context}` as NamespaceAndContext; 124 } 125 126 /** Get a symbol VName for a given namespace and context, if it exists. */ 127 get(symbol: ts.Symbol, ns: TSNamespace, context: Context): VName|undefined { 128 if (this.store.has(symbol)) { 129 const nsCtx = this.serialize(ns, context); 130 return this.store.get(symbol)!.get(nsCtx); 131 } 132 return undefined; 133 } 134 135 /** 136 * Set a symbol VName for a given namespace and context. Throws if a VName 137 * already exists. 138 */ 139 set(symbol: ts.Symbol, ns: TSNamespace, context: Context, vname: VName) { 140 let vnameMap = this.store.get(symbol); 141 const nsCtx = this.serialize(ns, context); 142 if (vnameMap) { 143 if (vnameMap.has(nsCtx)) { 144 throw new Error(`VName already set with signature ${ 145 vnameMap.get(nsCtx)!.signature}`); 146 } 147 vnameMap.set(nsCtx, vname); 148 } else { 149 this.store.set(symbol, new Map([[nsCtx, vname]])); 150 } 151 152 // Set the symbol VName for the given namespace and `Any` context, if it has 153 // not already been set. 154 const nsAny = this.serialize(ns, Context.Any); 155 vnameMap = this.store.get(symbol)!; 156 if (!vnameMap.has(nsAny)) { 157 vnameMap.set(nsAny, vname); 158 } 159 } 160 } 161 162 /** 163 * StandardIndexerContext provides the standard definition of information about 164 * a TypeScript program and common methods used by the TypeScript indexer and 165 * its plugins. See the IndexerContext interface definition for more details. 166 */ 167 class StandardIndexerContext implements IndexerHost { 168 private offsetTables = new Map<string, utf8.OffsetTable>(); 169 170 /** A shorter name for the rootDir in the CompilerOptions. */ 171 private sourceRoot: string; 172 173 /** 174 * rootDirs is the list of rootDirs in the compiler options, sorted 175 * longest first. See this.moduleName(). 176 */ 177 private rootDirs: string[]; 178 179 /** symbolNames is a store of ts.Symbols to their assigned VNames. */ 180 private symbolNames = new SymbolVNameStore(); 181 182 /** 183 * anonId increments for each anonymous block, to give them unique 184 * signatures. 185 */ 186 private anonId = 0; 187 188 /** 189 * anonNames maps nodes to the anonymous names assigned to them. 190 */ 191 private anonNames = new Map<ts.Node, string>(); 192 193 private typeChecker: ts.TypeChecker; 194 195 constructor( 196 public readonly program: ts.Program, 197 public readonly compilationUnit: CompilationUnit, 198 public readonly options: IndexingOptions, 199 ) { 200 this.sourceRoot = this.program.getCompilerOptions().rootDir || process.cwd(); 201 let rootDirs = this.program.getCompilerOptions().rootDirs || [this.sourceRoot]; 202 rootDirs = rootDirs.map(d => d + '/'); 203 rootDirs.sort((a, b) => b.length - a.length); 204 this.rootDirs = rootDirs; 205 this.typeChecker = this.program.getTypeChecker(); 206 } 207 208 getOffsetTable(path: string): Readonly<utf8.OffsetTable> { 209 let table = this.offsetTables.get(path); 210 if (!table) { 211 const buf = (this.options.readFile || fs.readFileSync)(path); 212 table = new utf8.OffsetTable(buf); 213 this.offsetTables.set(path, table); 214 } 215 return table; 216 } 217 218 getSymbolAtLocation(node: ts.Node): ts.Symbol|undefined { 219 // Practically any interesting node has a Symbol: variables, classes, functions. 220 // Both named and anonymous have Symbols. We tie Symbols to Vnames so its 221 // important to get Symbol object for as many nodes as possible. Unfortunately 222 // Typescript doesn't provide good API for extracting Symbol from Nodes. 223 // It is supported well for named nodes, probably logic being that if you can't 224 // refer to a node then no need to have Symbol. But for Kythe we need to handle 225 // anonymous nodes as well. So we do hacks here. 226 // See similar bugs that haven't been resolved properly: 227 // https://github.com/microsoft/TypeScript/issues/26511 228 // 229 // Open FR: https://github.com/microsoft/TypeScript/issues/55433 230 let sym = this.typeChecker.getSymbolAtLocation(node); 231 if (sym) return sym; 232 // Check if it's named node. 233 if ('name' in node) { 234 sym = this.typeChecker.getSymbolAtLocation((node as ts.NamedDeclaration).name!); 235 if (sym) return sym; 236 } 237 // Sad hack. Nodes have symbol property but it's not exposed in the API. 238 // We could create our own Symbol instance to avoid depending on non-public API. 239 // But it's not clear whether it will be less maintainance. 240 return (node as any).symbol; 241 } 242 243 getSymbolAtLocationFollowingAliases(node: ts.Node): ts.Symbol|undefined { 244 let sym = this.getSymbolAtLocation(node); 245 while (sym && (sym.flags & ts.SymbolFlags.Alias) > 0) { 246 // a hack to prevent following aliases in cases like: 247 // import * as fooNamespace from './foo'; 248 // here fooNamespace is an alias for the 'foo' module. 249 // We don't want to follow it so that users can easier usages 250 // of fooNamespace in the file. 251 const decl = sym.declarations?.[0]; 252 if (decl && ts.isNamespaceImport(decl)) { 253 break; 254 } 255 256 sym = this.typeChecker.getAliasedSymbol(sym); 257 } 258 return sym; 259 } 260 261 /** 262 * anonName assigns a freshly generated name to a Node. 263 * It's used to give stable names to e.g. anonymous objects. 264 */ 265 anonName(node: ts.Node): string { 266 let name = this.anonNames.get(node); 267 if (!name) { 268 name = `anon${this.anonId++}`; 269 this.anonNames.set(node, name); 270 } 271 return name; 272 } 273 274 /** 275 * scopedSignature computes a scoped name for a ts.Node. 276 * E.g. if you have a function `foo` containing a block containing a variable 277 * `bar`, it might return a VName like 278 * signature: "foo.block0.bar"" 279 * path: <appropriate path to module> 280 */ 281 scopedSignature(startNode: ts.Node): VName { 282 let moduleName: string|undefined; 283 const parts: string[] = []; 284 285 // Traverse the containing blocks upward, gathering names from nodes that 286 // introduce scopes. 287 for (let node: ts.Node|undefined = startNode, 288 lastNode: ts.Node|undefined = undefined; 289 node != null; lastNode = node, node = node.parent) { 290 // Nodes that are rvalues of a named initialization should not introduce a 291 // new scope. For instance, in `const a = class A {}`, `A` should 292 // contribute nothing to the scoped signature. 293 if (node.parent && hasExpressionInitializer(node.parent) && 294 node.parent.name.kind === ts.SyntaxKind.Identifier) { 295 continue; 296 } 297 298 switch (node.kind) { 299 case ts.SyntaxKind.ExportAssignment: 300 const exportDecl = node as ts.ExportAssignment; 301 if (!exportDecl.isExportEquals) { 302 // It's an "export default" statement. 303 // This is semantically equivalent to exporting a variable 304 // named 'default'. 305 parts.push('default'); 306 } else { 307 parts.push('export='); 308 } 309 break; 310 case ts.SyntaxKind.ArrowFunction: 311 // Arrow functions are anonymous, so generate a unique id. 312 parts.push(`arrow${this.anonId++}`); 313 break; 314 case ts.SyntaxKind.FunctionExpression: 315 // Function expressions look like 316 // (function() {}) 317 // which have no name but introduce an anonymous scope. 318 parts.push(`func${this.anonId++}`); 319 break; 320 case ts.SyntaxKind.Block: 321 // Blocks need their own scopes for contained variable declarations. 322 if (node.parent && 323 (node.parent.kind === ts.SyntaxKind.FunctionDeclaration || 324 node.parent.kind === ts.SyntaxKind.MethodDeclaration || 325 node.parent.kind === ts.SyntaxKind.Constructor || 326 node.parent.kind === ts.SyntaxKind.ForStatement || 327 node.parent.kind === ts.SyntaxKind.ForInStatement || 328 node.parent.kind === ts.SyntaxKind.ForOfStatement)) { 329 // A block that's an immediate child of the above node kinds 330 // already has a scoped name generated by that parent. 331 // (It would be fine to not handle this specially and just fall 332 // through to the below code, but avoiding it here makes the names 333 // simpler.) 334 continue; 335 } 336 parts.push(`block${this.anonId++}`); 337 break; 338 case ts.SyntaxKind.ForStatement: 339 case ts.SyntaxKind.ForInStatement: 340 case ts.SyntaxKind.ForOfStatement: 341 // Introduce a naming scope for all variables declared within the 342 // statement, so that the two 'x's declared here get different names: 343 // for (const x in y) { ... } 344 // for (const x in y) { ... } 345 parts.push(`for${this.anonId++}`); 346 break; 347 case ts.SyntaxKind.BindingElement: 348 case ts.SyntaxKind.ClassDeclaration: 349 case ts.SyntaxKind.ClassExpression: 350 case ts.SyntaxKind.EnumDeclaration: 351 case ts.SyntaxKind.EnumMember: 352 case ts.SyntaxKind.FunctionDeclaration: 353 case ts.SyntaxKind.InterfaceDeclaration: 354 case ts.SyntaxKind.ImportEqualsDeclaration: 355 case ts.SyntaxKind.ImportSpecifier: 356 case ts.SyntaxKind.ExportSpecifier: 357 case ts.SyntaxKind.MethodDeclaration: 358 case ts.SyntaxKind.MethodSignature: 359 case ts.SyntaxKind.NamespaceImport: 360 case ts.SyntaxKind.ObjectLiteralExpression: 361 case ts.SyntaxKind.Parameter: 362 case ts.SyntaxKind.PropertyAccessExpression: 363 case ts.SyntaxKind.PropertyAssignment: 364 case ts.SyntaxKind.PropertyDeclaration: 365 case ts.SyntaxKind.PropertySignature: 366 case ts.SyntaxKind.TypeAliasDeclaration: 367 case ts.SyntaxKind.TypeParameter: 368 case ts.SyntaxKind.VariableDeclaration: 369 case ts.SyntaxKind.GetAccessor: 370 case ts.SyntaxKind.SetAccessor: 371 case ts.SyntaxKind.ShorthandPropertyAssignment: 372 const decl = node as ts.NamedDeclaration; 373 if (decl.name) { 374 switch (decl.name.kind) { 375 case ts.SyntaxKind.Identifier: 376 case ts.SyntaxKind.PrivateIdentifier: 377 case ts.SyntaxKind.StringLiteral: 378 case ts.SyntaxKind.NumericLiteral: 379 case ts.SyntaxKind.ComputedPropertyName: 380 case ts.SyntaxKind.NoSubstitutionTemplateLiteral: 381 let part; 382 if (ts.isComputedPropertyName(decl.name)) { 383 const sym = this.getSymbolAtLocationFollowingAliases(decl.name); 384 part = sym ? sym.name : this.anonName(decl.name); 385 } else { 386 part = decl.name.text; 387 } 388 // Wrap literals in quotes, so that characters used in other 389 // signatures do not interfere with the signature created by a 390 // literal. For instance, a literal 391 // obj.prop 392 // may interefere with the signature of `prop` on an object 393 // `obj`. The literal receives a signature 394 // "obj.prop" 395 // to avoid this. 396 if (ts.isStringLiteral(decl.name)) { 397 part = `"${part}"`; 398 } 399 // Instance members of a class are scoped to the type of the 400 // class. 401 if (ts.isClassDeclaration(decl) && lastNode !== undefined && 402 ts.isClassElement(lastNode) && 403 !isStaticMember(lastNode, decl) && 404 // special case constructor. We want it to no have 405 // #type modifier as constructor will have the same name 406 // as class. 407 !ts.isConstructorDeclaration(startNode)) { 408 part += '#type'; 409 } 410 // Getters and setters semantically refer to the same entities 411 // but are declared differently, so they are differentiated. 412 if (ts.isGetAccessor(decl)) { 413 part += ':getter'; 414 } else if (ts.isSetAccessor(decl)) { 415 part += ':setter'; 416 } 417 parts.push(part); 418 break; 419 default: 420 // Skip adding an anonymous scope for variables declared in an 421 // array or object binding pattern like `const [a] = [0]`. 422 break; 423 } 424 } else { 425 parts.push(this.anonName(node)); 426 } 427 break; 428 case ts.SyntaxKind.Constructor: 429 // Class members declared with a shorthand in the constructor should 430 // be scoped to the class, not the constructor. 431 if (!ts.isParameterPropertyDeclaration(startNode, startNode.parent) && 432 startNode !== node) { 433 parts.push('constructor'); 434 } 435 break; 436 case ts.SyntaxKind.ImportClause: 437 // An import clause can have one of two forms: 438 // import foo from './bar'; 439 // import {foo as far} from './bar'; 440 // In the first case the clause has a name "foo". In this case add the 441 // name of the clause to the signature. 442 // In the second case the clause has no explicit name. This 443 // contributes nothing to the signature without risk of naming 444 // conflicts because TS imports are essentially file-global lvalues. 445 const importClause = node as ts.ImportClause; 446 if (importClause.name) { 447 parts.push(importClause.name.text); 448 } 449 break; 450 case ts.SyntaxKind.ModuleDeclaration: 451 const modDecl = node as ts.ModuleDeclaration; 452 if (modDecl.name.kind === ts.SyntaxKind.StringLiteral) { 453 // Syntax like: 454 // declare module 'foo/bar' {} 455 // This is the syntax for defining symbols in another, named 456 // module. 457 moduleName = (modDecl.name as ts.StringLiteral).text; 458 } else if (modDecl.name.kind === ts.SyntaxKind.Identifier) { 459 // Syntax like: 460 // declare module foo {} 461 // without quotes is just an obsolete way of saying 'namespace'. 462 parts.push((modDecl.name as ts.Identifier).text); 463 } 464 break; 465 case ts.SyntaxKind.SourceFile: 466 // moduleName can already be set if the target was contained within 467 // a "declare module 'foo/bar'" block (see the handling of 468 // ModuleDeclaration). Otherwise, the module name is derived from the 469 // name of the current file. 470 if (!moduleName) { 471 moduleName = this.moduleName((node as ts.SourceFile).fileName); 472 } 473 break; 474 case ts.SyntaxKind.JsxElement: 475 case ts.SyntaxKind.JsxSelfClosingElement: 476 case ts.SyntaxKind.JsxAttribute: 477 // Given a unique anonymous name to all JSX nodes. This prevents 478 // conflicts in cases where attributes would otherwise have the same 479 // name, like `src` in 480 // <img src={a} /> 481 // <img src={b} /> 482 parts.push(`jsx${this.anonId++}`); 483 break; 484 default: 485 // Most nodes are children of other nodes that do not introduce a 486 // new namespace, e.g. "return x;", so ignore all other parents 487 // by default. 488 // TODO: namespace {}, etc. 489 490 // If the node is actually some subtype that has a 'name' attribute 491 // and it's not empty it's likely this function should have handled 492 // it. Dynamically probe for this case and warn if we missed one. 493 if ((node as any).name != null) { 494 todo( 495 this.sourceRoot, node, 496 `scopedSignature: ${ts.SyntaxKind[node.kind]} ` + 497 `has unused 'name' property`); 498 } 499 } 500 } 501 502 // The names were gathered from bottom to top, so reverse before joining. 503 const signature = parts.reverse().join('.'); 504 return Object.assign( 505 this.pathToVName(moduleName!), {signature, language: LANGUAGE}); 506 } 507 508 /** 509 * getSymbolName computes the VName of a ts.Symbol. A Context can be 510 * optionally specified to help disambiguate nodes with multiple declarations. 511 * See the documentation of Context for more information. 512 */ 513 getSymbolName( 514 sym: ts.Symbol, ns: TSNamespace, context: Context = Context.Any): VName 515 |undefined { 516 const stored = this.symbolNames.get(sym, ns, context); 517 if (stored) return stored; 518 519 if (!sym.declarations || sym.declarations.length < 1) { 520 return undefined; 521 } 522 523 let declarations = sym.declarations; 524 // Disambiguate symbols with multiple declarations using a context. 525 if (sym.declarations.length > 1) { 526 switch (context) { 527 case Context.Getter: 528 declarations = declarations.filter(ts.isGetAccessor); 529 break; 530 case Context.Setter: 531 declarations = declarations.filter(ts.isSetAccessor); 532 break; 533 default: 534 break; 535 } 536 } 537 538 const decl = declarations[0]; 539 const vname = this.scopedSignature(decl); 540 // The signature of a value is undecorated. 541 // The signature of a type has the #type suffix. 542 // The signature of a namespace has the #namespace suffix. 543 if (ns === TSNamespace.TYPE) { 544 vname.signature += '#type'; 545 } else if (ns === TSNamespace.NAMESPACE) { 546 vname.signature += '#namespace'; 547 } else if (ns === TSNamespace.TYPE_MIGRATION) { 548 vname.signature += '#mtype'; 549 } 550 551 // Cache the VName for future lookups. 552 this.symbolNames.set(sym, ns, context, vname); 553 return vname; 554 } 555 556 /** 557 * moduleName returns the ES6 module name of a path to a source file. 558 * E.g. foo/bar.ts and foo/bar.d.ts both have the same module name, 559 * 'foo/bar', and rootDirs (like bazel-bin/) are eliminated. 560 * See README.md for a discussion of this. 561 */ 562 moduleName(sourcePath: string): string { 563 // Compute sourcePath as relative to one of the rootDirs. 564 // This canonicalizes e.g. bazel-bin/foo to just foo. 565 // Note that this.rootDirs is sorted longest first, so we'll use the 566 // longest match. 567 for (const rootDir of this.rootDirs) { 568 if (sourcePath.startsWith(rootDir)) { 569 sourcePath = path.relative(rootDir, sourcePath); 570 break; 571 } 572 } 573 return stripExtension(sourcePath); 574 } 575 576 /** 577 * pathToVName returns the VName for a given file path. 578 * 579 * This function is used for 2 distinct cases that should be ideally separated 580 * in 2 different functions. `path` can be one of two: 581 * 1. Full path like 'bazel-out/genfiles/path/to/file.ts'. 582 * This path is used to build VNames for files and anchors. 583 * 2. Module name like 'path/to/file'. 584 * This path is used to build VNames for semantic nodes. 585 * 586 * Only for full paths `pathVnames` contains an entry. For short paths (module 587 * names) this function will defaults to calculating vname based on path 588 * and compilation unit. 589 */ 590 pathToVName(path: string): VName { 591 const vname = this.compilationUnit.fileVNames.get(path); 592 return { 593 signature: '', 594 language: '', 595 corpus: vname && vname.corpus ? vname.corpus : 596 this.compilationUnit.rootVName.corpus, 597 root: vname && vname.corpus ? vname.root : this.compilationUnit.rootVName.root, 598 path: vname && vname.path ? vname.path : path, 599 }; 600 } 601 } 602 603 const RE_FIRST_NON_WS = /\S|$/; 604 const RE_NEWLINE = /\r?\n/; 605 const MAX_MS_TEXT_LENGTH = 1_000; 606 /** 607 * Formats a MarkedSource text component by 608 * - stripping the text to its first MAX_MS_TEXT_LENGTH characters 609 * - trimming the text 610 * - stripping the leading whitespace of each line in multi-line string by the 611 * shortest non-zero whitespace length. For example, 612 * [ 613 * 1, 614 * 2, 615 * ] 616 * becomes 617 * [ 618 * 1, 619 * 2, 620 * ] 621 */ 622 function fmtMarkedSource(s: string) { 623 if (s.search(RE_FIRST_NON_WS) === s.length) { 624 // String is all whitespace, keep as-is 625 return s; 626 } 627 let isChopped = false; 628 if (s.length > MAX_MS_TEXT_LENGTH) { 629 // Trim left first to pick up more chars before chopping the string 630 s = s.trimLeft(); 631 s = s.substring(0, MAX_MS_TEXT_LENGTH); 632 isChopped = true; 633 } 634 s = s.replace(/\t/g, ' '); // normalize tabs for display 635 const lines = s.split(RE_NEWLINE); 636 let shortestLeading = lines[lines.length - 1].search(RE_FIRST_NON_WS); 637 for (let i = 1; i < lines.length - 1; ++i) { 638 shortestLeading = 639 Math.min(shortestLeading, lines[i].search(RE_FIRST_NON_WS)); 640 } 641 for (let i = 1; i < lines.length; ++i) { 642 lines[i] = lines[i].substring(shortestLeading); 643 } 644 s = lines.join('\n'); 645 if (isChopped) { 646 s += '...'; 647 } 648 return s; 649 } 650 651 function isNonNullableArray<T>(arr: Array<T>): arr is Array<NonNullable<T>> { 652 return arr.findIndex(el => el === undefined || el === null) === -1; 653 } 654 655 /** Visitor manages the indexing process for a single TypeScript SourceFile. */ 656 class Visitor { 657 /** kFile is the VName for the 'file' node representing the source file. */ 658 kFile: VName; 659 660 /** A shorter name for the rootDir in the CompilerOptions. */ 661 sourceRoot: string; 662 663 typeChecker: ts.TypeChecker; 664 665 /** influencers is a stack of influencer VNames. */ 666 private readonly influencers: Set<VName>[] = []; 667 668 /** Cached anchor nodes. Signature is used as key. */ 669 private readonly anchors = new Map<string, VName>(); 670 671 constructor( 672 private readonly host: IndexerHost, 673 private file: ts.SourceFile, 674 ) { 675 this.sourceRoot = 676 this.host.program.getCompilerOptions().rootDir || process.cwd(); 677 678 this.typeChecker = this.host.program.getTypeChecker(); 679 680 this.kFile = this.newFileVName(file.fileName); 681 } 682 683 /** 684 * newFileVName returns a new VName for the given file path. 685 */ 686 newFileVName(path: string): VName { 687 return this.host.pathToVName(path); 688 } 689 690 /** 691 * newVName returns a new VName with a given signature and path. 692 */ 693 newVName(signature: string, path: string): VName { 694 return Object.assign( 695 this.newFileVName(path), {signature: signature, language: LANGUAGE}); 696 } 697 698 /** newAnchor emits a new anchor entry that covers a TypeScript node. */ 699 newAnchor(node: ts.Node, start = node.getStart(), end = node.end, tag = ''): 700 VName { 701 const signature = `@${tag}${start}:${end}`; 702 const cachedName = this.anchors.get(signature); 703 if (cachedName != null) return cachedName; 704 705 const name = 706 Object.assign({...this.kFile}, {signature, language: LANGUAGE}); 707 this.emitNode(name, NodeKind.ANCHOR); 708 const offsetTable = this.host.getOffsetTable(node.getSourceFile().fileName); 709 this.emitFact( 710 name, FactName.LOC_START, offsetTable.lookupUtf8(start).toString()); 711 this.emitFact( 712 name, FactName.LOC_END, offsetTable.lookupUtf8(end).toString()); 713 this.anchors.set(signature, name); 714 return name; 715 } 716 717 /** emitNode emits a new node entry, declaring the kind of a VName. */ 718 emitNode(source: VName, kind: NodeKind) { 719 this.emitFact(source, FactName.NODE_KIND, kind); 720 } 721 722 /** emitSubkind emits a new fact entry, declaring the subkind of a VName. */ 723 emitSubkind(source: VName, subkind: Subkind) { 724 this.emitFact(source, FactName.SUBKIND, subkind); 725 } 726 727 /** emitFact emits a new fact entry, tying an attribute to a VName. */ 728 emitFact(source: VName, name: FactName, value: string|Uint8Array) { 729 this.host.options.emit({ 730 source, 731 fact_name: name, 732 fact_value: Buffer.from(value).toString('base64'), 733 }); 734 } 735 736 /** emitEdge emits a new edge entry, relating two VNames. */ 737 emitEdge(source: VName, kind: EdgeKind|OrdinalEdge, target: VName) { 738 this.host.options.emit({ 739 source, 740 edge_kind: kind, 741 target, 742 fact_name: '/', 743 }); 744 } 745 746 /** forAllInfluencers executes fn for each influencer in the active set. */ 747 forAllInfluencers(fn: (influencer: VName) => void) { 748 if (this.influencers.length != 0) { 749 this.influencers[this.influencers.length - 1].forEach(fn); 750 } 751 } 752 753 /** addInfluencer adds influencer to the active set. */ 754 addInfluencer(influencer: VName) { 755 if (this.influencers.length != 0) { 756 this.influencers[this.influencers.length - 1].add(influencer); 757 } 758 } 759 760 /** popInfluencers pops the active influencer set. */ 761 popInfluencers() { 762 if (this.influencers.length != 0) { 763 this.influencers.pop(); 764 } 765 } 766 767 /** pushInfluencers pushes a new active influencer set. */ 768 pushInfluencers() { 769 this.influencers.push(new Set<VName>()); 770 } 771 772 visitTypeParameters( 773 parent: VName|undefined, 774 params: ReadonlyArray<ts.TypeParameterDeclaration>) { 775 for (var ordinal = 0; ordinal < params.length; ++ordinal) { 776 const param = params[ordinal]; 777 const sym = this.host.getSymbolAtLocationFollowingAliases(param.name); 778 if (!sym) { 779 todo( 780 this.sourceRoot, param, 781 `type param ${param.getText()} has no symbol`); 782 return; 783 } 784 const kTVar = this.host.getSymbolName(sym, TSNamespace.TYPE_MIGRATION); 785 if (kTVar && parent) { 786 this.emitNode(kTVar, NodeKind.TVAR); 787 this.emitEdge( 788 this.newAnchor( 789 param.name, param.name.getStart(), param.name.end, 'M'), 790 EdgeKind.DEFINES_BINDING, kTVar); 791 this.emitEdge(parent, makeOrdinalEdge(EdgeKind.TPARAM, ordinal), kTVar); 792 // ...<T extends A> 793 if (param.constraint) { 794 var superType = this.visitType(param.constraint); 795 if (superType) 796 this.emitEdge(kTVar, EdgeKind.BOUNDED_UPPER, superType); 797 } 798 // ...<T = A> 799 if (param.default) this.visitType(param.default); 800 } 801 } 802 } 803 804 /** 805 * Adds influence edges for return statements. 806 */ 807 visitReturnStatement(node: ts.ReturnStatement) { 808 this.pushInfluencers(); 809 ts.forEachChild(node, n => { 810 this.visit(n); 811 }); 812 const containingFunction = this.getContainingFunctionNode(node); 813 if (!ts.isSourceFile(containingFunction)) { 814 const containingVName = 815 this.getSymbolAndVNameForFunctionDeclaration(containingFunction) 816 .vname; 817 if (containingVName) { 818 this.forAllInfluencers(influencer => { 819 this.emitEdge(influencer, EdgeKind.INFLUENCES, containingVName); 820 }); 821 } 822 // Handle case like 823 // "return {name: 'Alice'};" 824 // where return type of the function is a named type, e.g. Person. 825 // This will connect 'name' property of the object literal to the 826 // Person.name property. 827 if (node.expression && ts.isObjectLiteralExpression(node.expression)) { 828 this.connectObjectLiteralToType( 829 node.expression, containingFunction.type); 830 } 831 } 832 this.popInfluencers(); 833 } 834 835 getCallAnchor(callee:any) { 836 if (!this.host.options.emitRefCallOverIdentifier) { 837 return undefined; 838 } 839 for (;;) { 840 if (ts.isIdentifier(callee)) { 841 return this.newAnchor(callee); 842 } 843 if (ts.isPropertyAccessExpression(callee)) { 844 callee = callee.name; 845 continue; 846 } 847 if (ts.isNewExpression(callee)) { 848 callee = callee.expression; 849 continue; 850 } 851 return undefined; 852 } 853 } 854 855 /** 856 * Emits `ref/call` edges required for call graph: 857 * https://kythe.io/docs/schema/callgraph.html 858 */ 859 visitCallOrNewExpression(node: ts.CallExpression|ts.NewExpression) { 860 ts.forEachChild(node, n => { 861 this.visit(n); 862 }); 863 864 // Special case dynamic imports as they are represendted as CallExpressions. 865 // We don't want to emit ref/call as we don't have anything to point it at: 866 // there is no import() function 867 if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) { 868 this.visitDynamicImportCall(node); 869 return; 870 } 871 // Handle case of immediately-invoked functions like: 872 // (() => do stuff... )(); 873 let expression: ts.Node = node.expression; 874 while (ts.isParenthesizedExpression(expression)) { 875 expression = expression.expression; 876 } 877 const symbol = this.host.getSymbolAtLocationFollowingAliases(expression); 878 if (!symbol) { 879 return; 880 } 881 const name = this.host.getSymbolName(symbol, TSNamespace.VALUE); 882 if (!name) { 883 return; 884 } 885 const callAnchor = this.getCallAnchor(node.expression) ?? this.newAnchor(node); 886 this.emitEdge(callAnchor, EdgeKind.REF_CALL, name); 887 888 // Each call should have a childof edge to its containing function 889 // scope. 890 const containingFunction = this.getContainingFunctionNode(node); 891 let containingVName: VName|undefined; 892 if (ts.isSourceFile(containingFunction)) { 893 containingVName = this.getSyntheticFileInitVName(); 894 } else { 895 containingVName = 896 this.getSymbolAndVNameForFunctionDeclaration(containingFunction) 897 .vname; 898 } 899 if (containingVName) { 900 this.emitEdge(callAnchor, EdgeKind.CHILD_OF, containingVName); 901 } 902 // Handle function/constructor calls like `doSomething({name: 'Alice'})` 903 // where type of the argument is, for example, an interface Person. This 904 // will add ref from object literal 'name' property to Person.name field. 905 if (node.arguments != null) { 906 const signature = this.typeChecker.getResolvedSignature(node); 907 if (signature == null) return; 908 for (let i = 0; i < node.arguments.length; i++) { 909 const argument = node.arguments[i]; 910 // get parameter of the function/constructor signature. 911 const signParameter = signature.parameters[i]?.valueDeclaration; 912 if (ts.isObjectLiteralExpression(argument) && signParameter && 913 ts.isParameter(signParameter)) { 914 this.connectObjectLiteralToType(argument, signParameter.type); 915 } 916 } 917 } 918 } 919 920 /** 921 * visitHeritage visits the X found in an 'extends X' or 'implements X'. 922 * 923 * These are subtle in an interesting way. When you have 924 * interface X extends Y {} 925 * that is referring to the *type* Y (because interfaces are types, not 926 * values). But it's also legal to write 927 * class X extends (class Z { ... }) {} 928 * where the thing in the extends clause is itself an expression, and the 929 * existing logic for visiting a class expression already handles modelling 930 * the class as both a type and a value. 931 * 932 * The full set of possible combinations is: 933 * - class extends => value 934 * - interface extends => type 935 * - class implements => type 936 * - interface implements => illegal 937 */ 938 visitHeritage( 939 classOrInterface: VName|undefined, 940 heritageClauses: ReadonlyArray<ts.HeritageClause>) { 941 for (const heritage of heritageClauses) { 942 if (heritage.token === ts.SyntaxKind.ExtendsKeyword && heritage.parent && 943 heritage.parent.kind !== ts.SyntaxKind.InterfaceDeclaration) { 944 this.visit(heritage); 945 } else { 946 this.visitType(heritage); 947 } 948 // Add extends edges. 949 if (classOrInterface == null) { 950 // classOrInterface is null for anonymous classes. 951 // But anonymous classes can implement and extends other 952 // classes and interfaces. So currently this edge 953 // is missing. Once we have nodes for anonymous classes - 954 // we can add this missing edges. 955 continue; 956 } 957 for (const baseType of heritage.types) { 958 const type = this.typeChecker.getTypeAtLocation(baseType); 959 if (!type || !type.symbol) { 960 continue; 961 } 962 const vname = this.host.getSymbolName(type.symbol, TSNamespace.TYPE); 963 if (vname) { 964 this.emitEdge(classOrInterface, EdgeKind.EXTENDS, vname); 965 } 966 } 967 } 968 } 969 970 visitInterfaceDeclaration(decl: ts.InterfaceDeclaration) { 971 const sym = this.host.getSymbolAtLocationFollowingAliases(decl.name); 972 if (!sym) { 973 todo( 974 this.sourceRoot, decl.name, 975 `interface ${decl.name.getText()} has no symbol`); 976 return; 977 } 978 const kType = this.host.getSymbolName(sym, TSNamespace.TYPE); 979 if (kType) { 980 this.emitNode(kType, NodeKind.INTERFACE); 981 this.emitEdge(this.newAnchor(decl.name), EdgeKind.DEFINES_BINDING, kType); 982 this.visitJSDoc(decl, kType); 983 this.emitMarkedSourceForClasslikeDeclaration(decl, kType); 984 } 985 986 if (decl.typeParameters) 987 this.visitTypeParameters(kType, decl.typeParameters); 988 if (decl.heritageClauses) this.visitHeritage(kType, decl.heritageClauses); 989 for (const member of decl.members) { 990 this.visit(member); 991 } 992 } 993 994 visitTypeAliasDeclaration(decl: ts.TypeAliasDeclaration) { 995 const sym = this.host.getSymbolAtLocationFollowingAliases(decl.name); 996 if (!sym) { 997 todo( 998 this.sourceRoot, decl.name, 999 `type alias ${decl.name.getText()} has no symbol`); 1000 return; 1001 } 1002 const kType = this.host.getSymbolName(sym, TSNamespace.TYPE); 1003 if (!kType) return; 1004 this.emitNode(kType, NodeKind.TALIAS); 1005 this.emitEdge(this.newAnchor(decl.name), EdgeKind.DEFINES_BINDING, kType); 1006 this.visitJSDoc(decl, kType); 1007 1008 if (decl.typeParameters) 1009 this.visitTypeParameters(kType, decl.typeParameters); 1010 const kAlias = this.visitType(decl.type); 1011 // Emit an "aliases" edge if the aliased type has a singular VName. 1012 if (kAlias) { 1013 this.emitEdge(kType, EdgeKind.ALIASES, kAlias); 1014 } 1015 } 1016 1017 /** 1018 * visitType is the main dispatch for visiting type nodes. 1019 * It's separate from visit() because bare ts.Identifiers within a normal 1020 * expression are values (handled by visit) but bare ts.Identifiers within 1021 * a type are types (handled here). 1022 * 1023 * @return the VName of the type, if available. (For more complex types, 1024 * e.g. Array<string>, we might not have a VName for the specific type.) 1025 */ 1026 visitType(node: ts.Node): VName|undefined { 1027 switch (node.kind) { 1028 case ts.SyntaxKind.TypeReference: 1029 const ref = node as ts.TypeReferenceNode; 1030 const kType = this.visitType((node as ts.TypeReferenceNode).typeName); 1031 1032 // Return no VName for types with type arguments because their VName is 1033 // not qualified. E.g. 1034 // Array<string> 1035 // Array<number> 1036 // have the same VName signature "Array" 1037 if (ref.typeArguments) { 1038 ref.typeArguments.forEach(type => this.visitType(type)); 1039 return; 1040 } 1041 return kType; 1042 case ts.SyntaxKind.Identifier: 1043 const sym = this.host.getSymbolAtLocationFollowingAliases(node); 1044 if (!sym) { 1045 todo(this.sourceRoot, node, `type ${node.getText()} has no symbol`); 1046 return; 1047 } 1048 const name = this.host.getSymbolName(sym, TSNamespace.TYPE); 1049 if (name) { 1050 this.emitEdge(this.newAnchor(node), EdgeKind.REF, name); 1051 } 1052 const mname = this.host.getSymbolName(sym, TSNamespace.TYPE_MIGRATION); 1053 if (mname) { 1054 this.emitEdge( 1055 this.newAnchor(node, node.getStart(), node.end, 'M'), 1056 EdgeKind.REF, mname); 1057 } 1058 return name; 1059 case ts.SyntaxKind.TypeReference: 1060 const typeRef = node as ts.TypeReferenceNode; 1061 if (!typeRef.typeArguments) { 1062 // If it's an direct type reference, e.g. SomeInterface 1063 // as opposed to SomeInterface<T>, then the VName for the type 1064 // reference is just the inner type name. 1065 return this.visitType(typeRef.typeName); 1066 } 1067 // Otherwise, leave it to the default handling. 1068 break; 1069 case ts.SyntaxKind.TypeQuery: 1070 // This is a 'typeof' expression, which takes a value as its argument, 1071 // so use visit() instead of visitType(). 1072 const typeQuery = node as ts.TypeQueryNode; 1073 this.visit(typeQuery.exprName); 1074 return; // Avoid default recursion. 1075 } 1076 1077 // Default recursion, but using visitType(), not visit(). 1078 ts.forEachChild(node, n => { 1079 this.visitType(n); 1080 }); 1081 // Because we don't know the specific thing we visited, give the caller 1082 // back no name. 1083 return undefined; 1084 } 1085 1086 /** 1087 * getPathFromModule gets the "module path" from the module import 1088 * symbol referencing a module system path to reference to a module. 1089 * 1090 * E.g. from 1091 * import ... from './foo'; 1092 * getPathFromModule(the './foo' node) might return a string like 1093 * 'path/to/project/foo'. See this.moduleName(). 1094 */ 1095 getModulePathFromModuleReference(sym: ts.Symbol): string|undefined { 1096 const sf = sym.valueDeclaration; 1097 // If the module is not a source file, it does not have a unique file path. 1098 // This can happen in cases of importing local modules, like 1099 // declare namespace Foo {} 1100 // import foo = Foo; 1101 if (!sf || !ts.isSourceFile(sf)) return undefined; 1102 return this.host.moduleName(sf.fileName); 1103 } 1104 1105 /** 1106 * Returns the location of a text in the source code of a node. 1107 */ 1108 getTextSpan(node: ts.Node, text: string): {start: number, end: number} { 1109 const ofs = node.getText().indexOf(text); 1110 if (ofs < 0) throw new Error(`${text} not found in ${node.getText()}`); 1111 const start = node.getStart() + ofs; 1112 const end = start + text.length; 1113 return {start, end}; 1114 } 1115 1116 /** 1117 * Returns the symbol of a class constructor if it exists, otherwise nothing. 1118 */ 1119 getCtorSymbol(klass: ts.ClassDeclaration): ts.Symbol|undefined { 1120 if (klass.name) { 1121 const sym = this.host.getSymbolAtLocationFollowingAliases(klass.name); 1122 if (sym && sym.members) { 1123 return sym.members.get(ts.InternalSymbolName.Constructor); 1124 } 1125 } 1126 return undefined; 1127 } 1128 1129 getSymbolAndVNameForFunctionDeclaration(node: ts.FunctionLikeDeclaration): 1130 {sym?: ts.Symbol, vname?: VName} { 1131 let context: Context|undefined = undefined; 1132 if (ts.isGetAccessor(node)) { 1133 context = Context.Getter; 1134 } else if (ts.isSetAccessor(node)) { 1135 context = Context.Setter; 1136 } 1137 const sym = this.host.getSymbolAtLocationFollowingAliases(node); 1138 if (!sym) { 1139 return {}; 1140 } 1141 const vname = this.host.getSymbolName(sym, TSNamespace.VALUE, context); 1142 return {sym, vname}; 1143 } 1144 1145 /** 1146 * Given a node finds a function node that contains given node and returns it. 1147 * If the node is not inside a function - returns SourceFile node. Examples: 1148 * 1149 * function foo() { 1150 * var b = 123; 1151 * } 1152 * var c = 567; 1153 * 1154 * For 'b' node this function will return 'foo' node. 1155 * For 'c' node this function will return SourceFile node. 1156 */ 1157 getContainingFunctionNode(node: ts.Node): ts.FunctionLikeDeclaration 1158 |ts.SourceFile { 1159 node = node.parent; 1160 for (; node.kind !== ts.SyntaxKind.SourceFile; node = node.parent) { 1161 const kind = node.kind; 1162 if (kind === ts.SyntaxKind.FunctionDeclaration || 1163 kind === ts.SyntaxKind.FunctionExpression || 1164 kind === ts.SyntaxKind.ArrowFunction || 1165 kind === ts.SyntaxKind.MethodDeclaration || 1166 kind === ts.SyntaxKind.Constructor || 1167 kind === ts.SyntaxKind.GetAccessor || 1168 kind === ts.SyntaxKind.SetAccessor || 1169 kind === ts.SyntaxKind.MethodSignature) { 1170 return node as ts.FunctionLikeDeclaration; 1171 } 1172 } 1173 return node as ts.SourceFile; 1174 } 1175 1176 getSyntheticFileInitVName(): VName { 1177 return this.newVName( 1178 'fileInit:synthetic', this.host.moduleName(this.file.fileName)); 1179 } 1180 1181 /** 1182 * visitImport handles a single entry in an import statement, e.g. 1183 * "bar" in code like 1184 * import {foo, bar} from 'baz'; 1185 * See visitImportDeclaration for the code handling the entire statement. 1186 * 1187 * @param name TypeScript import declaration node 1188 * @param bindingAnchor anchor that "defines/binding" the local import 1189 * definition. If the bindingAnchor is null then the local definition 1190 * won't be created and refs to the local definition will be reassigned 1191 * to the remote definition. 1192 * @param refAnchor anchor that "ref" the import's remote declaration 1193 */ 1194 visitImport( 1195 name: ts.Node, bindingAnchor: Readonly<VName>|null, 1196 refAnchor: Readonly<VName>) { 1197 // An import both aliases a symbol from another module 1198 // (call it the remote symbol) and it defines a local symbol. 1199 // 1200 // Those two symbols often have the same name, with statements like: 1201 // import {foo} from 'bar'; 1202 // But they can be different, in e.g. 1203 // import {foo as baz} from 'bar'; 1204 // Which maps the remote symbol named 'foo' to a local named 'baz'. 1205 // 1206 // In all cases TypeScript maintains two different ts.Symbol objects, 1207 // one for the local and one for the remote. In principle for the 1208 // simple import statement 1209 // import {foo} from 'bar'; 1210 // "foo" should both: 1211 // - "ref/imports" the remote symbol 1212 // - "defines/binding" the local symbol 1213 1214 const localSym = this.host.getSymbolAtLocation(name); 1215 if (!localSym) { 1216 throw new Error(`TODO: local name ${name} has no symbol`); 1217 } 1218 const remoteSym = this.typeChecker.getAliasedSymbol(localSym); 1219 1220 // The imported symbol can refer to a type, a value, or both. Attempt to 1221 // define local imports and reference remote definitions in both cases. 1222 if (remoteSym.flags & ts.SymbolFlags.Value) { 1223 const kRemoteValue = 1224 this.host.getSymbolName(remoteSym, TSNamespace.VALUE); 1225 const kLocalValue = this.host.getSymbolName(localSym, TSNamespace.VALUE); 1226 if (!kRemoteValue || !kLocalValue) return; 1227 1228 if (bindingAnchor) { 1229 // The local import value is a "variable" with an "import" subkind, and 1230 // aliases its remote definition. 1231 this.emitNode(kLocalValue, NodeKind.VARIABLE); 1232 this.emitFact(kLocalValue, FactName.SUBKIND, Subkind.IMPORT); 1233 this.emitEdge(kLocalValue, EdgeKind.ALIASES, kRemoteValue); 1234 this.emitEdge(bindingAnchor, EdgeKind.DEFINES_BINDING, kLocalValue); 1235 } 1236 // Emit edges from the referencing anchor to the import's remote definition. 1237 this.emitEdge(refAnchor, EdgeKind.REF_IMPORTS, kRemoteValue); 1238 } 1239 if (remoteSym.flags & ts.SymbolFlags.Type) { 1240 const kRemoteType = this.host.getSymbolName(remoteSym, TSNamespace.TYPE); 1241 const kLocalType = this.host.getSymbolName(localSym, TSNamespace.TYPE); 1242 if (!kRemoteType || !kLocalType) return; 1243 1244 if (bindingAnchor) { 1245 // The local import value is a "talias" (type alias) with an "import" 1246 // subkind, and aliases its remote definition. 1247 this.emitNode(kLocalType, NodeKind.TALIAS); 1248 this.emitFact(kLocalType, FactName.SUBKIND, Subkind.IMPORT); 1249 this.emitEdge(kLocalType, EdgeKind.ALIASES, kRemoteType); 1250 this.emitEdge(bindingAnchor, EdgeKind.DEFINES_BINDING, kLocalType); 1251 } 1252 // Emit edges from the referencing anchor to the import's remote definition. 1253 this.emitEdge(refAnchor, EdgeKind.REF_IMPORTS, kRemoteType); 1254 } 1255 } 1256 1257 /** 1258 * Emits `ref/import` edge from a module name to its definition. 1259 * It is used by static and dynamic imports: 1260 * 1261 * import {Bar} from './foo'; // static 1262 * 1263 * const {Bar} = await import('./foo'); // dynamic 1264 * 1265 * @param moduleRef Module reference. E.g. './foo' from the examples above. 1266 * @returns true if corresponding module was found and an edge was emitted. 1267 */ 1268 emitRefToImportedModule(moduleRef: ts.Node): boolean { 1269 const moduleSym = this.host.getSymbolAtLocation(moduleRef); 1270 if (!moduleSym) { 1271 // This can occur when the module failed to resolve to anything. 1272 // See testdata/import_missing.ts for more on how that could happen. 1273 return false; 1274 } 1275 const modulePath = this.getModulePathFromModuleReference(moduleSym); 1276 if (modulePath) { 1277 const kModule = this.newVName('module', modulePath); 1278 this.emitEdge(this.newAnchor(moduleRef), EdgeKind.REF_IMPORTS, kModule); 1279 } else { 1280 // Check if module being imported is declared via `declare module` 1281 // and if so - output ref to that statement. 1282 const decl = moduleSym.valueDeclaration; 1283 if (decl && ts.isModuleDeclaration(decl)) { 1284 const kModule = 1285 this.host.getSymbolName(moduleSym, TSNamespace.NAMESPACE); 1286 if (!kModule) return false; 1287 this.emitEdge(this.newAnchor(moduleRef), EdgeKind.REF_IMPORTS, kModule); 1288 } 1289 } 1290 return true; 1291 } 1292 1293 /** visitImportDeclaration handles the various forms of "import ...". */ 1294 visitImportDeclaration(decl: ts.ImportDeclaration| 1295 ts.ImportEqualsDeclaration) { 1296 // All varieties of import statements reference a module on the right, 1297 // so start by linking that. 1298 let moduleRef; 1299 if (ts.isImportDeclaration(decl)) { 1300 // This is a regular import declaration 1301 // import ... from ...; 1302 // where the module name is moduleSpecifier. 1303 moduleRef = decl.moduleSpecifier; 1304 } else { 1305 // This is an import equals declaration, which has two cases: 1306 // import foo = require('./bar'); 1307 // import foo = M.bar; 1308 // In the first case the moduleReference is an ExternalModuleReference 1309 // whose module name is the expression inside the `require` call. 1310 // In the second case the moduleReference is the module name. 1311 moduleRef = ts.isExternalModuleReference(decl.moduleReference) ? 1312 decl.moduleReference.expression : 1313 decl.moduleReference; 1314 } 1315 const moduleFound = this.emitRefToImportedModule(moduleRef); 1316 if (!moduleFound) { 1317 return; 1318 } 1319 1320 // TODO(#4021): See discussion. 1321 // Pending changes, an anchor in a Code Search UI cannot currently be 1322 // displayed as a node definition and as referencing other nodes. Instead, 1323 // for non-renamed imports the local node definition is placed on the 1324 // "import" text: 1325 // //- @foo ref BarFoo 1326 // //- @import defines/binding LocalFoo 1327 // //- LocalFoo aliases BarFoo 1328 // import {foo} from 'bar'; 1329 // For renamed imports the definition and references are separated as 1330 // expected: 1331 // //- @foo ref BarFoo 1332 // //- @baz defines/binding LocalBaz 1333 // //- @baz aliases BarFoo 1334 // import {foo as baz} from 'bar'; 1335 // 1336 // Create an anchor for the import text. 1337 const importTextSpan = this.getTextSpan(decl, 'import'); 1338 const importTextAnchor = 1339 this.newAnchor(decl, importTextSpan.start, importTextSpan.end); 1340 1341 if (ts.isImportEqualsDeclaration(decl)) { 1342 // This is an equals import, e.g.: 1343 // import foo = require('./bar'); 1344 // 1345 // TODO(#4021): Bind the local definition and reference the remote 1346 // definition on the import name. 1347 const refAnchor = this.newAnchor(decl.name); 1348 this.visitImport(decl.name, /* bindingAnchor= */ null, refAnchor); 1349 return; 1350 } 1351 1352 if (!decl.importClause) { 1353 // This is a side-effecting import that doesn't declare anything, e.g.: 1354 // import 'foo'; 1355 return; 1356 } 1357 const clause = decl.importClause; 1358 1359 if (clause.name) { 1360 // This is a default import, e.g.: 1361 // import foo from './bar'; 1362 // 1363 // TODO(#4021): Bind the local definition and reference the remote 1364 // definition on the import name. 1365 const refAnchor = this.newAnchor(clause.name); 1366 this.visitImport(clause.name, /* bindingAnchor= */ null, refAnchor); 1367 return; 1368 } 1369 1370 if (!clause.namedBindings) { 1371 // TODO: I believe clause.name or clause.namedBindings are always present, 1372 // which means this check is not necessary, but the types don't show that. 1373 throw new Error(`import declaration ${decl.getText()} has no bindings`); 1374 } 1375 switch (clause.namedBindings.kind) { 1376 case ts.SyntaxKind.NamespaceImport: 1377 // This is a namespace import, e.g.: 1378 // import * as foo from 'foo'; 1379 const name = clause.namedBindings.name; 1380 const sym = this.host.getSymbolAtLocation(name); 1381 if (!sym) { 1382 todo( 1383 this.sourceRoot, clause, 1384 `namespace import ${clause.getText()} has no symbol`); 1385 return; 1386 } 1387 const kModuleObject = this.host.getSymbolName(sym, TSNamespace.VALUE); 1388 if (!kModuleObject) return; 1389 this.emitNode(kModuleObject, NodeKind.VARIABLE); 1390 this.emitEdge( 1391 this.newAnchor(name), EdgeKind.DEFINES_BINDING, kModuleObject); 1392 break; 1393 case ts.SyntaxKind.NamedImports: 1394 // This is named imports, e.g.: 1395 // import {bar, baz} from 'foo'; 1396 const imports = clause.namedBindings.elements; 1397 for (const imp of imports) { 1398 // If the named import has a property name, e.g. `bar` in 1399 // import {bar as baz} from 'foo'; 1400 // bind the local definition on the import name "baz" and reference 1401 // the remote definition on the property name "bar". Otherwise, bind 1402 // the local definition on "import" and reference the remote 1403 // definition on the import name. 1404 // 1405 // TODO(#4021): Unify binding and reference anchors. 1406 let bindingAnchor, refAnchor; 1407 if (imp.propertyName) { 1408 bindingAnchor = this.newAnchor(imp.name); 1409 refAnchor = this.newAnchor(imp.propertyName); 1410 } else { 1411 refAnchor = this.newAnchor(imp.name); 1412 bindingAnchor = null; 1413 } 1414 this.visitImport(imp.name, bindingAnchor, refAnchor); 1415 } 1416 break; 1417 } 1418 } 1419 1420 /** 1421 * Handles dynamic imports like: 1422 * 1423 * const {SomeSym} = await import('./another/file'); 1424 * 1425 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import 1426 * 1427 * @param node import() statement. TS represents dynamic imports as CallExpressions. 1428 */ 1429 visitDynamicImportCall(node: ts.CallExpression) { 1430 const moduleRef = node.arguments[0]; 1431 this.emitRefToImportedModule(moduleRef); 1432 } 1433 1434 /** 1435 * When a file imports another file, with syntax like 1436 * import * as x from 'some/path'; 1437 * we wants 'some/path' to refer to a VName that just means "the entire 1438 * file". It doesn't refer to any text in particular, so we just mark 1439 * the first letter in the file as the anchor for this. 1440 */ 1441 emitModuleAnchor(sf: ts.SourceFile) { 1442 const kMod = 1443 this.newVName('module', this.host.moduleName(this.file.fileName)); 1444 this.emitFact(kMod, FactName.NODE_KIND, NodeKind.RECORD); 1445 this.emitEdge(this.kFile, EdgeKind.CHILD_OF, kMod); 1446 1447 // Emit the anchor, bound to the beginning of the file. 1448 const anchor = this.newAnchor(this.file, 0, 0); 1449 this.emitEdge(anchor, EdgeKind.DEFINES_IMPLICIT, kMod); 1450 } 1451 1452 /** 1453 * Emits an implicit property for a getter or setter. 1454 * For instance, a getter/setter `foo` in class `A` will emit an implicit 1455 * property on that class with signature `A.foo`, and create "property/reads" 1456 * and "property/writes" from the getters/setters to the implicit property. 1457 */ 1458 emitImplicitProperty( 1459 decl: ts.GetAccessorDeclaration|ts.SetAccessorDeclaration, anchor: VName, 1460 funcVName: VName) { 1461 // Remove trailing ":getter"/":setter" suffix 1462 const propSignature = funcVName.signature.split(':').slice(0, -1).join(':'); 1463 const implicitProp = {...funcVName, signature: propSignature}; 1464 1465 this.emitNode(implicitProp, NodeKind.VARIABLE); 1466 this.emitSubkind(implicitProp, Subkind.IMPLICIT); 1467 this.emitEdge(anchor, EdgeKind.DEFINES_BINDING, implicitProp); 1468 1469 const sym = this.host.getSymbolAtLocation(decl.name); 1470 if (!sym || !sym.declarations) { 1471 throw new Error('Getter/setter declaration has no symbols.'); 1472 } 1473 1474 if (sym.declarations.find(ts.isGetAccessor)) { 1475 // Emit a "property/reads" edge between the getter and the property 1476 const getter = 1477 this.host.getSymbolName(sym, TSNamespace.VALUE, Context.Getter); 1478 if (!getter) return; 1479 this.emitEdge(getter, EdgeKind.PROPERTY_READS, implicitProp); 1480 } 1481 if (sym.declarations.find(ts.isSetAccessor)) { 1482 // Emit a "property/writes" edge between the setter and the property 1483 const setter = 1484 this.host.getSymbolName(sym, TSNamespace.VALUE, Context.Setter); 1485 if (!setter) return; 1486 this.emitEdge(setter, EdgeKind.PROPERTY_WRITES, implicitProp); 1487 } 1488 } 1489 1490 /** 1491 * Handles code like: 1492 * export default ...; 1493 * export = ...; 1494 */ 1495 visitExportAssignment(assign: ts.ExportAssignment) { 1496 if (assign.isExportEquals) { 1497 const span = this.getTextSpan(assign, 'export ='); 1498 const anchor = this.newAnchor(assign, span.start, span.end); 1499 this.emitEdge( 1500 anchor, EdgeKind.DEFINES_BINDING, this.host.scopedSignature(assign)); 1501 } else { 1502 // export default <expr>; 1503 // is the same as exporting the expression under the symbol named 1504 // "default". But we don't have a nice name to link the symbol to! 1505 // So instead we link the keyword "default" itself to the VName. 1506 // The TypeScript AST does not expose the location of the 'default' 1507 // keyword so we just find it in the source text to link it. 1508 const span = this.getTextSpan(assign, 'default'); 1509 const anchor = this.newAnchor(assign, span.start, span.end); 1510 this.emitEdge( 1511 anchor, EdgeKind.DEFINES_BINDING, this.host.scopedSignature(assign)); 1512 } 1513 } 1514 1515 /** 1516 * Handles code that explicitly exports a symbol, like: 1517 * export {Foo} from './bar'; 1518 * 1519 * Note that export can also be a modifier on a declaration, like: 1520 * export class Foo {} 1521 * and that case is handled as part of the ordinary declaration handling. 1522 */ 1523 visitExportDeclaration(decl: ts.ExportDeclaration) { 1524 if (decl.exportClause && ts.isNamedExports(decl.exportClause)) { 1525 for (const exp of decl.exportClause.elements) { 1526 const localSym = this.host.getSymbolAtLocation(exp.name); 1527 if (!localSym) { 1528 console.error(`TODO: export ${exp.name} has no symbol`); 1529 continue; 1530 } 1531 const remoteSym = this.typeChecker.getAliasedSymbol(localSym); 1532 const anchor = this.newAnchor(exp.name); 1533 // Aliased export; propertyName is the 'as <...>' bit. 1534 const propertyAnchor = 1535 exp.propertyName ? this.newAnchor(exp.propertyName) : null; 1536 // Symbol is a value. 1537 if (remoteSym.flags & ts.SymbolFlags.Value) { 1538 const kExport = this.host.getSymbolName(remoteSym, TSNamespace.VALUE); 1539 if (kExport) { 1540 this.emitEdge(anchor, EdgeKind.REF, kExport); 1541 if (propertyAnchor) { 1542 this.emitEdge(propertyAnchor, EdgeKind.REF, kExport); 1543 } 1544 } 1545 } 1546 // Symbol is a type. 1547 if (remoteSym.flags & ts.SymbolFlags.Type) { 1548 const kExport = this.host.getSymbolName(remoteSym, TSNamespace.TYPE); 1549 if (kExport) { 1550 this.emitEdge(anchor, EdgeKind.REF, kExport); 1551 if (propertyAnchor) { 1552 this.emitEdge(propertyAnchor, EdgeKind.REF, kExport); 1553 } 1554 } 1555 } 1556 } 1557 } 1558 if (decl.moduleSpecifier) { 1559 const moduleSym = this.host.getSymbolAtLocation(decl.moduleSpecifier); 1560 if (moduleSym) { 1561 const moduleName = this.getModulePathFromModuleReference(moduleSym); 1562 if (moduleName) { 1563 const kModule = this.newVName('module', moduleName); 1564 this.emitEdge( 1565 this.newAnchor(decl.moduleSpecifier), EdgeKind.REF_IMPORTS, 1566 kModule); 1567 } 1568 } 1569 } 1570 } 1571 1572 visitVariableStatement(stmt: ts.VariableStatement) { 1573 // A VariableStatement contains potentially multiple variable declarations, 1574 // as in: 1575 // var x = 3, y = 4; 1576 // In the (common) case where there's a single variable declared, we look 1577 // for documentation for that variable above the entire statement. 1578 if (stmt.declarationList.declarations.length === 1) { 1579 const vname = 1580 this.visitVariableDeclaration(stmt.declarationList.declarations[0]); 1581 if (vname) this.visitJSDoc(stmt, vname); 1582 return; 1583 } 1584 1585 // Otherwise, use default recursion over the statement. 1586 ts.forEachChild(stmt, n => this.visit(n)); 1587 } 1588 1589 /** 1590 * Note: visitVariableDeclaration is also used for class properties; 1591 * the decl parameter is the union of the attributes of the two types. 1592 * @return the generated VName for the declaration, if any. 1593 */ 1594 visitVariableDeclaration(decl: { 1595 name: ts.BindingName|ts.PropertyName, 1596 type?: ts.TypeNode, 1597 initializer?: ts.Expression, kind: ts.SyntaxKind, 1598 }&ts.Node): VName|undefined { 1599 let vname: VName|undefined; 1600 switch (decl.name.kind) { 1601 case ts.SyntaxKind.Identifier: 1602 case ts.SyntaxKind.PrivateIdentifier: 1603 case ts.SyntaxKind.StringLiteral: 1604 case ts.SyntaxKind.NumericLiteral: 1605 const sym = this.host.getSymbolAtLocation(decl.name); 1606 if (!sym) { 1607 todo( 1608 this.sourceRoot, decl.name, 1609 `declaration ${decl.name.getText()} has no symbol`); 1610 return undefined; 1611 } 1612 vname = this.host.getSymbolName(sym, TSNamespace.VALUE); 1613 if (vname) { 1614 this.emitNode(vname, NodeKind.VARIABLE); 1615 this.emitEdge( 1616 this.newAnchor(decl.name), EdgeKind.DEFINES_BINDING, vname); 1617 } 1618 1619 decl.name.forEachChild(child => this.visit(child)); 1620 break; 1621 case ts.SyntaxKind.ObjectBindingPattern: 1622 case ts.SyntaxKind.ArrayBindingPattern: 1623 for (const element of (decl.name as ts.BindingPattern).elements) { 1624 this.visit(element); 1625 } 1626 // Handle case like `const {name} = p;` where p is or type Person. 1627 // This will add ref from the `name` variable to Person.name. 1628 if (ts.isObjectBindingPattern(decl.name)) { 1629 this.connectObjectBindingPatternToType(decl.name); 1630 } 1631 break; 1632 case ts.SyntaxKind.ComputedPropertyName: 1633 decl.name.forEachChild(child => this.visit(child)); 1634 break; 1635 default: 1636 break; 1637 } 1638 1639 if (decl.type) this.visitType(decl.type); 1640 if (decl.initializer) this.visit(decl.initializer); 1641 if (decl.type && decl.initializer && 1642 ts.isObjectLiteralExpression(decl.initializer)) { 1643 this.connectObjectLiteralToType(decl.initializer, decl.type); 1644 } 1645 if (!vname) { 1646 return undefined; 1647 } 1648 1649 if (ts.isVariableDeclaration(decl) || ts.isPropertyAssignment(decl) || 1650 ts.isPropertyDeclaration(decl) || ts.isBindingElement(decl) || 1651 ts.isShorthandPropertyAssignment(decl) || 1652 ts.isPropertySignature(decl) || ts.isJsxAttribute(decl)) { 1653 this.emitMarkedSourceForVariable(decl, vname); 1654 } else { 1655 todo(this.sourceRoot, decl, 'Emit variable delaration code'); 1656 } 1657 1658 if (ts.isPropertyDeclaration(decl)) { 1659 const declNode = decl as ts.PropertyDeclaration; 1660 if (isStaticMember(declNode, declNode.parent)) { 1661 this.emitFact(vname, FactName.TAG_STATIC, ''); 1662 } 1663 } 1664 if (ts.isPropertySignature(decl) || 1665 ts.isPropertyDeclaration(decl) || 1666 ts.isPropertyAssignment(decl) || 1667 ts.isShorthandPropertyAssignment(decl)) { 1668 this.emitSubkind(vname, Subkind.FIELD); 1669 this.emitChildofEdge(vname, decl.parent); 1670 } 1671 if (ts.isShorthandPropertyAssignment(decl)) { 1672 const origSym = this.typeChecker.getShorthandAssignmentValueSymbol(decl); 1673 if (origSym) { 1674 const origVName = this.host.getSymbolName(origSym, TSNamespace.VALUE); 1675 if (origVName) { 1676 this.emitEdge(this.newAnchor(decl.name), EdgeKind.REF_ID, origVName); 1677 } 1678 } 1679 } 1680 return vname; 1681 } 1682 1683 getIdentifierForMarkedSourceNode(node: ts.Node): string { 1684 if (ts.isConstructorDeclaration(node)) { 1685 return 'constructor'; 1686 } 1687 if ('name' in node) { 1688 return (node as ts.DeclarationStatement).name?.getText() ?? 'anonymous'; 1689 } 1690 return 'anonymous'; 1691 } 1692 1693 collectContextPartsForMarkedSource(node: ts.Node): string[] { 1694 switch (node.kind) { 1695 case ts.SyntaxKind.PropertyDeclaration: 1696 case ts.SyntaxKind.PropertySignature: 1697 case ts.SyntaxKind.EnumMember: 1698 case ts.SyntaxKind.MethodDeclaration: 1699 case ts.SyntaxKind.MethodSignature: 1700 case ts.SyntaxKind.Constructor: 1701 // Parent is a class/interface/enum. Use their name as context. 1702 return [this.getIdentifierForMarkedSourceNode(node.parent)]; 1703 case ts.SyntaxKind.Parameter: 1704 const method = node.parent; 1705 if (!ts.isMethodSignature(method) 1706 && !ts.isMethodDeclaration(method) 1707 && !ts.isConstructorDeclaration(method) 1708 && !ts.isFunctionDeclaration(method)) { 1709 // We expect that parent of parameter is always a method. If it is not 1710 // then return undefined so no context will be emitted. 1711 return []; 1712 } 1713 // If method has parent (class) then add it to context. So it looks like 1714 // 'ClassName.methodName'. Otherwise if it's a standalone function - just 1715 // return function name. 1716 const methodContext = this.collectContextPartsForMarkedSource(method); 1717 methodContext.push(this.getIdentifierForMarkedSourceNode(method)); 1718 return methodContext; 1719 } 1720 // For all other nodes like variables, functions, classes don't use context 1721 // for now. Other languages use namespace or filename as context for those but 1722 // it doesn't provide much information. 1723 return []; 1724 } 1725 1726 /** 1727 * This function builds CONTEXT node for marked source for a node. Context is what 1728 * the given node belongs to. For example for class method context is the name of the class. 1729 * 1730 * When the provided node doesn't have a context, e.g. variable, then this method returns null. 1731 * 1732 * Compared to other languages (Java, Go) context calculation here is simpler. We 1733 * don't produce fully qualified class name including package (as TS doesn't have a concept of 1734 * qualified name). Though we could add filename as namespace in future. 1735 * 1736 * We also don't handle nesting. In TS one can have class within a method within a class 1737 * within a method. It is possible to fully calculate such name (similar to what we do in 1738 * scopedSignature) but given that it's user-visible string - keep it simple. 1739 * Even though it's incomplete. 1740 */ 1741 buildMarkedSourceContextNode(node: ts.Node): JSONMarkedSource|null { 1742 const parts = this.collectContextPartsForMarkedSource(node); 1743 if (parts.length === 0) { 1744 return null; 1745 } else { 1746 return { 1747 kind: MarkedSourceKind.CONTEXT, 1748 post_child_text: '.', 1749 add_final_list_token: true, 1750 child: parts.map(c => ({kind: MarkedSourceKind.IDENTIFIER, pre_text: c})), 1751 }; 1752 } 1753 } 1754 1755 /** 1756 * Emits a code fact for a variable or property declaration, specifying how 1757 * the declaration should be presented to users. 1758 * 1759 * The form of the code fact is 1760 * ((property)|(local var)|const|let) <name>: <type>( = <initializer>)? 1761 * where `(local var)` is the declaration of a variable in a catch clause. 1762 */ 1763 emitMarkedSourceForVariable( 1764 decl: ts.VariableDeclaration|ts.PropertyAssignment| 1765 ts.PropertyDeclaration|ts.BindingElement|ts.ShorthandPropertyAssignment| 1766 ts.PropertySignature|ts.JsxAttribute|ts.ParameterDeclaration|ts.EnumMember, 1767 declVName: VName) { 1768 const codeParts: JSONMarkedSource[] = []; 1769 let varDecl; 1770 const bindingPath: Array<string|number|undefined> = []; 1771 if (ts.isBindingElement(decl)) { 1772 // The node we want to emit code for is a BindingElement. This parent of 1773 // this is always a BindingPattern; the parent of the BindingPattern is 1774 // another BindingPattern, a ParameterDeclaration, or VariableDeclaration. 1775 // We handle ParameterDeclarations in `visitParameters`, so here we only 1776 // care about the declaration from a VariableDeclaration. 1777 bindingPath.push(this.bindingElemIndex(decl)); 1778 varDecl = decl.parent.parent; 1779 while (!ts.isVariableDeclaration(varDecl) && varDecl !== undefined) { 1780 if (ts.isParameter(varDecl)) return; 1781 bindingPath.push(this.bindingElemIndex(varDecl)); 1782 varDecl = varDecl.parent.parent; 1783 } 1784 if (varDecl === undefined) { 1785 todo( 1786 this.sourceRoot, decl, 1787 `Does not have variable and parameter declaration.`); 1788 } 1789 } else { 1790 varDecl = decl; 1791 } 1792 const context = this.buildMarkedSourceContextNode(decl); 1793 if (context != null) { 1794 codeParts.push(context); 1795 } 1796 if (ts.isParameter(varDecl) && varDecl.dotDotDotToken) { 1797 codeParts.push({ 1798 // TODO: this should probably be TYPE but the current renderer adds an 1799 // unnecessary space after TYPEs 1800 kind: MarkedSourceKind.MODIFIER, 1801 pre_text: '...', 1802 }); 1803 } 1804 codeParts.push({ 1805 kind: MarkedSourceKind.IDENTIFIER, 1806 pre_text: fmtMarkedSource(this.getIdentifierForMarkedSourceNode(decl)), 1807 }); 1808 if (!ts.isEnumMember(varDecl)) { 1809 const ty = this.typeChecker.getTypeAtLocation(decl); 1810 const tyStr = this.typeChecker.typeToString(ty, decl); 1811 codeParts.push( 1812 {kind: MarkedSourceKind.TYPE, pre_text: ': ', post_text: fmtMarkedSource(tyStr)}); 1813 1814 } 1815 if ('initializer' in varDecl && varDecl.initializer) { 1816 let init: ts.Node = varDecl.initializer; 1817 1818 if (ts.isObjectLiteralExpression(init) || 1819 ts.isArrayLiteralExpression(init)) { 1820 const narrowedInit = isNonNullableArray(bindingPath) && 1821 this.walkObjectLikeLiteral(init, bindingPath.reverse()); 1822 init = narrowedInit || init; 1823 } 1824 1825 codeParts.push( 1826 {kind: MarkedSourceKind.INITIALIZER, pre_text: fmtMarkedSource(init.getText())}); 1827 } 1828 1829 const markedSource = {kind: MarkedSourceKind.BOX, child: codeParts}; 1830 this.emitFact(declVName, FactName.CODE_JSON, JSON.stringify(markedSource)); 1831 } 1832 1833 /** 1834 * Emits a code fact for a class specifying how the declaration should be presented to users. 1835 */ 1836 emitMarkedSourceForClasslikeDeclaration( 1837 decl: ts.ClassLikeDeclaration|ts.InterfaceDeclaration|ts.EnumDeclaration, declVName: VName) { 1838 const markedSource: JSONMarkedSource = 1839 {kind: MarkedSourceKind.IDENTIFIER, pre_text: this.getIdentifierForMarkedSourceNode(decl)}; 1840 this.emitFact(declVName, FactName.CODE_JSON, JSON.stringify(markedSource)); 1841 } 1842 1843 /** 1844 * Emits a code fact for a function specifying how the declaration should be presented to users. 1845 */ 1846 emitMarkedSourceForFunction(decl: ts.FunctionLikeDeclaration, declVName: VName) { 1847 const codeParts: JSONMarkedSource[] = []; 1848 const context = this.buildMarkedSourceContextNode(decl); 1849 if (context != null) { 1850 codeParts.push(context); 1851 } 1852 codeParts.push({ 1853 kind: MarkedSourceKind.IDENTIFIER, 1854 pre_text: this.getIdentifierForMarkedSourceNode(decl), 1855 }); 1856 codeParts.push({ 1857 kind: MarkedSourceKind.PARAMETER_LOOKUP_BY_PARAM, 1858 pre_text: '(', 1859 post_child_text: ', ', 1860 post_text: ')' 1861 }); 1862 const signature = this.typeChecker.getTypeAtLocation(decl).getCallSignatures()[0]; 1863 if (signature) { 1864 const returnType = signature.getReturnType(); 1865 const returnTypeStr = this.typeChecker.typeToString(returnType, decl); 1866 codeParts.push({ 1867 kind: MarkedSourceKind.TYPE, 1868 pre_text: ': ', 1869 post_text: fmtMarkedSource(returnTypeStr), 1870 }); 1871 } 1872 const markedSource = {kind: MarkedSourceKind.BOX, child: codeParts}; 1873 this.emitFact(declVName, FactName.CODE_JSON, JSON.stringify(markedSource)); 1874 } 1875 1876 /** 1877 * Given a path of properties, walks the properties/elements of an 1878 * object/array literal, yielding the final node along the path. 1879 * 1880 * If the path or object literal is malformed (i.e. the property does not 1881 * exist or the object to walk is not a literal), nothing is returned. 1882 */ 1883 walkObjectLikeLiteral( 1884 objLiteral: ts.ArrayLiteralExpression|ts.ObjectLiteralExpression, 1885 path: Array<string|number>, 1886 ): ts.Node|undefined { 1887 let node: ts.Node = objLiteral; 1888 for (const prop of path) { 1889 let next: ts.Node|undefined; 1890 if (ts.isObjectLiteralExpression(node)) { 1891 // The property name node text is the "index" of the property. See 1892 // `bindingElemIndex` for more details. 1893 next = node.properties.find( 1894 p => p.name && this.getPropertyNameStr(p.name) === prop); 1895 if (next && ts.isPropertyAssignment(next)) { 1896 next = next.initializer; 1897 } 1898 } else if ( 1899 ts.isArrayLiteralExpression(node) && typeof prop === 'number') { 1900 next = (node as ts.ArrayLiteralExpression).elements[prop]; 1901 } 1902 if (!next) { 1903 todo( 1904 this.sourceRoot, node, 1905 `expected to be an object-like literal with property '${prop}'`) 1906 return; 1907 } 1908 node = next; 1909 } 1910 return node; 1911 } 1912 1913 /** 1914 * Given an object literal that is used in a place where given type is 1915 * expected - adds refs from the literals properties to the type's properties. 1916 */ 1917 connectObjectLiteralToType( 1918 literal: ts.ObjectLiteralExpression, typeNode: ts.TypeNode|undefined) { 1919 if (typeNode == null) return; 1920 const type = this.typeChecker.getTypeFromTypeNode(typeNode); 1921 for (const prop of literal.properties) { 1922 if (ts.isPropertyAssignment(prop) || 1923 ts.isShorthandPropertyAssignment(prop) || 1924 ts.isMethodDeclaration(prop)) { 1925 this.emitPropertyRef(prop.name, type); 1926 } 1927 } 1928 } 1929 1930 /** 1931 * Given object binding pattern like `const {name} = getPerson();` tries to 1932 * add refs from binding variables to the propeties of the type. Like `name` 1933 * is connected to `Person.name`. 1934 */ 1935 connectObjectBindingPatternToType(binding: ts.ObjectBindingPattern) { 1936 const type = this.typeChecker.getTypeAtLocation(binding); 1937 for (const prop of binding.elements) { 1938 if (prop.propertyName) { 1939 this.emitPropertyRef(prop.propertyName, type); 1940 } else if (ts.isIdentifier(prop.name)) { 1941 this.emitPropertyRef(prop.name, type); 1942 } 1943 } 1944 } 1945 1946 /** 1947 * Helper function to emit `ref` node from a given property (usually of object 1948 * literal or destructuring pattern) to the type. Checks whether provided type 1949 * contains a property with the same name and will use it for `ref` node. 1950 */ 1951 emitPropertyRef(prop: ts.PropertyName, type: ts.Type) { 1952 const propName = this.getPropertyNameStr(prop); 1953 if (propName == null) return; 1954 const propertyOnType = type.getProperty(propName); 1955 if (propertyOnType == null) return; 1956 const propFlags = propertyOnType.flags; 1957 const isType = (propFlags & 1958 (ts.SymbolFlags.Class | ts.SymbolFlags.Interface | 1959 ts.SymbolFlags.RegularEnum | ts.SymbolFlags.TypeAlias)) > 0; 1960 const vname = this.host.getSymbolName( 1961 propertyOnType, isType ? TSNamespace.TYPE : TSNamespace.VALUE); 1962 if (vname == null) return; 1963 const anchor = this.newAnchor(prop); 1964 this.emitEdge(anchor, EdgeKind.REF_ID, vname); 1965 } 1966 1967 /** 1968 * Returns the property "index" of a bound element in a binding pattern, if 1969 * known. For example, 1970 * - `1` has index `2` in the binding pattern `[3, 2, 1]` 1971 * - `c` has index `c` in the binding pattern `{a, b, c}` 1972 * - `calias` has index `c` in the binding pattern `{a, b, c: calias}` 1973 */ 1974 bindingElemIndex(elem: ts.BindingElement): string|number|undefined { 1975 const bindingPat = elem.parent; 1976 if (ts.isObjectBindingPattern(bindingPat)) { 1977 if (elem.propertyName) { 1978 return this.getPropertyNameStr(elem.propertyName); 1979 } 1980 switch (elem.name.kind) { 1981 case ts.SyntaxKind.Identifier: 1982 return elem.name.text; 1983 case ts.SyntaxKind.ArrayBindingPattern: 1984 case ts.SyntaxKind.ObjectBindingPattern: 1985 return undefined; 1986 } 1987 } else { 1988 return bindingPat.elements.indexOf(elem); 1989 } 1990 } 1991 1992 /** 1993 * Returns the string content of a property name, if known. 1994 * The name of complex computed properties is often not known. 1995 */ 1996 getPropertyNameStr(elem: ts.PropertyName): string|undefined { 1997 switch (elem.kind) { 1998 case ts.SyntaxKind.Identifier: 1999 case ts.SyntaxKind.StringLiteral: 2000 case ts.SyntaxKind.NumericLiteral: 2001 case ts.SyntaxKind.PrivateIdentifier: 2002 case ts.SyntaxKind.NoSubstitutionTemplateLiteral: 2003 return elem.text; 2004 case ts.SyntaxKind.ComputedPropertyName: 2005 const name = this.host.getSymbolAtLocation(elem)?.name; 2006 // If the computed property expression is more complicated than an 2007 // identifier (e.g. `['red' + 'cat']` or `[fn()]`), the name isn't 2008 // resolved and the symbol name is marked as "__computed". This doesn't 2009 // help us for indexing, so return "undefined" in such cases. 2010 // Constant evaluation of the computed property name is not always 2011 // possible (e.g. the expression may be a function call), and usage of 2012 // computed properties is probably rare enough that handling the "simple 2013 // case" is good enough for now. 2014 return name !== '__computed' ? name : undefined; 2015 } 2016 } 2017 2018 /** 2019 * Emit "overrides" edges if this method overrides extended classes or 2020 * implemented interfaces, which are listed in the Heritage Clauses of 2021 * a class or interface. 2022 * class X extends A implements B, C {} 2023 * ^^^^^^^^^-^^^^^^^^^^^^^^^----- `HeritageClause`s 2024 * Look at each type listed in the heritage clauses and traverse its 2025 * members. If the type has a member that matches the method visited in 2026 * this function (`kFunc`), emit an "overrides" edge to that member. 2027 */ 2028 emitOverridesEdgeForFunction( 2029 funcSym: ts.Symbol, funcVName: VName, 2030 parent: ts.ClassLikeDeclaration|ts.InterfaceDeclaration) { 2031 if (parent.heritageClauses == null) { 2032 return; 2033 } 2034 for (const heritage of parent.heritageClauses) { 2035 for (const baseType of heritage.types) { 2036 const type = this.typeChecker.getTypeAtLocation(baseType.expression); 2037 if (!type || !type.symbol || !type.symbol.members) { 2038 continue; 2039 } 2040 2041 const funcName = funcSym.name; 2042 const funcFlags = funcSym.flags; 2043 2044 // Find a member of with the same type (same flags) and same name 2045 // as the overriding method. 2046 const overriddenCondition = (sym: ts.Symbol) => 2047 Boolean(sym.flags & funcFlags) && sym.name === funcName; 2048 2049 const overridden = toArray<ts.Symbol>(type.symbol.members.values()) 2050 .find(overriddenCondition); 2051 if (overridden) { 2052 const base = this.host.getSymbolName(overridden, TSNamespace.VALUE); 2053 if (base) { 2054 this.emitEdge(funcVName, EdgeKind.OVERRIDES, base); 2055 } 2056 } else { 2057 // If parent class or interface doesn't have this method - it's possible 2058 // that parent's parent might. To check for that recurse to the parent's parent 2059 // classes/interfaces. 2060 const decl = type.symbol.declarations?.[0]; 2061 if (decl && (ts.isClassLike(decl) || ts.isInterfaceDeclaration(decl))) { 2062 this.emitOverridesEdgeForFunction(funcSym, funcVName, decl); 2063 } 2064 } 2065 } 2066 } 2067 } 2068 2069 visitFunctionLikeDeclaration(decl: ts.FunctionLikeDeclaration) { 2070 this.visitDecorators(ts.canHaveDecorators(decl) ? ts.getDecorators(decl) : []); 2071 const {sym, vname} = this.getSymbolAndVNameForFunctionDeclaration(decl); 2072 if (!vname) { 2073 todo( 2074 this.sourceRoot, decl, 2075 `function declaration ${decl.getText()} has no symbol`); 2076 return; 2077 } 2078 const isNameComputedProperty = 2079 decl.name && decl.name.kind === ts.SyntaxKind.ComputedPropertyName; 2080 if (isNameComputedProperty) { 2081 this.visit((decl.name as ts.ComputedPropertyName).expression); 2082 } 2083 2084 // Treat functions with computed names as anonymous. From developer point of 2085 // view in the following expression: `const k = {[foo]() {}};` the part 2086 // `[foo]` isn't a separate symbol that you can click. Only `foo` should be 2087 // xref'ed and lead to the `foo` definition. 2088 if (decl.name && !isNameComputedProperty) { 2089 if (!sym) { 2090 todo( 2091 this.sourceRoot, decl.name, 2092 `function declaration ${decl.name.getText()} has no symbol`); 2093 return; 2094 } 2095 2096 const declAnchor = this.newAnchor(decl.name); 2097 this.emitNode(vname, NodeKind.FUNCTION); 2098 this.emitEdge(declAnchor, EdgeKind.DEFINES_BINDING, vname); 2099 2100 // Getters/setters also emit an implicit class property entry. If a 2101 // getter is present, it will bind this entry; otherwise a setter will. 2102 if (ts.isGetAccessor(decl) || 2103 (ts.isSetAccessor(decl) && 2104 (!sym.declarations || !sym.declarations.find(ts.isGetAccessor)))) { 2105 this.emitImplicitProperty(decl, declAnchor, vname); 2106 } 2107 2108 this.visitJSDoc(decl, vname, false); 2109 } 2110 this.emitEdge(this.newAnchor(decl), EdgeKind.DEFINES, vname); 2111 2112 if (decl.parent) { 2113 this.emitChildofEdge(vname, decl.parent); 2114 if ((ts.isClassLike(decl.parent) || 2115 ts.isInterfaceDeclaration(decl.parent)) && 2116 sym) { 2117 this.emitOverridesEdgeForFunction(sym, vname, decl.parent); 2118 } 2119 } 2120 2121 this.visitParameters(decl.parameters, vname); 2122 2123 if (decl.type) { 2124 // "type" here is the return type of the function. 2125 this.visitType(decl.type); 2126 } 2127 2128 if (decl.typeParameters) 2129 this.visitTypeParameters(vname, decl.typeParameters); 2130 if (decl.body) { 2131 this.visit(decl.body); 2132 } else { 2133 this.emitFact(vname, FactName.COMPLETE, 'incomplete'); 2134 } 2135 this.emitMarkedSourceForFunction(decl, vname); 2136 } 2137 2138 /** 2139 * Emits childof edge from member to their parents. Parent can be class/interface/enum. 2140 * See https://kythe.io/docs/schema/#childof 2141 */ 2142 emitChildofEdge(vname: VName, parent: ts.Node) { 2143 if (!ts.isClassLike(parent) && !ts.isInterfaceDeclaration(parent) && 2144 !ts.isEnumDeclaration(parent)) { 2145 return; 2146 } 2147 const parentName = parent.name; 2148 if (parentName == null) { 2149 return; 2150 } 2151 const parentSym = this.host.getSymbolAtLocationFollowingAliases(parentName); 2152 if (!parentSym) { 2153 todo(this.sourceRoot, parentName, `parent ${parentName} has no symbol`); 2154 return; 2155 } 2156 const kParent = this.host.getSymbolName(parentSym, TSNamespace.TYPE); 2157 if (kParent) { 2158 this.emitEdge(vname, EdgeKind.CHILD_OF, kParent); 2159 } 2160 } 2161 2162 /** 2163 * Visits a function parameters, which can be recursive in the case of 2164 * parameters created via bound elements: 2165 * function foo({a, b: {c, d}}, e, f) {} 2166 * In this code, a, c, d, e, f are all parameters with increasing parameter 2167 * numbers [0, 4]. 2168 */ 2169 visitParameters( 2170 parameters: ReadonlyArray<ts.ParameterDeclaration>, kFunc: VName) { 2171 let paramNum = 0; 2172 const recurseVisit = 2173 (param: ts.ParameterDeclaration|ts.BindingElement) => { 2174 this.visitDecorators(ts.canHaveDecorators(param) ? ts.getDecorators(param) : []); 2175 2176 switch (param.name.kind) { 2177 case ts.SyntaxKind.Identifier: 2178 const sym = this.host.getSymbolAtLocationFollowingAliases(param.name); 2179 if (!sym) { 2180 todo( 2181 this.sourceRoot, param.name, 2182 `param ${param.name.getText()} has no symbol`); 2183 return; 2184 } 2185 const kParam = this.host.getSymbolName(sym, TSNamespace.VALUE); 2186 if (!kParam) return; 2187 this.emitNode(kParam, NodeKind.VARIABLE); 2188 2189 this.emitEdge( 2190 kFunc, makeOrdinalEdge(EdgeKind.PARAM, paramNum), kParam); 2191 ++paramNum; 2192 2193 if (ts.isParameterPropertyDeclaration(param, param.parent)) { 2194 // Class members defined in the parameters of a constructor are 2195 // children of the class type. 2196 const parentName = param.parent.parent.name; 2197 if (parentName !== undefined) { 2198 const parentSym = this.host.getSymbolAtLocationFollowingAliases(parentName); 2199 if (parentSym !== undefined) { 2200 const kClass = 2201 this.host.getSymbolName(parentSym, TSNamespace.TYPE); 2202 if (!kClass) return; 2203 this.emitEdge(kParam, EdgeKind.CHILD_OF, kClass); 2204 } 2205 } 2206 } else { 2207 this.emitEdge(kParam, EdgeKind.CHILD_OF, kFunc); 2208 } 2209 2210 this.emitEdge( 2211 this.newAnchor(param.name), EdgeKind.DEFINES_BINDING, kParam); 2212 this.emitMarkedSourceForVariable(param, kParam); 2213 break; 2214 case ts.SyntaxKind.ObjectBindingPattern: 2215 case ts.SyntaxKind.ArrayBindingPattern: 2216 const elements = toArray(param.name.elements.entries()); 2217 for (const [index, element] of elements) { 2218 if (ts.isBindingElement(element)) { 2219 recurseVisit(element); 2220 } 2221 } 2222 // Handles case like: 2223 // funtion doSomething({name}: Person) { ... } 2224 // and adds refs from `name` variable to the Person.name field. 2225 if (ts.isObjectBindingPattern(param.name)) { 2226 this.connectObjectBindingPatternToType(param.name); 2227 } 2228 break; 2229 default: 2230 break; 2231 } 2232 2233 if (ts.isParameter(param) && param.type) this.visitType(param.type); 2234 if (param.initializer) this.visit(param.initializer); 2235 } 2236 2237 for (const element of parameters) { 2238 recurseVisit(element); 2239 } 2240 } 2241 2242 visitDecorators(decors: ReadonlyArray<ts.Decorator> | undefined) { 2243 if (decors) { 2244 for (const decor of decors) { 2245 this.visit(decor); 2246 } 2247 } 2248 } 2249 2250 /** 2251 * Visits a module declaration, which can look like any of the following: 2252 * declare module 'foo'; 2253 * declare module 'foo' {} 2254 * declare module foo {} 2255 * namespace Foo {} 2256 */ 2257 visitModuleDeclaration(decl: ts.ModuleDeclaration) { 2258 let sym = this.host.getSymbolAtLocation(decl.name); 2259 if (!sym) { 2260 todo( 2261 this.sourceRoot, decl.name, 2262 `module declaration ${decl.name.getText()} has no symbol`); 2263 return; 2264 } 2265 // A TypeScript module declaration declares both a namespace (a Kythe 2266 // "record") and a value (most similar to a "package", which defines a 2267 // module with declarations). 2268 const kNamespace = this.host.getSymbolName(sym, TSNamespace.NAMESPACE); 2269 const kValue = this.host.getSymbolName(sym, TSNamespace.VALUE); 2270 if (!kNamespace || !kValue) return; 2271 // It's possible that same namespace appears multiple time. We need to 2272 // emit only single node for that namespace and single defines/binding 2273 // edge. 2274 if (sym.valueDeclaration === decl) { 2275 this.emitNode(kNamespace, NodeKind.RECORD); 2276 this.emitSubkind(kNamespace, Subkind.NAMESPACE); 2277 this.emitNode(kValue, NodeKind.PACKAGE); 2278 2279 const nameAnchor = this.newAnchor(decl.name); 2280 this.emitEdge(nameAnchor, EdgeKind.DEFINES_BINDING, kNamespace); 2281 this.emitEdge(nameAnchor, EdgeKind.DEFINES_BINDING, kValue); 2282 // If no body then it is incomplete module definition, like declare module 2283 // 'foo'; 2284 this.emitFact( 2285 kNamespace, FactName.COMPLETE, 2286 decl.body ? 'definition' : 'incomplete'); 2287 } 2288 2289 // The entire module declaration defines the created namespace. 2290 this.emitEdge(this.newAnchor(decl), EdgeKind.DEFINES, kValue); 2291 2292 this.visitDecorators(ts.canHaveDecorators(decl) ? ts.getDecorators(decl) : []); 2293 if (decl.body) this.visit(decl.body); 2294 } 2295 2296 visitClassDeclaration(decl: ts.ClassDeclaration) { 2297 this.visitDecorators(ts.canHaveDecorators(decl) ? ts.getDecorators(decl) : []); 2298 let kClass: VName|undefined; 2299 if (decl.name) { 2300 const sym = this.host.getSymbolAtLocation(decl.name); 2301 if (!sym) { 2302 todo( 2303 this.sourceRoot, decl.name, 2304 `class ${decl.name.getText()} has no symbol`); 2305 return; 2306 } 2307 // A 'class' declaration declares both a type (a 'record', representing 2308 // instances of the class) and a value (least ambigiously, also the 2309 // class declaration). 2310 kClass = this.host.getSymbolName(sym, TSNamespace.TYPE); 2311 if (!kClass) return; 2312 this.emitNode(kClass, NodeKind.RECORD); 2313 const anchor = this.newAnchor(decl.name); 2314 this.emitEdge(anchor, EdgeKind.DEFINES_BINDING, kClass); 2315 2316 // Emit constructor. 2317 const kCtor = this.host.getSymbolName(sym, TSNamespace.VALUE); 2318 if (!kCtor) return; 2319 let ctorAnchor = anchor; 2320 // If the class has an explicit constructor method - use it as an anchor. 2321 const ctorSymbol = this.getCtorSymbol(decl); 2322 if (ctorSymbol && ctorSymbol.declarations) { 2323 const ctorDecl = ctorSymbol.declarations[0]; 2324 const span = this.getTextSpan(ctorDecl, 'constructor'); 2325 ctorAnchor = this.newAnchor(ctorDecl, span.start, span.end); 2326 } 2327 this.emitNode(kCtor, NodeKind.FUNCTION); 2328 this.emitSubkind(kCtor, Subkind.CONSTRUCTOR); 2329 this.emitEdge(ctorAnchor, EdgeKind.DEFINES_BINDING, kCtor); 2330 2331 this.visitJSDoc(decl, kClass); 2332 this.emitMarkedSourceForClasslikeDeclaration(decl, kClass); 2333 } 2334 if (decl.typeParameters) 2335 this.visitTypeParameters(kClass, decl.typeParameters); 2336 if (decl.heritageClauses) this.visitHeritage(kClass, decl.heritageClauses); 2337 for (const member of decl.members) { 2338 this.visit(member); 2339 } 2340 } 2341 2342 visitEnumDeclaration(decl: ts.EnumDeclaration) { 2343 const sym = this.host.getSymbolAtLocation(decl.name); 2344 if (!sym) return; 2345 const kType = this.host.getSymbolName(sym, TSNamespace.TYPE); 2346 if (!kType) return; 2347 this.emitNode(kType, NodeKind.RECORD); 2348 const kValue = this.host.getSymbolName(sym, TSNamespace.VALUE); 2349 if (!kValue) return; 2350 this.emitNode(kValue, NodeKind.CONSTANT); 2351 2352 const anchor = this.newAnchor(decl.name); 2353 this.emitEdge(anchor, EdgeKind.DEFINES_BINDING, kType); 2354 this.emitEdge(anchor, EdgeKind.DEFINES_BINDING, kValue); 2355 for (const member of decl.members) { 2356 this.visit(member); 2357 } 2358 this.emitMarkedSourceForClasslikeDeclaration(decl, kValue); 2359 } 2360 2361 visitEnumMember(decl: ts.EnumMember) { 2362 const sym = this.host.getSymbolAtLocation(decl.name); 2363 if (!sym) return; 2364 const kMember = this.host.getSymbolName(sym, TSNamespace.VALUE); 2365 if (!kMember) return; 2366 this.emitNode(kMember, NodeKind.CONSTANT); 2367 this.emitEdge(this.newAnchor(decl.name), EdgeKind.DEFINES_BINDING, kMember); 2368 this.emitMarkedSourceForVariable(decl, kMember); 2369 this.emitChildofEdge(kMember, decl.parent); 2370 } 2371 2372 visitExpressionMember(node: ts.Node) { 2373 const sym = this.host.getSymbolAtLocationFollowingAliases(node); 2374 if (!sym) { 2375 // E.g. a field of an "any". 2376 return; 2377 } 2378 if (!sym.declarations || sym.declarations.length === 0) { 2379 // An undeclared symbol, e.g. "undefined". 2380 return; 2381 } 2382 const isClass = (sym.flags & ts.SymbolFlags.Class) > 0; 2383 const isConstructorCall = ts.isNewExpression(node.parent); 2384 const ns = isClass && !isConstructorCall ? TSNamespace.TYPE : TSNamespace.VALUE; 2385 const name = this.host.getSymbolName(sym, ns); 2386 if (!name) return; 2387 const anchor = this.newAnchor(node); 2388 2389 const refType = this.getRefType(node, sym); 2390 if (refType == RefType.READ || refType == RefType.READ_WRITE) { 2391 this.emitEdge(anchor, EdgeKind.REF, name); 2392 } 2393 if (refType == RefType.WRITE || refType == RefType.READ_WRITE) { 2394 this.emitEdge(anchor, EdgeKind.REF_WRITES, name); 2395 } 2396 // For classes emit ref/id to the type node in addition to regular 2397 // ref. When user check refs for a class - they usually check get 2398 // refs of the class node, not the constructor node. That's why 2399 // we need ref/id from all usages to the class node. 2400 if (isConstructorCall) { 2401 const className = this.host.getSymbolName(sym, TSNamespace.TYPE); 2402 if (className != null) { 2403 this.emitEdge(anchor, EdgeKind.REF_ID, className); 2404 } 2405 } 2406 this.addInfluencer(name); 2407 } 2408 2409 /** 2410 * Determines the type of reference type of a {@link ts.Node} in its parent 2411 * expression. 2412 * @param node The {@link ts.Node} being referenced 2413 * @param sym The {@link ts.Symbol} associated with the node 2414 * @returns The {@link RefType} indicating whether the reference is READ, 2415 * WRITE, or READ_WRITE 2416 */ 2417 getRefType(node: ts.Node, sym: ts.Symbol): RefType { 2418 // If the identifier being accessed is a property of a class, we need to 2419 // recurse through the parents nodes until we get the true parent 2420 // expression. 2421 let parent = node.parent; 2422 while (ts.isPropertyAccessExpression(parent)) { 2423 parent = parent.parent; 2424 } 2425 2426 if (ts.isPrefixUnaryExpression(parent) || 2427 ts.isPostfixUnaryExpression(parent)) { 2428 let operator: ts.SyntaxKind = parent.operator; 2429 let operand: ts.Expression = parent.operand; 2430 2431 const operandNode = ts.isPropertyAccessExpression(operand) ? 2432 this.propertyAccessIdentifier(operand) as ts.Node : 2433 operand as ts.Node; 2434 2435 // Check if the operand is the same as referenced symbol and if this was 2436 // an increment or decrement operation. If both are true, this is a 2437 // READ_WRITE reference. 2438 if (this.expressionMatchesSymbol(operand, sym) && 2439 operandNode.pos === node.pos) { 2440 if (operator === ts.SyntaxKind.PlusPlusToken || 2441 operator === ts.SyntaxKind.MinusMinusToken) { 2442 return RefType.READ_WRITE; 2443 } 2444 } 2445 } else if (ts.isBinaryExpression(parent)) { 2446 const lhsNode = ts.isPropertyAccessExpression(parent.left) ? 2447 this.propertyAccessIdentifier(parent.left) as ts.Node : 2448 parent.left as ts.Node; 2449 const opString = ts.tokenToString(parent.operatorToken.kind) || ''; 2450 2451 if (this.expressionMatchesSymbol(parent.left, sym) && 2452 lhsNode.pos === node.pos) { 2453 const compoundAssignmentOperators = [ 2454 '+=', '-=', '*=', '**=', '/=', '%=', '<<=', '>>=', '>>>=', '&=', '|=', 2455 '^=' 2456 ]; 2457 if (compoundAssignmentOperators.includes(opString)) { 2458 // Compound assignment operations are always a READ_WRITE reference 2459 return RefType.READ_WRITE; 2460 } else if (opString === '=') { 2461 // If the symbol is on the left side of a assignment operation, it 2462 // is always at least a WRITE reference. However, we then need to 2463 // check if the parent expression is also a binary expression. If so, 2464 // we check whether the current binary expression is on the right 2465 // side of that expression, indicating a chained assignment such as 2466 // x = y = z. If this is the case, the reference becomes READ_WRITE 2467 // instead. 2468 if (parent.parent !== null && ts.isBinaryExpression(parent.parent)) { 2469 if (ts.tokenToString(parent.parent.operatorToken.kind) === '=' && 2470 parent.parent.right === parent) { 2471 return RefType.READ_WRITE; 2472 } 2473 } 2474 return RefType.WRITE; 2475 } 2476 } 2477 } 2478 return RefType.READ; 2479 } 2480 2481 /** 2482 * Check whether a {@link ts.Expression} matches a symbol. If the expression 2483 * is a {@link ts.PropertyAccessExpression}, the check is performed against 2484 * the base property being accessed. 2485 */ 2486 expressionMatchesSymbol(expression: ts.Expression, symbol: ts.Symbol): 2487 boolean { 2488 if (ts.isIdentifier(expression)) { 2489 return this.host.getSymbolAtLocation(expression) === symbol; 2490 } else if (ts.isPropertyAccessExpression(expression)) { 2491 return this.host.getSymbolAtLocation( 2492 this.propertyAccessIdentifier(expression)) === symbol; 2493 } 2494 return false; 2495 } 2496 2497 2498 /** 2499 * Recurses through a {@link ts.PropertyAccessExpression} to find the 2500 * identifier of the base property being accessed. Recursion is necessary 2501 * because a chained property accesses such as `obj.member.member` will 2502 * have two layers of {@link ts.PropertyAccessExpression}s. 2503 * 2504 * @param expression The {@link ts.PropertyAccessExpression} to process 2505 * @returns The identifier of the base property being accessed 2506 */ 2507 propertyAccessIdentifier(expression: ts.PropertyAccessExpression): 2508 ts.Identifier|ts.PrivateIdentifier { 2509 while (ts.isPropertyAccessExpression(expression.parent)) { 2510 expression = expression.parent; 2511 } 2512 return expression.name; 2513 } 2514 2515 /** 2516 * Emits a reference from a "this" keyword to the type of the "this" object. 2517 */ 2518 visitThisKeyword(keyword: ts.ThisExpression) { 2519 const sym = this.host.getSymbolAtLocation(keyword); 2520 if (!sym) { 2521 // "this" refers to an object with no particular type, e.g. 2522 // let obj = { 2523 // foo() { this.foo(); } 2524 // }; 2525 return; 2526 } 2527 if (!sym.declarations || sym.declarations.length === 0) { 2528 // "this" keyword is `globalThis`, which has no declarations. 2529 return; 2530 } 2531 2532 const type = this.host.getSymbolName(sym, TSNamespace.TYPE); 2533 if (!type) return; 2534 const thisAnchor = this.newAnchor(keyword); 2535 this.emitEdge(thisAnchor, EdgeKind.REF, type); 2536 } 2537 2538 /** 2539 * visitJSDoc attempts to attach a 'doc' node to a given target, by looking 2540 * for JSDoc comments. 2541 */ 2542 visitJSDoc(node: ts.Node, target: VName, emitDeprecation: boolean = true) { 2543 if (emitDeprecation) { 2544 this.maybeTagDeprecated(node, target); 2545 } 2546 2547 const text = node.getFullText(); 2548 const comments = ts.getLeadingCommentRanges(text, 0); 2549 if (!comments) return; 2550 2551 let jsdoc: string|undefined; 2552 for (const commentRange of comments) { 2553 if (commentRange.kind !== ts.SyntaxKind.MultiLineCommentTrivia) continue; 2554 const comment = 2555 text.substring(commentRange.pos + 2, commentRange.end - 2); 2556 if (!comment.startsWith('*')) { 2557 // Not a JSDoc comment. 2558 continue; 2559 } 2560 // Strip the ' * ' bits that start lines within the comment. 2561 jsdoc = comment.replace(/^[ \t]*\* ?/mg, ''); 2562 break; 2563 } 2564 if (jsdoc === undefined) return; 2565 2566 // Strip leading and trailing whitespace. 2567 jsdoc = jsdoc.replace(/^\s+/, '').replace(/\s+$/, ''); 2568 const doc = this.newVName(target.signature + '#doc', target.path); 2569 this.emitNode(doc, NodeKind.DOC); 2570 this.emitEdge(doc, EdgeKind.DOCUMENTS, target); 2571 this.emitFact(doc, FactName.TEXT, jsdoc); 2572 } 2573 2574 visitAsExpressions(node: ts.AsExpression) { 2575 ts.forEachChild(node, (child) => { 2576 this.visit(child); 2577 }); 2578 // Handle case like `{name: 'Alice'} as Person` and connect `name` property 2579 // to Person.name. 2580 if (ts.isObjectLiteralExpression(node.expression)) { 2581 this.connectObjectLiteralToType(node.expression, node.type); 2582 } 2583 } 2584 2585 /** 2586 * Tags a node as deprecated if its JSDoc marks it as so. 2587 * TODO(TS 4.0): TS 4.0 exposes a JSDocDeprecatedTag. 2588 */ 2589 maybeTagDeprecated(node: ts.Node, nodeVName: VName) { 2590 const deprecatedTag = 2591 ts.getJSDocTags(node).find(tag => tag.tagName.text === 'deprecated'); 2592 if (deprecatedTag) { 2593 this.emitFact( 2594 nodeVName, FactName.TAG_DEPRECATED, 2595 (deprecatedTag.comment || '') as any); 2596 } 2597 } 2598 2599 /** visit is the main dispatch for visiting AST nodes. */ 2600 visit(node: ts.Node): void { 2601 switch (node.kind) { 2602 case ts.SyntaxKind.ImportDeclaration: 2603 case ts.SyntaxKind.ImportEqualsDeclaration: 2604 return this.visitImportDeclaration( 2605 node as ts.ImportDeclaration | ts.ImportEqualsDeclaration); 2606 case ts.SyntaxKind.ExportAssignment: 2607 return this.visitExportAssignment(node as ts.ExportAssignment); 2608 case ts.SyntaxKind.ExportDeclaration: 2609 return this.visitExportDeclaration(node as ts.ExportDeclaration); 2610 case ts.SyntaxKind.VariableStatement: 2611 return this.visitVariableStatement(node as ts.VariableStatement); 2612 case ts.SyntaxKind.VariableDeclaration: 2613 this.visitVariableDeclaration(node as ts.VariableDeclaration); 2614 return; 2615 case ts.SyntaxKind.PropertyAssignment: // property in object literal 2616 case ts.SyntaxKind.PropertyDeclaration: 2617 case ts.SyntaxKind.PropertySignature: 2618 case ts.SyntaxKind.ShorthandPropertyAssignment: 2619 const vname = 2620 this.visitVariableDeclaration(node as ts.PropertyDeclaration); 2621 if (vname) this.visitJSDoc(node, vname); 2622 return; 2623 case ts.SyntaxKind.ArrowFunction: 2624 case ts.SyntaxKind.Constructor: 2625 case ts.SyntaxKind.FunctionDeclaration: 2626 case ts.SyntaxKind.FunctionExpression: 2627 case ts.SyntaxKind.MethodDeclaration: 2628 case ts.SyntaxKind.MethodSignature: 2629 case ts.SyntaxKind.GetAccessor: 2630 case ts.SyntaxKind.SetAccessor: 2631 return this.visitFunctionLikeDeclaration( 2632 node as ts.FunctionLikeDeclaration); 2633 case ts.SyntaxKind.ClassDeclaration: 2634 return this.visitClassDeclaration(node as ts.ClassDeclaration); 2635 case ts.SyntaxKind.InterfaceDeclaration: 2636 return this.visitInterfaceDeclaration(node as ts.InterfaceDeclaration); 2637 case ts.SyntaxKind.TypeAliasDeclaration: 2638 return this.visitTypeAliasDeclaration(node as ts.TypeAliasDeclaration); 2639 case ts.SyntaxKind.EnumDeclaration: 2640 return this.visitEnumDeclaration(node as ts.EnumDeclaration); 2641 case ts.SyntaxKind.EnumMember: 2642 return this.visitEnumMember(node as ts.EnumMember); 2643 case ts.SyntaxKind.TypeReference: 2644 this.visitType(node as ts.TypeNode); 2645 return; 2646 case ts.SyntaxKind.BindingElement: 2647 this.visitVariableDeclaration(node as ts.BindingElement); 2648 return; 2649 case ts.SyntaxKind.JsxAttribute: 2650 // TODO: go/ts51upgrade - Auto-added to unblock TS5.1 migration. 2651 // TS2345: Argument of type 'JsxAttribute' is not assignable to parameter of type '{ name: ObjectBindingPattern | ArrayBindingPattern | Identifier | StringLiteral | NumericLiteral | ComputedPropertyName | PrivateIdentifier; type?: TypeNode | undefined; initializer?: Expression | undefined; kind: SyntaxKind; } & Node'. 2652 // @ts-ignore 2653 this.visitVariableDeclaration(node as ts.JsxAttribute); 2654 return; 2655 case ts.SyntaxKind.Identifier: 2656 case ts.SyntaxKind.PrivateIdentifier: 2657 case ts.SyntaxKind.StringLiteral: 2658 case ts.SyntaxKind.NumericLiteral: 2659 // Assume that this identifer is occurring as part of an 2660 // expression; we handle identifiers that occur in other 2661 // circumstances (e.g. in a type) separately in visitType. 2662 this.visitExpressionMember(node); 2663 return; 2664 case ts.SyntaxKind.ThisKeyword: 2665 return this.visitThisKeyword(node as ts.ThisExpression); 2666 case ts.SyntaxKind.ModuleDeclaration: 2667 return this.visitModuleDeclaration(node as ts.ModuleDeclaration); 2668 case ts.SyntaxKind.CallExpression: 2669 case ts.SyntaxKind.NewExpression: 2670 this.visitCallOrNewExpression( 2671 node as ts.CallExpression | ts.NewExpression); 2672 return; 2673 case ts.SyntaxKind.ReturnStatement: 2674 this.visitReturnStatement(node as ts.ReturnStatement) 2675 return; 2676 case ts.SyntaxKind.AsExpression: 2677 this.visitAsExpressions(node as ts.AsExpression); 2678 return; 2679 default: 2680 // Use default recursive processing. 2681 return ts.forEachChild(node, n => this.visit(n)); 2682 } 2683 } 2684 2685 /** index is the main entry point, starting the recursive visit. */ 2686 index() { 2687 this.emitFact(this.kFile, FactName.NODE_KIND, NodeKind.FILE); 2688 this.emitFact(this.kFile, FactName.TEXT, this.file.text); 2689 2690 this.emitModuleAnchor(this.file); 2691 2692 // Emit file-level init function to contain all call anchors that 2693 // don't have parent functions. 2694 const fileInitFunc = this.getSyntheticFileInitVName(); 2695 this.emitFact(fileInitFunc, FactName.NODE_KIND, NodeKind.FUNCTION); 2696 this.emitEdge( 2697 this.newAnchor(this.file, 0, 0), EdgeKind.DEFINES, fileInitFunc); 2698 2699 ts.forEachChild(this.file, n => this.visit(n)); 2700 } 2701 } 2702 2703 /** 2704 * Main plugin that runs over all srcs files in a compilation unit and emits 2705 * Kythe data for all symbols in those files. 2706 */ 2707 class TypescriptIndexer implements Plugin { 2708 name = 'TypescriptIndexerPlugin'; 2709 2710 index(context: IndexerHost) { 2711 for (const path of context.compilationUnit.srcs) { 2712 const sourceFile = context.program.getSourceFile(path); 2713 if (!sourceFile) { 2714 throw new Error(`requested indexing ${path} not found in program`); 2715 } 2716 const visitor = new Visitor(context, sourceFile); 2717 visitor.index(); 2718 } 2719 } 2720 } 2721 2722 /** 2723 * index indexes a TypeScript program, producing Kythe JSON objects for the 2724 * source files in the specified paths. 2725 * 2726 * (A ts.Program is a configured collection of parsed source files, but 2727 * the caller must specify the source files within the program that they want 2728 * Kythe output for, because e.g. the standard library is contained within 2729 * the Program and we only want to process it once.) 2730 * 2731 * @param paths Files to index 2732 */ 2733 export function index(compilationUnit: CompilationUnit, options: IndexingOptions): ts.Diagnostic[] { 2734 const program = ts.createProgram({ 2735 rootNames: compilationUnit.rootFiles, 2736 options: options.compilerOptions, 2737 host: options.compilerHost, 2738 }); 2739 // Note: we only call getPreEmitDiagnostics (which causes type checking to 2740 // happen) on the input paths as provided in paths. This means we don't 2741 // e.g. type-check the standard library unless we were explicitly told to. 2742 const diags: ts.Diagnostic[] = []; 2743 for (const path of compilationUnit.srcs) { 2744 for (const diag of ts.getPreEmitDiagnostics(program, program.getSourceFile(path))) { 2745 diags.push(diag); 2746 } 2747 } 2748 // Note: don't abort if there are diagnostics. This allows us to 2749 // index programs with errors. We return these diagnostics at the end 2750 // so the caller can act on them if it wants. 2751 2752 const indexingContext = new StandardIndexerContext(program, compilationUnit, options); 2753 const plugins = [new TypescriptIndexer(), ...(options.plugins ?? [])]; 2754 for (const plugin of plugins) { 2755 try { 2756 plugin.index(indexingContext); 2757 } catch (err) { 2758 if (indexingContext.options.failAnalysisOnPluginError) { 2759 throw err; 2760 } 2761 console.error(`Plugin ${plugin.name} errored:`, err); 2762 } 2763 } 2764 2765 return diags; 2766 } 2767 2768 /** 2769 * loadTsConfig loads a tsconfig.json from a path, throwing on any errors 2770 * like "file not found" or parse errors. 2771 */ 2772 export function loadTsConfig( 2773 tsconfigPath: string, projectPath: string, 2774 host: ts.ParseConfigHost = ts.sys): ts.ParsedCommandLine { 2775 projectPath = path.resolve(projectPath); 2776 const {config: json, error} = ts.readConfigFile(tsconfigPath, host.readFile); 2777 if (error) { 2778 throw new Error(ts.formatDiagnostics([error], ts.createCompilerHost({}))); 2779 } 2780 const config = ts.parseJsonConfigFileContent(json, host, projectPath); 2781 if (config.errors.length > 0) { 2782 throw new Error( 2783 ts.formatDiagnostics(config.errors, ts.createCompilerHost({}))); 2784 } 2785 return config; 2786 } 2787 2788 function main(argv: string[]) { 2789 if (argv.length < 1) { 2790 console.error('usage: indexer path/to/tsconfig.json [PATH...]'); 2791 return 1; 2792 } 2793 2794 const config = loadTsConfig(argv[0], path.dirname(argv[0])); 2795 let inPaths = argv.slice(1); 2796 if (inPaths.length === 0) { 2797 inPaths = config.fileNames; 2798 } 2799 2800 // This program merely demonstrates the API, so use a fake corpus/root/etc. 2801 const rootVName: VName = { 2802 corpus: 'corpus', 2803 root: '', 2804 path: '', 2805 signature: '', 2806 language: '', 2807 }; 2808 const compilationUnit: CompilationUnit = { 2809 srcs: inPaths, 2810 rootVName, 2811 rootFiles: inPaths, 2812 fileVNames: new Map(), 2813 }; 2814 index(compilationUnit, { 2815 compilerOptions: config.options, 2816 compilerHost: ts.createCompilerHost(config.options), 2817 emit(obj: JSONFact | JSONEdge) { 2818 console.log(JSON.stringify(obj)); 2819 } 2820 }); 2821 return 0; 2822 } 2823 2824 if (require.main === module) { 2825 // Note: do not use process.exit(), because that does not ensure that 2826 // process.stdout has been flushed(!). 2827 process.exitCode = main(process.argv.slice(2)); 2828 }