| /* |
| * Copyright 2017 The Kythe Authors. All rights reserved. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| import 'source-map-support/register'; |
| import * as fs from 'fs'; |
| import * as path from 'path'; |
| import * as ts from 'typescript'; |
| |
| import {EdgeKind, FactName, JSONEdge, JSONFact, makeOrdinalEdge, NodeKind, OrdinalEdge, Subkind, VName} from './kythe'; |
| import * as utf8 from './utf8'; |
| |
| const LANGUAGE = 'typescript'; |
| |
| /** |
| * An indexer host holds information about the program indexing and methods |
| * used by the TypeScript indexer that may also be useful to plugins, reducing |
| * code duplication. |
| */ |
| export interface IndexerHost { |
| /** |
| * Gets the offset table for a file path. |
| * These are used to lookup UTF-8 offsets (used by Kythe) from UTF-16 offsets |
| * (used by TypeScript), and vice versa. |
| */ |
| getOffsetTable(path: string): Readonly<utf8.OffsetTable>; |
| /** |
| * getSymbolAtLocation is the same as ts.TypeChecker.getSymbolAtLocation, |
| * except that it has a return type that properly captures that |
| * getSymbolAtLocation can return undefined. (The TypeScript API itself is |
| * not yet null-safe, so it hasn't been annotated with full types.) |
| */ |
| getSymbolAtLocation(node: ts.Node): ts.Symbol|undefined; |
| /** |
| * Computes the VName (and signature) of a ts.Symbol. A Context can be |
| * optionally specified to help disambiguate nodes with multiple declarations. |
| * See the documentation of Context for more information. |
| */ |
| getSymbolName(sym: ts.Symbol, ns: TSNamespace, context?: Context): VName; |
| /** |
| * scopedSignature computes a scoped name for a ts.Node. |
| * E.g. if you have a function `foo` containing a block containing a variable |
| * `bar`, it might return a VName like |
| * signature: "foo.block0.bar"" |
| * path: <appropriate path to module> |
| */ |
| scopedSignature(startNode: ts.Node): VName; |
| /** |
| * Converts a file path into a file VName. |
| */ |
| pathToVName(path: string): VName; |
| /** |
| * Returns the module name of a TypeScript source file. |
| * See moduleName() for more details. |
| */ |
| moduleName(path: string): string; |
| /** |
| * Paths to index. |
| */ |
| paths: string[]; |
| /** |
| * TypeScript program. |
| */ |
| program: ts.Program; |
| /** |
| * Strategy to emit Kythe entries by. |
| * |
| * TODO(ayazhafiz): change type to `JSONFact|JSONEdge` after downstream |
| * clients are updated to use Kythe types. |
| */ |
| emit(obj: {}): void; |
| } |
| |
| /** |
| * A indexer plugin adds extra functionality with the same inputs as the base |
| * indexer. |
| */ |
| export interface Plugin { |
| /** Name of the plugin. It will be printed to stderr when running plugin. */ |
| name: string; |
| /** |
| * Indexes a TypeScript program with extra functionality. |
| * Takes a indexer host, which provides useful properties and methods that |
| * the plugin can defer to rather than reimplementing. |
| */ |
| index(context: IndexerHost): void; |
| } |
| |
| /** |
| * toArray converts an Iterator to an array of its values. |
| * It's necessary when running in ES5 environments where for-of loops |
| * don't iterate through Iterators. |
| */ |
| function toArray<T>(it: Iterator<T>): T[] { |
| const array: T[] = []; |
| for (let next = it.next(); !next.done; next = it.next()) { |
| array.push(next.value); |
| } |
| return array; |
| } |
| |
| /** |
| * stripExtension strips the .d.ts or .ts extension from a path. |
| * It's used to map a file path to the module name. |
| */ |
| function stripExtension(path: string): string { |
| return path.replace(/\.(d\.)?ts$/, ''); |
| } |
| |
| /** |
| * TSNamespace represents the three declaration namespaces of TypeScript: types, |
| * values, and (confusingly) namespaces. A given symbol may be a type, and/or a |
| * value, and/or a namespace. |
| * |
| * See the table at |
| * https://www.typescriptlang.org/docs/handbook/declaration-merging.html |
| * for a listing of namespace groups for various declaration types and further |
| * discussion. |
| */ |
| export enum TSNamespace { |
| TYPE, |
| VALUE, |
| NAMESPACE, |
| } |
| |
| /** |
| * Context represents the environment a node is declared in, and may be used for |
| * disambiguating a node's declarations if it has multiple. |
| */ |
| export enum Context { |
| /** |
| * No disambiguation about a node's declarations. May be lazily generated |
| * from other contexts; see SymbolVNameStore documentation. |
| */ |
| Any, |
| /** The node is declared as a getter. */ |
| Getter, |
| /** The node is declared as a setter. */ |
| Setter, |
| } |
| |
| /** |
| * Determines if a node is a variable-like declaration. |
| * |
| * TODO(https://github.com/microsoft/TypeScript/issues/33115): Replace this with |
| * a native `ts.isHasExpressionInitializer` if TypeScript ever adds it. |
| */ |
| function hasExpressionInitializer(node: ts.Node): |
| node is ts.HasExpressionInitializer { |
| return ts.isVariableDeclaration(node) || ts.isParameter(node) || |
| ts.isBindingElement(node) || ts.isPropertySignature(node) || |
| ts.isPropertyDeclaration(node) || ts.isPropertyAssignment(node) || |
| ts.isEnumMember(node); |
| } |
| |
| /** |
| * Determines if a node is a static member of a class. |
| */ |
| function isStaticMember(node: ts.Node, klass: ts.Declaration): boolean { |
| return ts.isPropertyDeclaration(node) && node.parent === klass && |
| ((ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Static) > 0); |
| } |
| |
| function todo(sourceRoot: string, node: ts.Node, message: string) { |
| const sourceFile = node.getSourceFile(); |
| const file = path.relative(sourceRoot, sourceFile.fileName); |
| const {line, character} = |
| ts.getLineAndCharacterOfPosition(sourceFile, node.getStart()); |
| console.warn(`TODO: ${file}:${line}:${character}: ${message}`); |
| } |
| |
| type NamespaceAndContext = string&{__brand: 'nsctx'}; |
| /** |
| * A SymbolVNameStore stores a mapping of symbols to the (many) VNames it may |
| * have. Each TypeScript symbol can be be of a different TypeScript namespace |
| * and be declared in a unique context, leading to a total (`TSNamespace` * |
| * `Context`) number of possible VNames for the symbol. |
| * |
| * TSNamespace + Context |
| * ----------- ------- |
| * TYPE Any |
| * ts.Symbol -> VALUE Getter -> VName |
| * NAMESPACE Setter |
| * ... |
| * |
| * The `Any` context makes no guarantee of symbol declaration disambiguation. |
| * As a result, unless explicitly set for a given symbol and namespace, the |
| * VName of an `Any` context is lazily set to the VName of an arbitrary context. |
| */ |
| class SymbolVNameStore { |
| private readonly store = |
| new Map<ts.Symbol, Map<NamespaceAndContext, Readonly<VName>>>(); |
| |
| /** |
| * Serializes a namespace and context as a string to lookup in the store. |
| * |
| * Each instance of a JavaScript object is unique, so using one as a key fails |
| * because a new object would be generated every time the store is queried. |
| */ |
| private serialize(ns: TSNamespace, context: Context): NamespaceAndContext { |
| return `${ns}${context}` as NamespaceAndContext; |
| } |
| |
| /** Get a symbol VName for a given namespace and context, if it exists. */ |
| get(symbol: ts.Symbol, ns: TSNamespace, context: Context): VName|undefined { |
| if (this.store.has(symbol)) { |
| const nsCtx = this.serialize(ns, context); |
| return this.store.get(symbol)!.get(nsCtx); |
| } |
| return undefined; |
| } |
| |
| /** |
| * Set a symbol VName for a given namespace and context. Throws if a VName |
| * already exists. |
| */ |
| set(symbol: ts.Symbol, ns: TSNamespace, context: Context, vname: VName) { |
| let vnameMap = this.store.get(symbol); |
| const nsCtx = this.serialize(ns, context); |
| if (vnameMap) { |
| if (vnameMap.has(nsCtx)) { |
| throw new Error(`VName already set with signature ${ |
| vnameMap.get(nsCtx)!.signature}`); |
| } |
| vnameMap.set(nsCtx, vname); |
| } else { |
| this.store.set(symbol, new Map([[nsCtx, vname]])); |
| } |
| |
| // Set the symbol VName for the given namespace and `Any` context, if it has |
| // not already been set. |
| const nsAny = this.serialize(ns, Context.Any); |
| vnameMap = this.store.get(symbol)!; |
| if (!vnameMap.has(nsAny)) { |
| vnameMap.set(nsAny, vname); |
| } |
| } |
| } |
| |
| /** |
| * isParameterPropertyDeclaration wraps ts.isParameterPropertyDeclaration and |
| * exposes an API that's compatible across TypeScript 3.5 & 3.6. |
| */ |
| function isParameterPropertyDeclaration(node: ts.Node, parent: ts.Node): node is ts.ParameterPropertyDeclaration { |
| // TODO: remove/inline once fully on TypeScript 3.6+ |
| return (ts.isParameterPropertyDeclaration as any)(node, parent); |
| } |
| |
| /** |
| * StandardIndexerContext provides the standard definition of information about |
| * a TypeScript program and common methods used by the TypeScript indexer and |
| * its plugins. See the IndexerContext interface definition for more details. |
| */ |
| class StandardIndexerContext implements IndexerHost { |
| private offsetTables = new Map<string, utf8.OffsetTable>(); |
| |
| /** A shorter name for the rootDir in the CompilerOptions. */ |
| private sourceRoot: string; |
| |
| /** |
| * rootDirs is the list of rootDirs in the compiler options, sorted |
| * longest first. See this.moduleName(). |
| */ |
| private rootDirs: string[]; |
| |
| /** symbolNames is a store of ts.Symbols to their assigned VNames. */ |
| private symbolNames = new SymbolVNameStore(); |
| |
| /** |
| * anonId increments for each anonymous block, to give them unique |
| * signatures. |
| */ |
| private anonId = 0; |
| |
| /** |
| * anonNames maps nodes to the anonymous names assigned to them. |
| */ |
| private anonNames = new Map<ts.Node, string>(); |
| |
| private typeChecker: ts.TypeChecker; |
| |
| constructor( |
| /** |
| * The VName for the CompilationUnit, containing compilation-wide info. |
| */ |
| private readonly compilationUnit: VName, |
| /** |
| * A map of path to path-specific VName. |
| */ |
| private readonly pathVNames: Map<string, VName>, |
| /** All source file paths in the TypeScript program. */ |
| public paths: string[], |
| public program: ts.Program, |
| private readFile: (path: string) => Buffer = fs.readFileSync, |
| ) { |
| this.sourceRoot = program.getCompilerOptions().rootDir || process.cwd(); |
| let rootDirs = program.getCompilerOptions().rootDirs || [this.sourceRoot]; |
| rootDirs = rootDirs.map(d => d + '/'); |
| rootDirs.sort((a, b) => b.length - a.length); |
| this.rootDirs = rootDirs; |
| this.typeChecker = this.program.getTypeChecker(); |
| } |
| |
| getOffsetTable(path: string): Readonly<utf8.OffsetTable> { |
| let table = this.offsetTables.get(path); |
| if (!table) { |
| const buf = this.readFile(path); |
| table = new utf8.OffsetTable(buf); |
| this.offsetTables.set(path, table); |
| } |
| return table; |
| } |
| |
| getSymbolAtLocation(node: ts.Node): ts.Symbol|undefined { |
| return this.typeChecker.getSymbolAtLocation(node); |
| } |
| |
| /** |
| * anonName assigns a freshly generated name to a Node. |
| * It's used to give stable names to e.g. anonymous objects. |
| */ |
| anonName(node: ts.Node): string { |
| let name = this.anonNames.get(node); |
| if (!name) { |
| name = `anon${this.anonId++}`; |
| this.anonNames.set(node, name); |
| } |
| return name; |
| } |
| |
| /** |
| * scopedSignature computes a scoped name for a ts.Node. |
| * E.g. if you have a function `foo` containing a block containing a variable |
| * `bar`, it might return a VName like |
| * signature: "foo.block0.bar"" |
| * path: <appropriate path to module> |
| */ |
| scopedSignature(startNode: ts.Node): VName { |
| let moduleName: string|undefined; |
| const parts: string[] = []; |
| |
| // Traverse the containing blocks upward, gathering names from nodes that |
| // introduce scopes. |
| for (let node: ts.Node|undefined = startNode, |
| lastNode: ts.Node|undefined = undefined; |
| node != null; lastNode = node, node = node.parent) { |
| // Nodes that are rvalues of a named initialization should not introduce a |
| // new scope. For instance, in `const a = class A {}`, `A` should |
| // contribute nothing to the scoped signature. |
| if (node.parent && hasExpressionInitializer(node.parent) && |
| node.parent.name.kind === ts.SyntaxKind.Identifier) { |
| continue; |
| } |
| |
| switch (node.kind) { |
| case ts.SyntaxKind.ExportAssignment: |
| const exportDecl = node as ts.ExportAssignment; |
| if (!exportDecl.isExportEquals) { |
| // It's an "export default" statement. |
| // This is semantically equivalent to exporting a variable |
| // named 'default'. |
| parts.push('default'); |
| } else { |
| parts.push('export='); |
| } |
| break; |
| case ts.SyntaxKind.ArrowFunction: |
| // Arrow functions are anonymous, so generate a unique id. |
| parts.push(`arrow${this.anonId++}`); |
| break; |
| case ts.SyntaxKind.FunctionExpression: |
| // Function expressions look like |
| // (function() {}) |
| // which have no name but introduce an anonymous scope. |
| parts.push(`func${this.anonId++}`); |
| break; |
| case ts.SyntaxKind.Block: |
| // Blocks need their own scopes for contained variable declarations. |
| if (node.parent && |
| (node.parent.kind === ts.SyntaxKind.FunctionDeclaration || |
| node.parent.kind === ts.SyntaxKind.MethodDeclaration || |
| node.parent.kind === ts.SyntaxKind.Constructor || |
| node.parent.kind === ts.SyntaxKind.ForStatement || |
| node.parent.kind === ts.SyntaxKind.ForInStatement || |
| node.parent.kind === ts.SyntaxKind.ForOfStatement)) { |
| // A block that's an immediate child of the above node kinds |
| // already has a scoped name generated by that parent. |
| // (It would be fine to not handle this specially and just fall |
| // through to the below code, but avoiding it here makes the names |
| // simpler.) |
| continue; |
| } |
| parts.push(`block${this.anonId++}`); |
| break; |
| case ts.SyntaxKind.ForStatement: |
| case ts.SyntaxKind.ForInStatement: |
| case ts.SyntaxKind.ForOfStatement: |
| // Introduce a naming scope for all variables declared within the |
| // statement, so that the two 'x's declared here get different names: |
| // for (const x in y) { ... } |
| // for (const x in y) { ... } |
| parts.push(`for${this.anonId++}`); |
| break; |
| case ts.SyntaxKind.BindingElement: |
| case ts.SyntaxKind.ClassDeclaration: |
| case ts.SyntaxKind.ClassExpression: |
| case ts.SyntaxKind.EnumDeclaration: |
| case ts.SyntaxKind.EnumMember: |
| case ts.SyntaxKind.FunctionDeclaration: |
| case ts.SyntaxKind.InterfaceDeclaration: |
| case ts.SyntaxKind.ImportEqualsDeclaration: |
| case ts.SyntaxKind.ImportSpecifier: |
| case ts.SyntaxKind.ExportSpecifier: |
| case ts.SyntaxKind.MethodDeclaration: |
| case ts.SyntaxKind.MethodSignature: |
| case ts.SyntaxKind.NamespaceImport: |
| case ts.SyntaxKind.ObjectLiteralExpression: |
| case ts.SyntaxKind.Parameter: |
| case ts.SyntaxKind.PropertyAccessExpression: |
| case ts.SyntaxKind.PropertyAssignment: |
| case ts.SyntaxKind.PropertyDeclaration: |
| case ts.SyntaxKind.PropertySignature: |
| case ts.SyntaxKind.TypeAliasDeclaration: |
| case ts.SyntaxKind.TypeParameter: |
| case ts.SyntaxKind.VariableDeclaration: |
| case ts.SyntaxKind.GetAccessor: |
| case ts.SyntaxKind.SetAccessor: |
| case ts.SyntaxKind.ShorthandPropertyAssignment: |
| const decl = node as ts.NamedDeclaration; |
| if (decl.name) { |
| switch (decl.name.kind) { |
| case ts.SyntaxKind.Identifier: |
| case ts.SyntaxKind.StringLiteral: |
| case ts.SyntaxKind.NumericLiteral: |
| case ts.SyntaxKind.ComputedPropertyName: |
| case ts.SyntaxKind.NoSubstitutionTemplateLiteral: |
| let part; |
| if (ts.isComputedPropertyName(decl.name)) { |
| const sym = this.getSymbolAtLocation(decl.name); |
| part = sym ? sym.name : this.anonName(decl.name); |
| } else { |
| part = decl.name.text; |
| } |
| // Wrap literals in quotes, so that characters used in other |
| // signatures do not interfere with the signature created by a |
| // literal. For instance, a literal |
| // obj.prop |
| // may interefere with the signature of `prop` on an object |
| // `obj`. The literal receives a signature |
| // "obj.prop" |
| // to avoid this. |
| if (ts.isStringLiteral(decl.name)) { |
| part = `"${part}"`; |
| } |
| // Instance members of a class are scoped to the type of the |
| // class. |
| if (ts.isClassDeclaration(decl) && lastNode !== undefined && |
| ts.isClassElement(lastNode) && |
| !isStaticMember(lastNode, decl)) { |
| part += '#type'; |
| } |
| // Getters and setters semantically refer to the same entities |
| // but are declared differently, so they are differentiated. |
| if (ts.isGetAccessor(decl)) { |
| part += ':getter'; |
| } else if (ts.isSetAccessor(decl)) { |
| part += ':setter'; |
| } |
| parts.push(part); |
| break; |
| default: |
| // Skip adding an anonymous scope for variables declared in an |
| // array or object binding pattern like `const [a] = [0]`. |
| break; |
| } |
| } else { |
| parts.push(this.anonName(node)); |
| } |
| break; |
| case ts.SyntaxKind.Constructor: |
| // Class members declared with a shorthand in the constructor should |
| // be scoped to the class, not the constructor. |
| if (!isParameterPropertyDeclaration(startNode, startNode.parent)) { |
| parts.push('constructor'); |
| } |
| break; |
| case ts.SyntaxKind.ImportClause: |
| // An import clause can have one of two forms: |
| // import foo from './bar'; |
| // import {foo as far} from './bar'; |
| // In the first case the clause has a name "foo". In this case add the |
| // name of the clause to the signature. |
| // In the second case the clause has no explicit name. This |
| // contributes nothing to the signature without risk of naming |
| // conflicts because TS imports are essentially file-global lvalues. |
| const importClause = node as ts.ImportClause; |
| if (importClause.name) { |
| parts.push(importClause.name.text); |
| } |
| break; |
| case ts.SyntaxKind.ModuleDeclaration: |
| const modDecl = node as ts.ModuleDeclaration; |
| if (modDecl.name.kind === ts.SyntaxKind.StringLiteral) { |
| // Syntax like: |
| // declare module 'foo/bar' {} |
| // This is the syntax for defining symbols in another, named |
| // module. |
| moduleName = (modDecl.name as ts.StringLiteral).text; |
| } else if (modDecl.name.kind === ts.SyntaxKind.Identifier) { |
| // Syntax like: |
| // declare module foo {} |
| // without quotes is just an obsolete way of saying 'namespace'. |
| parts.push((modDecl.name as ts.Identifier).text); |
| } |
| break; |
| case ts.SyntaxKind.SourceFile: |
| // moduleName can already be set if the target was contained within |
| // a "declare module 'foo/bar'" block (see the handling of |
| // ModuleDeclaration). Otherwise, the module name is derived from the |
| // name of the current file. |
| if (!moduleName) { |
| moduleName = this.moduleName((node as ts.SourceFile).fileName); |
| } |
| break; |
| case ts.SyntaxKind.JsxElement: |
| case ts.SyntaxKind.JsxSelfClosingElement: |
| case ts.SyntaxKind.JsxAttribute: |
| // Given a unique anonymous name to all JSX nodes. This prevents |
| // conflicts in cases where attributes would otherwise have the same |
| // name, like `src` in |
| // <img src={a} /> |
| // <img src={b} /> |
| parts.push(`jsx${this.anonId++}`); |
| break; |
| default: |
| // Most nodes are children of other nodes that do not introduce a |
| // new namespace, e.g. "return x;", so ignore all other parents |
| // by default. |
| // TODO: namespace {}, etc. |
| |
| // If the node is actually some subtype that has a 'name' attribute |
| // it's likely this function should have handled it. Dynamically |
| // probe for this case and warn if we missed one. |
| if ('name' in (node as any)) { |
| todo( |
| this.sourceRoot, node, |
| `scopedSignature: ${ts.SyntaxKind[node.kind]} ` + |
| `has unused 'name' property`); |
| } |
| } |
| } |
| |
| // The names were gathered from bottom to top, so reverse before joining. |
| const signature = parts.reverse().join('.'); |
| return Object.assign( |
| this.pathToVName(moduleName!), {signature, language: LANGUAGE}); |
| } |
| |
| /** |
| * getSymbolName computes the VName of a ts.Symbol. A Context can be |
| * optionally specified to help disambiguate nodes with multiple declarations. |
| * See the documentation of Context for more information. |
| */ |
| getSymbolName( |
| sym: ts.Symbol, ns: TSNamespace, context: Context = Context.Any): VName { |
| const stored = this.symbolNames.get(sym, ns, context); |
| if (stored) return stored; |
| |
| let declarations = sym.declarations; |
| if (declarations.length < 1) { |
| throw new Error('TODO: symbol has no declarations?'); |
| } |
| |
| // Disambiguate symbols with multiple declarations using a context. |
| if (sym.declarations.length > 1) { |
| switch (context) { |
| case Context.Getter: |
| declarations = declarations.filter(ts.isGetAccessor); |
| break; |
| case Context.Setter: |
| declarations = declarations.filter(ts.isSetAccessor); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| const decl = declarations[0]; |
| const vname = this.scopedSignature(decl); |
| // The signature of a value is undecorated. |
| // The signature of a type has the #type suffix. |
| // The signature of a namespace has the #namespace suffix. |
| if (ns === TSNamespace.TYPE) { |
| vname.signature += '#type'; |
| } else if (ns === TSNamespace.NAMESPACE) { |
| vname.signature += '#namespace'; |
| } |
| |
| // Cache the VName for future lookups. |
| this.symbolNames.set(sym, ns, context, vname); |
| return vname; |
| } |
| |
| /** |
| * moduleName returns the ES6 module name of a path to a source file. |
| * E.g. foo/bar.ts and foo/bar.d.ts both have the same module name, |
| * 'foo/bar', and rootDirs (like bazel-bin/) are eliminated. |
| * See README.md for a discussion of this. |
| */ |
| moduleName(sourcePath: string): string { |
| // Compute sourcePath as relative to one of the rootDirs. |
| // This canonicalizes e.g. bazel-bin/foo to just foo. |
| // Note that this.rootDirs is sorted longest first, so we'll use the |
| // longest match. |
| for (const rootDir of this.rootDirs) { |
| if (sourcePath.startsWith(rootDir)) { |
| sourcePath = path.relative(rootDir, sourcePath); |
| break; |
| } |
| } |
| return stripExtension(sourcePath); |
| } |
| |
| /** |
| * pathToVName returns the VName for a given file path. |
| */ |
| pathToVName(path: string): VName { |
| const vname = this.pathVNames.get(path); |
| return { |
| signature: '', |
| language: '', |
| corpus: vname && vname.corpus ? vname.corpus : |
| this.compilationUnit.corpus, |
| root: vname && vname.corpus ? vname.root : this.compilationUnit.root, |
| path: vname && vname.path ? vname.path : path, |
| }; |
| } |
| |
| /** |
| * emit emits a Kythe entry, structured as a JSON object. Defaults to |
| * emitting to stdout but users may replace it. |
| */ |
| emit = (obj: JSONFact|JSONEdge) => { |
| console.log(JSON.stringify(obj)); |
| }; |
| } |
| |
| type ImportVNameSet = { |
| [where in 'type' | 'value']?: |
| {local: Readonly<VName>, remote: Readonly<VName>} |
| }; |
| |
| /** Visitor manages the indexing process for a single TypeScript SourceFile. */ |
| class Visitor { |
| /** kFile is the VName for the 'file' node representing the source file. */ |
| kFile: VName; |
| |
| /** A shorter name for the rootDir in the CompilerOptions. */ |
| sourceRoot: string; |
| |
| typeChecker: ts.TypeChecker; |
| |
| constructor( |
| private readonly host: IndexerHost, |
| private file: ts.SourceFile, |
| ) { |
| this.sourceRoot = |
| this.host.program.getCompilerOptions().rootDir || process.cwd(); |
| |
| this.typeChecker = this.host.program.getTypeChecker(); |
| |
| this.kFile = this.newFileVName(file.fileName); |
| } |
| |
| /** |
| * newFileVName returns a new VName for the given file path. |
| */ |
| newFileVName(path: string): VName { |
| return this.host.pathToVName(path); |
| } |
| |
| /** |
| * newVName returns a new VName with a given signature and path. |
| */ |
| newVName(signature: string, path: string): VName { |
| return Object.assign( |
| this.newFileVName(path), {signature: signature, language: LANGUAGE}); |
| } |
| |
| /** newAnchor emits a new anchor entry that covers a TypeScript node. */ |
| newAnchor(node: ts.Node, start = node.getStart(), end = node.end): VName { |
| const name = Object.assign( |
| {...this.kFile}, {signature: `@${start}:${end}`, language: LANGUAGE}); |
| this.emitNode(name, 'anchor'); |
| const offsetTable = this.host.getOffsetTable(node.getSourceFile().fileName); |
| this.emitFact( |
| name, FactName.LOC_START, offsetTable.lookupUtf8(start).toString()); |
| this.emitFact( |
| name, FactName.LOC_END, offsetTable.lookupUtf8(end).toString()); |
| return name; |
| } |
| |
| /** emitNode emits a new node entry, declaring the kind of a VName. */ |
| emitNode(source: VName, kind: string) { |
| this.emitFact(source, FactName.NODE_KIND, kind); |
| } |
| |
| /** emitSubkind emits a new fact entry, declaring the subkind of a VName. */ |
| emitSubkind(source: VName, subkind: Subkind) { |
| this.emitFact(source, FactName.SUBKIND, subkind); |
| } |
| |
| /** emitFact emits a new fact entry, tying an attribute to a VName. */ |
| emitFact(source: VName, name: FactName, value: string) { |
| this.host.emit({ |
| source, |
| fact_name: name, |
| fact_value: Buffer.from(value).toString('base64'), |
| }); |
| } |
| |
| /** emitEdge emits a new edge entry, relating two VNames. */ |
| emitEdge(source: VName, kind: EdgeKind|OrdinalEdge, target: VName) { |
| this.host.emit({ |
| source, |
| edge_kind: kind, |
| target, |
| fact_name: '/', |
| }); |
| } |
| |
| visitTypeParameters(params: ReadonlyArray<ts.TypeParameterDeclaration>) { |
| for (const param of params) { |
| const sym = this.host.getSymbolAtLocation(param.name); |
| if (!sym) { |
| todo( |
| this.sourceRoot, param, |
| `type param ${param.getText()} has no symbol`); |
| return; |
| } |
| const kType = this.host.getSymbolName(sym, TSNamespace.TYPE); |
| this.emitNode(kType, 'absvar'); |
| this.emitEdge( |
| this.newAnchor(param.name), EdgeKind.DEFINES_BINDING, kType); |
| // ...<T extends A> |
| if (param.constraint) { |
| const superType = this.visitType(param.constraint); |
| if (superType) this.emitEdge(kType, EdgeKind.BOUNDED_UPPER, superType); |
| } |
| // ...<T = A> |
| if (param.default) this.visitType(param.default); |
| } |
| } |
| |
| /** |
| * visitHeritage visits the X found in an 'extends X' or 'implements X'. |
| * |
| * These are subtle in an interesting way. When you have |
| * interface X extends Y {} |
| * that is referring to the *type* Y (because interfaces are types, not |
| * values). But it's also legal to write |
| * class X extends (class Z { ... }) {} |
| * where the thing in the extends clause is itself an expression, and the |
| * existing logic for visiting a class expression already handles modelling |
| * the class as both a type and a value. |
| * |
| * The full set of possible combinations is: |
| * - class extends => value |
| * - interface extends => type |
| * - class implements => type |
| * - interface implements => illegal |
| */ |
| visitHeritage(heritageClauses: ReadonlyArray<ts.HeritageClause>) { |
| for (const heritage of heritageClauses) { |
| if (heritage.token === ts.SyntaxKind.ExtendsKeyword && heritage.parent && |
| heritage.parent.kind !== ts.SyntaxKind.InterfaceDeclaration) { |
| this.visit(heritage); |
| } else { |
| this.visitType(heritage); |
| } |
| } |
| } |
| |
| visitInterfaceDeclaration(decl: ts.InterfaceDeclaration) { |
| const sym = this.host.getSymbolAtLocation(decl.name); |
| if (!sym) { |
| todo( |
| this.sourceRoot, decl.name, |
| `interface ${decl.name.getText()} has no symbol`); |
| return; |
| } |
| const kType = this.host.getSymbolName(sym, TSNamespace.TYPE); |
| this.emitNode(kType, 'interface'); |
| this.emitEdge(this.newAnchor(decl.name), EdgeKind.DEFINES_BINDING, kType); |
| |
| if (decl.typeParameters) this.visitTypeParameters(decl.typeParameters); |
| if (decl.heritageClauses) this.visitHeritage(decl.heritageClauses); |
| for (const member of decl.members) { |
| this.visit(member); |
| } |
| } |
| |
| visitTypeAliasDeclaration(decl: ts.TypeAliasDeclaration) { |
| const sym = this.host.getSymbolAtLocation(decl.name); |
| if (!sym) { |
| todo( |
| this.sourceRoot, decl.name, |
| `type alias ${decl.name.getText()} has no symbol`); |
| return; |
| } |
| const kType = this.host.getSymbolName(sym, TSNamespace.TYPE); |
| this.emitNode(kType, 'talias'); |
| this.emitEdge(this.newAnchor(decl.name), EdgeKind.DEFINES_BINDING, kType); |
| |
| if (decl.typeParameters) this.visitTypeParameters(decl.typeParameters); |
| const kAlias = this.visitType(decl.type); |
| // Emit an "aliases" edge if the aliased type has a singular VName. |
| if (kAlias) { |
| this.emitEdge(kType, EdgeKind.ALIASES, kAlias); |
| } |
| } |
| |
| /** |
| * visitType is the main dispatch for visiting type nodes. |
| * It's separate from visit() because bare ts.Identifiers within a normal |
| * expression are values (handled by visit) but bare ts.Identifiers within |
| * a type are types (handled here). |
| * |
| * @return the VName of the type, if available. (For more complex types, |
| * e.g. Array<string>, we might not have a VName for the specific type.) |
| */ |
| visitType(node: ts.Node): VName|undefined { |
| switch (node.kind) { |
| case ts.SyntaxKind.TypeReference: |
| const ref = node as ts.TypeReferenceNode; |
| const kType = this.visitType((node as ts.TypeReferenceNode).typeName); |
| |
| // Return no VName for types with type arguments because their VName is |
| // not qualified. E.g. |
| // Array<string> |
| // Array<number> |
| // have the same VName signature "Array" |
| if (ref.typeArguments) { |
| ref.typeArguments.forEach(type => this.visitType(type)); |
| return; |
| } |
| return kType; |
| case ts.SyntaxKind.Identifier: |
| const sym = this.host.getSymbolAtLocation(node); |
| if (!sym) { |
| todo(this.sourceRoot, node, `type ${node.getText()} has no symbol`); |
| return; |
| } |
| const name = this.host.getSymbolName(sym, TSNamespace.TYPE); |
| this.emitEdge(this.newAnchor(node), EdgeKind.REF, name); |
| return name; |
| case ts.SyntaxKind.TypeReference: |
| const typeRef = node as ts.TypeReferenceNode; |
| if (!typeRef.typeArguments) { |
| // If it's an direct type reference, e.g. SomeInterface |
| // as opposed to SomeInterface<T>, then the VName for the type |
| // reference is just the inner type name. |
| return this.visitType(typeRef.typeName); |
| } |
| // Otherwise, leave it to the default handling. |
| break; |
| case ts.SyntaxKind.TypeQuery: |
| // This is a 'typeof' expression, which takes a value as its argument, |
| // so use visit() instead of visitType(). |
| const typeQuery = node as ts.TypeQueryNode; |
| this.visit(typeQuery.exprName); |
| return; // Avoid default recursion. |
| } |
| |
| // Default recursion, but using visitType(), not visit(). |
| ts.forEachChild(node, n => { |
| this.visitType(n); |
| }); |
| // Because we don't know the specific thing we visited, give the caller |
| // back no name. |
| return undefined; |
| } |
| |
| /** |
| * getPathFromModule gets the "module path" from the module import |
| * symbol referencing a module system path to reference to a module. |
| * |
| * E.g. from |
| * import ... from './foo'; |
| * getPathFromModule(the './foo' node) might return a string like |
| * 'path/to/project/foo'. See this.moduleName(). |
| */ |
| getModulePathFromModuleReference(sym: ts.Symbol): string|undefined { |
| const sf = sym.valueDeclaration; |
| // If the module is not a source file, it does not have a unique file path. |
| // This can happen in cases of importing local modules, like |
| // declare namespace Foo {} |
| // import foo = Foo; |
| if (!ts.isSourceFile(sf)) return undefined; |
| return this.host.moduleName(sf.fileName); |
| } |
| |
| /** |
| * Returns the location of a text in the source code of a node. |
| */ |
| getTextSpan(node: ts.Node, text: string): {start: number, end: number} { |
| const ofs = node.getText().indexOf(text); |
| if (ofs < 0) throw new Error(`${text} not found in ${node.getText()}`); |
| const start = node.getStart() + ofs; |
| const end = start + text.length; |
| return {start, end}; |
| } |
| |
| /** |
| * Returns the symbol of a class constructor if it exists, otherwise nothing. |
| */ |
| getCtorSymbol(klass: ts.ClassDeclaration): ts.Symbol|undefined { |
| if (klass.name) { |
| const sym = this.host.getSymbolAtLocation(klass.name); |
| if (sym && sym.members) { |
| return sym.members.get(ts.InternalSymbolName.Constructor); |
| } |
| } |
| return undefined; |
| } |
| |
| /** |
| * visitImport handles a single entry in an import statement, e.g. |
| * "bar" in code like |
| * import {foo, bar} from 'baz'; |
| * See visitImportDeclaration for the code handling the entire statement. |
| * |
| * @param name TypeScript import declaration node |
| * @param bindingAnchor anchor that "defines/binding" the local import |
| * definition |
| * @param refAnchor anchor that "ref" the import's remote declaration |
| */ |
| visitImport( |
| name: ts.Node, bindingAnchor: Readonly<VName>, |
| refAnchor: Readonly<VName>) { |
| // An import both aliases a symbol from another module |
| // (call it the remote symbol) and it defines a local symbol. |
| // |
| // Those two symbols often have the same name, with statements like: |
| // import {foo} from 'bar'; |
| // But they can be different, in e.g. |
| // import {foo as baz} from 'bar'; |
| // Which maps the remote symbol named 'foo' to a local named 'baz'. |
| // |
| // In all cases TypeScript maintains two different ts.Symbol objects, |
| // one for the local and one for the remote. In principle for the |
| // simple import statement |
| // import {foo} from 'bar'; |
| // "foo" should both: |
| // - "ref/imports" the remote symbol |
| // - "defines/binding" the local symbol |
| |
| const localSym = this.host.getSymbolAtLocation(name); |
| if (!localSym) { |
| throw new Error(`TODO: local name ${name} has no symbol`); |
| } |
| const remoteSym = this.typeChecker.getAliasedSymbol(localSym); |
| |
| // The imported symbol can refer to a type, a value, or both. Attempt to |
| // define local imports and reference remote definitions in both cases. |
| if (remoteSym.flags & ts.SymbolFlags.Value) { |
| const kRemoteValue = |
| this.host.getSymbolName(remoteSym, TSNamespace.VALUE); |
| const kLocalValue = this.host.getSymbolName(localSym, TSNamespace.VALUE); |
| |
| // The local import value is a "variable" with an "import" subkind, and |
| // aliases its remote definition. |
| this.emitNode(kLocalValue, NodeKind.VARIABLE); |
| this.emitFact(kLocalValue, FactName.SUBKIND, Subkind.IMPORT); |
| this.emitEdge(kLocalValue, EdgeKind.ALIASES, kRemoteValue); |
| |
| // Emit edges from the binding and referencing anchors to the import's |
| // local and remote definition, respectively. |
| this.emitEdge(bindingAnchor, EdgeKind.DEFINES_BINDING, kLocalValue); |
| this.emitEdge(refAnchor, EdgeKind.REF_IMPORTS, kRemoteValue); |
| } |
| if (remoteSym.flags & ts.SymbolFlags.Type) { |
| const kRemoteType = this.host.getSymbolName(remoteSym, TSNamespace.TYPE); |
| const kLocalType = this.host.getSymbolName(localSym, TSNamespace.TYPE); |
| |
| // The local import value is a "talias" (type alias) with an "import" |
| // subkind, and aliases its remote definition. |
| this.emitNode(kLocalType, NodeKind.TALIAS); |
| this.emitFact(kLocalType, FactName.SUBKIND, Subkind.IMPORT); |
| this.emitEdge(kLocalType, EdgeKind.ALIASES, kRemoteType); |
| |
| // Emit edges from the binding and referencing anchors to the import's |
| // local and remote definition, respectively. |
| this.emitEdge(bindingAnchor, EdgeKind.DEFINES_BINDING, kLocalType); |
| this.emitEdge(refAnchor, EdgeKind.REF_IMPORTS, kRemoteType); |
| } |
| } |
| |
| /** visitImportDeclaration handles the various forms of "import ...". */ |
| visitImportDeclaration(decl: ts.ImportDeclaration| |
| ts.ImportEqualsDeclaration) { |
| // All varieties of import statements reference a module on the right, |
| // so start by linking that. |
| let moduleRef; |
| if (ts.isImportDeclaration(decl)) { |
| // This is a regular import declaration |
| // import ... from ...; |
| // where the module name is moduleSpecifier. |
| moduleRef = decl.moduleSpecifier; |
| } else { |
| // This is an import equals declaration, which has two cases: |
| // import foo = require('./bar'); |
| // import foo = M.bar; |
| // In the first case the moduleReference is an ExternalModuleReference |
| // whose module name is the expression inside the `require` call. |
| // In the second case the moduleReference is the module name. |
| moduleRef = ts.isExternalModuleReference(decl.moduleReference) ? |
| decl.moduleReference.expression : |
| decl.moduleReference; |
| } |
| const moduleSym = this.host.getSymbolAtLocation(moduleRef); |
| if (!moduleSym) { |
| // This can occur when the module failed to resolve to anything. |
| // See testdata/import_missing.ts for more on how that could happen. |
| return; |
| } |
| const modulePath = this.getModulePathFromModuleReference(moduleSym); |
| if (modulePath) { |
| const kModule = this.newVName('module', modulePath); |
| this.emitEdge(this.newAnchor(moduleRef), EdgeKind.REF_IMPORTS, kModule); |
| } else { |
| // Check if module being imported is declared via `declare module` |
| // and if so - output ref to that statement. |
| const decl = moduleSym.valueDeclaration; |
| if (ts.isModuleDeclaration(decl)) { |
| const kModule = this.host.getSymbolName(moduleSym, TSNamespace.NAMESPACE); |
| this.emitEdge(this.newAnchor(moduleRef), EdgeKind.REF_IMPORTS, kModule); |
| } |
| } |
| |
| // TODO(#4021): See discussion. |
| // Pending changes, an anchor in a Code Search UI cannot currently be |
| // displayed as a node definition and as referencing other nodes. Instead, |
| // for non-renamed imports the local node definition is placed on the |
| // "import" text: |
| // //- @foo ref BarFoo |
| // //- @import defines/binding LocalFoo |
| // //- LocalFoo aliases BarFoo |
| // import {foo} from 'bar'; |
| // For renamed imports the definition and references are separated as |
| // expected: |
| // //- @foo ref BarFoo |
| // //- @baz defines/binding LocalBaz |
| // //- @baz aliases BarFoo |
| // import {foo as baz} from 'bar'; |
| // |
| // Create an anchor for the import text. |
| const importTextSpan = this.getTextSpan(decl, 'import'); |
| const importTextAnchor = |
| this.newAnchor(decl, importTextSpan.start, importTextSpan.end); |
| |
| if (ts.isImportEqualsDeclaration(decl)) { |
| // This is an equals import, e.g.: |
| // import foo = require('./bar'); |
| // |
| // TODO(#4021): Bind the local definition and reference the remote |
| // definition on the import name. |
| this.visitImport(decl.name, importTextAnchor, this.newAnchor(decl.name)); |
| return; |
| } |
| |
| if (!decl.importClause) { |
| // This is a side-effecting import that doesn't declare anything, e.g.: |
| // import 'foo'; |
| return; |
| } |
| const clause = decl.importClause; |
| |
| if (clause.name) { |
| // This is a default import, e.g.: |
| // import foo from './bar'; |
| // |
| // TODO(#4021): Bind the local definition and reference the remote |
| // definition on the import name. |
| this.visitImport( |
| clause.name, importTextAnchor, this.newAnchor(clause.name)); |
| return; |
| } |
| |
| if (!clause.namedBindings) { |
| // TODO: I believe clause.name or clause.namedBindings are always present, |
| // which means this check is not necessary, but the types don't show that. |
| throw new Error(`import declaration ${decl.getText()} has no bindings`); |
| } |
| switch (clause.namedBindings.kind) { |
| case ts.SyntaxKind.NamespaceImport: |
| // This is a namespace import, e.g.: |
| // import * as foo from 'foo'; |
| const name = clause.namedBindings.name; |
| const sym = this.host.getSymbolAtLocation(name); |
| if (!sym) { |
| todo( |
| this.sourceRoot, clause, |
| `namespace import ${clause.getText()} has no symbol`); |
| return; |
| } |
| const kModuleObject = this.host.getSymbolName(sym, TSNamespace.VALUE); |
| this.emitNode(kModuleObject, 'variable'); |
| this.emitEdge( |
| this.newAnchor(name), EdgeKind.DEFINES_BINDING, kModuleObject); |
| break; |
| case ts.SyntaxKind.NamedImports: |
| // This is named imports, e.g.: |
| // import {bar, baz} from 'foo'; |
| const imports = clause.namedBindings.elements; |
| for (const imp of imports) { |
| // If the named import has a property name, e.g. `bar` in |
| // import {bar as baz} from 'foo'; |
| // bind the local definition on the import name "baz" and reference |
| // the remote definition on the property name "bar". Otherwise, bind |
| // the local definition on "import" and reference the remote |
| // definition on the import name. |
| // |
| // TODO(#4021): Unify binding and reference anchors. |
| let bindingAnchor, refAnchor; |
| if (imp.propertyName) { |
| bindingAnchor = this.newAnchor(imp.name); |
| refAnchor = this.newAnchor(imp.propertyName); |
| } else { |
| bindingAnchor = importTextAnchor; |
| refAnchor = this.newAnchor(imp.name); |
| } |
| this.visitImport(imp.name, bindingAnchor, refAnchor); |
| } |
| break; |
| } |
| } |
| |
| /** |
| * When a file imports another file, with syntax like |
| * import * as x from 'some/path'; |
| * we wants 'some/path' to refer to a VName that just means "the entire |
| * file". It doesn't refer to any text in particular, so we just mark |
| * the first letter in the file as the anchor for this. |
| */ |
| emitModuleAnchor(sf: ts.SourceFile) { |
| const kMod = |
| this.newVName('module', this.host.moduleName(this.file.fileName)); |
| this.emitFact(kMod, FactName.NODE_KIND, 'record'); |
| this.emitEdge(this.kFile, EdgeKind.CHILD_OF, kMod); |
| |
| // Emit the anchor, bound to the beginning of the file. |
| const anchor = this.newAnchor(this.file, 0, 1); |
| this.emitEdge(anchor, EdgeKind.DEFINES_BINDING, kMod); |
| } |
| |
| /** |
| * Emits an implicit property for a getter or setter. |
| * For instance, a getter/setter `foo` in class `A` will emit an implicit |
| * property on that class with signature `A.foo`, and create "property/reads" |
| * and "property/writes" from the getters/setters to the implicit property. |
| */ |
| emitImplicitProperty( |
| decl: ts.GetAccessorDeclaration|ts.SetAccessorDeclaration, anchor: VName, |
| funcVName: VName) { |
| // Remove trailing ":getter"/":setter" suffix |
| const propSignature = funcVName.signature.split(':').slice(0, -1).join(':'); |
| const implicitProp = {...funcVName, signature: propSignature}; |
| |
| this.emitNode(implicitProp, 'variable'); |
| this.emitSubkind(implicitProp, Subkind.IMPLICIT); |
| this.emitEdge(anchor, EdgeKind.DEFINES_BINDING, implicitProp); |
| |
| const sym = this.host.getSymbolAtLocation(decl.name); |
| if (!sym) throw new Error('Getter/setter declaration has no symbols.'); |
| |
| if (sym.declarations.find(ts.isGetAccessor)) { |
| // Emit a "property/reads" edge between the getter and the property |
| const getter = |
| this.host.getSymbolName(sym, TSNamespace.VALUE, Context.Getter); |
| this.emitEdge(getter, EdgeKind.PROPERTY_READS, implicitProp); |
| } |
| if (sym.declarations.find(ts.isSetAccessor)) { |
| // Emit a "property/writes" edge between the setter and the property |
| const setter = |
| this.host.getSymbolName(sym, TSNamespace.VALUE, Context.Setter); |
| this.emitEdge(setter, EdgeKind.PROPERTY_WRITES, implicitProp); |
| } |
| } |
| |
| /** |
| * Handles code like: |
| * export default ...; |
| * export = ...; |
| */ |
| visitExportAssignment(assign: ts.ExportAssignment) { |
| if (assign.isExportEquals) { |
| const span = this.getTextSpan(assign, 'export ='); |
| const anchor = this.newAnchor(assign, span.start, span.end); |
| this.emitEdge( |
| anchor, EdgeKind.DEFINES_BINDING, this.host.scopedSignature(assign)); |
| } else { |
| // export default <expr>; |
| // is the same as exporting the expression under the symbol named |
| // "default". But we don't have a nice name to link the symbol to! |
| // So instead we link the keyword "default" itself to the VName. |
| // The TypeScript AST does not expose the location of the 'default' |
| // keyword so we just find it in the source text to link it. |
| const span = this.getTextSpan(assign, 'default'); |
| const anchor = this.newAnchor(assign, span.start, span.end); |
| this.emitEdge( |
| anchor, EdgeKind.DEFINES_BINDING, this.host.scopedSignature(assign)); |
| } |
| } |
| |
| /** |
| * Handles code that explicitly exports a symbol, like: |
| * export {Foo} from './bar'; |
| * |
| * Note that export can also be a modifier on a declaration, like: |
| * export class Foo {} |
| * and that case is handled as part of the ordinary declaration handling. |
| */ |
| visitExportDeclaration(decl: ts.ExportDeclaration) { |
| if (decl.exportClause) { |
| for (const exp of decl.exportClause.elements) { |
| const localSym = this.host.getSymbolAtLocation(exp.name); |
| if (!localSym) { |
| console.error(`TODO: export ${exp.name} has no symbol`); |
| continue; |
| } |
| const remoteSym = this.typeChecker.getAliasedSymbol(localSym); |
| const anchor = this.newAnchor(exp.name); |
| // Aliased export; propertyName is the 'as <...>' bit. |
| const propertyAnchor = exp.propertyName ? |
| this.newAnchor(exp.propertyName) : null; |
| // Symbol is a value. |
| if (remoteSym.flags & ts.SymbolFlags.Value) { |
| const kExport = |
| this.host.getSymbolName(remoteSym, TSNamespace.VALUE); |
| this.emitEdge(anchor, EdgeKind.REF, kExport); |
| if (propertyAnchor) { |
| this.emitEdge(propertyAnchor, EdgeKind.REF, kExport); |
| } |
| } |
| // Symbol is a type. |
| if (remoteSym.flags & ts.SymbolFlags.Type) { |
| const kExport = |
| this.host.getSymbolName(remoteSym, TSNamespace.TYPE); |
| this.emitEdge(anchor, EdgeKind.REF, kExport); |
| if (propertyAnchor) { |
| this.emitEdge(propertyAnchor, EdgeKind.REF, kExport); |
| } |
| } |
| } |
| } |
| if (decl.moduleSpecifier) { |
| const moduleSym = this.host.getSymbolAtLocation(decl.moduleSpecifier); |
| if (moduleSym) { |
| const moduleName = this.getModulePathFromModuleReference(moduleSym); |
| if (moduleName) { |
| const kModule = this.newVName('module', moduleName); |
| this.emitEdge( |
| this.newAnchor(decl.moduleSpecifier), EdgeKind.REF_IMPORTS, |
| kModule); |
| } |
| } |
| } |
| } |
| |
| visitVariableStatement(stmt: ts.VariableStatement) { |
| // A VariableStatement contains potentially multiple variable declarations, |
| // as in: |
| // var x = 3, y = 4; |
| // In the (common) case where there's a single variable declared, we look |
| // for documentation for that variable above the entire statement. |
| if (stmt.declarationList.declarations.length === 1) { |
| const vname = |
| this.visitVariableDeclaration(stmt.declarationList.declarations[0]); |
| if (vname) this.visitJSDoc(stmt, vname); |
| return; |
| } |
| |
| // Otherwise, use default recursion over the statement. |
| ts.forEachChild(stmt, n => this.visit(n)); |
| } |
| |
| /** |
| * Note: visitVariableDeclaration is also used for class properties; |
| * the decl parameter is the union of the attributes of the two types. |
| * @return the generated VName for the declaration, if any. |
| */ |
| visitVariableDeclaration(decl: { |
| name: ts.BindingName|ts.PropertyName, |
| type?: ts.TypeNode, |
| initializer?: ts.Expression, kind: ts.SyntaxKind, |
| }): VName|undefined { |
| let vname: VName|undefined; |
| switch (decl.name.kind) { |
| case ts.SyntaxKind.Identifier: |
| case ts.SyntaxKind.ComputedPropertyName: |
| case ts.SyntaxKind.StringLiteral: |
| case ts.SyntaxKind.NumericLiteral: |
| const sym = this.host.getSymbolAtLocation(decl.name); |
| if (!sym) { |
| todo( |
| this.sourceRoot, decl.name, |
| `declaration ${decl.name.getText()} has no symbol`); |
| return undefined; |
| } |
| vname = this.host.getSymbolName(sym, TSNamespace.VALUE); |
| this.emitNode(vname, 'variable'); |
| |
| this.emitEdge( |
| this.newAnchor(decl.name), EdgeKind.DEFINES_BINDING, vname); |
| |
| decl.name.forEachChild(child => this.visit(child)); |
| break; |
| case ts.SyntaxKind.ObjectBindingPattern: |
| case ts.SyntaxKind.ArrayBindingPattern: |
| for (const element of (decl.name as ts.BindingPattern).elements) { |
| this.visit(element); |
| } |
| break; |
| default: |
| break; |
| } |
| if (decl.type) this.visitType(decl.type); |
| if (decl.initializer) this.visit(decl.initializer); |
| if (vname && decl.kind === ts.SyntaxKind.PropertyDeclaration) { |
| const declNode = decl as ts.PropertyDeclaration; |
| if (isStaticMember(declNode, declNode.parent)) { |
| this.emitFact(vname, FactName.TAG_STATIC, ''); |
| } |
| } |
| return vname; |
| } |
| |
| visitFunctionLikeDeclaration(decl: ts.FunctionLikeDeclaration) { |
| this.visitDecorators(decl.decorators || []); |
| let kFunc: VName; |
| let funcSym: ts.Symbol|undefined = undefined; |
| let context: Context|undefined = undefined; |
| if (ts.isGetAccessor(decl)) { |
| context = Context.Getter; |
| } else if (ts.isSetAccessor(decl)) { |
| context = Context.Setter; |
| } |
| if (decl.name) { |
| funcSym = this.host.getSymbolAtLocation(decl.name); |
| if (decl.name.kind === ts.SyntaxKind.ComputedPropertyName) { |
| this.visit((decl.name as ts.ComputedPropertyName).expression); |
| } |
| if (!funcSym) { |
| todo( |
| this.sourceRoot, decl.name, |
| `function declaration ${decl.name.getText()} has no symbol`); |
| return; |
| } |
| kFunc = this.host.getSymbolName(funcSym, TSNamespace.VALUE, context); |
| |
| const declAnchor = this.newAnchor(decl.name); |
| this.emitNode(kFunc, 'function'); |
| this.emitEdge(declAnchor, EdgeKind.DEFINES_BINDING, kFunc); |
| |
| // Getters/setters also emit an implicit class property entry. If a |
| // getter is present, it will bind this entry; otherwise a setter will. |
| if (ts.isGetAccessor(decl) || |
| (ts.isSetAccessor(decl) && |
| !funcSym.declarations.find(ts.isGetAccessor))) { |
| this.emitImplicitProperty(decl, declAnchor, kFunc); |
| } |
| |
| this.visitJSDoc(decl, kFunc); |
| } else { |
| // TODO: choose VName for anonymous functions. |
| kFunc = this.newVName('TODO', 'TODOPath'); |
| } |
| this.emitEdge(this.newAnchor(decl), EdgeKind.DEFINES, kFunc); |
| |
| if (decl.parent) { |
| // Emit a "childof" edge on class/interface members. |
| if (ts.isClassLike(decl.parent) || |
| ts.isInterfaceDeclaration(decl.parent)) { |
| const parentName = decl.parent.name; |
| if (parentName !== undefined) { |
| const parentSym = this.host.getSymbolAtLocation(parentName); |
| if (!parentSym) { |
| todo( |
| this.sourceRoot, parentName, |
| `parent ${parentName} has no symbol`); |
| return; |
| } |
| const kParent = this.host.getSymbolName(parentSym, TSNamespace.TYPE); |
| this.emitEdge(kFunc, EdgeKind.CHILD_OF, kParent); |
| } |
| |
| // Emit "overrides" edges if this method overrides extended classes or |
| // implemented interfaces, which are listed in the Heritage Clauses of |
| // a class or interface. |
| // class X extends A implements B, C {} |
| // ^^^^^^^^^-^^^^^^^^^^^^^^^----- `HeritageClause`s |
| // Look at each type listed in the heritage clauses and traverse its |
| // members. If the type has a member that matches the method visited in |
| // this function (`kFunc`), emit an "overrides" edge to that member. |
| if (funcSym && decl.parent.heritageClauses) { |
| for (const heritage of decl.parent.heritageClauses) { |
| for (const baseType of heritage.types) { |
| const baseSym = |
| this.host.getSymbolAtLocation(baseType.expression); |
| if (!baseSym || !baseSym.members) { |
| continue; |
| } |
| |
| const funcName = funcSym.name; |
| const funcFlags = funcSym.flags; |
| |
| // Find a member of with the same type (same flags) and same name |
| // as the overriding method. |
| const overridenCondition = (sym: ts.Symbol) => |
| Boolean(sym.flags & funcFlags) && sym.name === funcName; |
| |
| const overriden = |
| toArray(baseSym.members.values()).find(overridenCondition); |
| if (overriden) { |
| this.emitEdge( |
| kFunc, EdgeKind.OVERRIDES, |
| this.host.getSymbolName(overriden, TSNamespace.VALUE)); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| this.visitParameters(decl.parameters, kFunc); |
| |
| if (decl.type) { |
| // "type" here is the return type of the function. |
| this.visitType(decl.type); |
| } |
| |
| if (decl.typeParameters) this.visitTypeParameters(decl.typeParameters); |
| if (decl.body) { |
| this.visit(decl.body); |
| } else { |
| this.emitFact(kFunc, FactName.COMPLETE, 'incomplete'); |
| } |
| } |
| |
| /** |
| * Visits a function parameters, which can be recursive in the case of |
| * parameters created via bound elements: |
| * function foo({a, b: {c, d}}, e, f) {} |
| * In this code, a, c, d, e, f are all parameters with increasing parameter |
| * numbers [0, 4]. |
| */ |
| visitParameters( |
| parameters: ReadonlyArray<ts.ParameterDeclaration>, kFunc: VName) { |
| let paramNum = 0; |
| const recurseVisit = |
| (param: ts.ParameterDeclaration|ts.BindingElement) => { |
| this.visitDecorators(param.decorators || []); |
| |
| switch (param.name.kind) { |
| case ts.SyntaxKind.Identifier: |
| const sym = this.host.getSymbolAtLocation(param.name); |
| if (!sym) { |
| todo( |
| this.sourceRoot, param.name, |
| `param ${param.name.getText()} has no symbol`); |
| return; |
| } |
| const kParam = this.host.getSymbolName(sym, TSNamespace.VALUE); |
| this.emitNode(kParam, 'variable'); |
| |
| this.emitEdge( |
| kFunc, makeOrdinalEdge(EdgeKind.PARAM, paramNum), kParam); |
| ++paramNum; |
| |
| if (isParameterPropertyDeclaration(param, param.parent)) { |
| // Class members defined in the parameters of a constructor are |
| // children of the class type. |
| const parentName = param.parent.parent.name; |
| if (parentName !== undefined) { |
| const parentSym = this.host.getSymbolAtLocation(parentName); |
| if (parentSym !== undefined) { |
| const kClass = |
| this.host.getSymbolName(parentSym, TSNamespace.TYPE); |
| this.emitEdge(kParam, EdgeKind.CHILD_OF, kClass); |
| } |
| } |
| } else { |
| this.emitEdge(kParam, EdgeKind.CHILD_OF, kFunc); |
| } |
| |
| this.emitEdge( |
| this.newAnchor(param.name), EdgeKind.DEFINES_BINDING, kParam); |
| break; |
| case ts.SyntaxKind.ObjectBindingPattern: |
| case ts.SyntaxKind.ArrayBindingPattern: |
| const elements = toArray(param.name.elements.entries()); |
| for (const [index, element] of elements) { |
| if (ts.isBindingElement(element)) { |
| recurseVisit(element); |
| } |
| } |
| break; |
| default: |
| break; |
| } |
| |
| if (ts.isParameter(param) && param.type) this.visitType(param.type); |
| if (param.initializer) this.visit(param.initializer); |
| } |
| |
| for (const element of parameters) { |
| recurseVisit(element); |
| } |
| } |
| |
| visitDecorators(decors: ReadonlyArray<ts.Decorator>) { |
| for (const decor of decors) { |
| this.visit(decor); |
| } |
| } |
| |
| /** |
| * Visits a module declaration, which can look like any of the following: |
| * declare module 'foo'; |
| * declare module 'foo' {} |
| * declare module foo {} |
| * namespace Foo {} |
| */ |
| visitModuleDeclaration(decl: ts.ModuleDeclaration) { |
| let sym = this.host.getSymbolAtLocation(decl.name); |
| if (!sym) { |
| todo( |
| this.sourceRoot, decl.name, |
| `module declaration ${decl.name.getText()} has no symbol`); |
| return; |
| } |
| // A TypeScript module declaration declares both a namespace (a Kythe |
| // "record") and a value (most similar to a "package", which defines a |
| // module with declarations). |
| const kNamespace = this.host.getSymbolName(sym, TSNamespace.NAMESPACE); |
| const kValue = this.host.getSymbolName(sym, TSNamespace.VALUE); |
| // It's possible that same namespace appears multiple time. We need to |
| // emit only single node for that namespace and single defines/binding |
| // edge. |
| if (sym.valueDeclaration === decl) { |
| this.emitNode(kNamespace, 'record'); |
| this.emitSubkind(kNamespace, Subkind.NAMESPACE); |
| this.emitNode(kValue, 'package'); |
| |
| const nameAnchor = this.newAnchor(decl.name); |
| this.emitEdge(nameAnchor, EdgeKind.DEFINES_BINDING, kNamespace); |
| this.emitEdge(nameAnchor, EdgeKind.DEFINES_BINDING, kValue); |
| // If no body then it is incomplete module definition, like declare module |
| // 'foo'; |
| this.emitFact( |
| kNamespace, FactName.COMPLETE, |
| decl.body ? 'definition' : 'incomplete'); |
| } |
| |
| // The entire module declaration defines the created namespace. |
| this.emitEdge(this.newAnchor(decl), EdgeKind.DEFINES, kValue); |
| |
| if (decl.decorators) this.visitDecorators(decl.decorators); |
| if (decl.body) this.visit(decl.body); |
| } |
| |
| visitClassDeclaration(decl: ts.ClassDeclaration) { |
| this.visitDecorators(decl.decorators || []); |
| if (decl.name) { |
| const sym = this.host.getSymbolAtLocation(decl.name); |
| if (!sym) { |
| todo( |
| this.sourceRoot, decl.name, |
| `class ${decl.name.getText()} has no symbol`); |
| return; |
| } |
| // A 'class' declaration declares both a type (a 'record', representing |
| // instances of the class) and a value (least ambigiously, also the |
| // class declaration). |
| const kClass = this.host.getSymbolName(sym, TSNamespace.TYPE); |
| this.emitNode(kClass, 'record'); |
| const kClassCtor = this.host.getSymbolName(sym, TSNamespace.VALUE); |
| this.emitNode(kClassCtor, 'function'); |
| |
| const anchor = this.newAnchor(decl.name); |
| this.emitEdge(anchor, EdgeKind.DEFINES_BINDING, kClass); |
| this.emitEdge(anchor, EdgeKind.DEFINES_BINDING, kClassCtor); |
| |
| // If the class has a constructor, emit an entry for it. |
| const ctorSymbol = this.getCtorSymbol(decl); |
| if (ctorSymbol) { |
| const ctorDecl = ctorSymbol.declarations[0]; |
| const span = this.getTextSpan(ctorDecl, 'constructor'); |
| const classCtorAnchor = this.newAnchor(ctorDecl, span.start, span.end); |
| |
| const ctorVName = |
| this.host.getSymbolName(ctorSymbol, TSNamespace.VALUE); |
| |
| this.emitNode(ctorVName, 'function'); |
| this.emitSubkind(ctorVName, Subkind.CONSTRUCTOR); |
| this.emitEdge(classCtorAnchor, EdgeKind.DEFINES_BINDING, ctorVName); |
| } |
| |
| this.visitJSDoc(decl, kClass); |
| } |
| if (decl.typeParameters) this.visitTypeParameters(decl.typeParameters); |
| if (decl.heritageClauses) this.visitHeritage(decl.heritageClauses); |
| for (const member of decl.members) { |
| this.visit(member); |
| } |
| } |
| |
| visitEnumDeclaration(decl: ts.EnumDeclaration) { |
| const sym = this.host.getSymbolAtLocation(decl.name); |
| if (!sym) return; |
| const kType = this.host.getSymbolName(sym, TSNamespace.TYPE); |
| this.emitNode(kType, 'record'); |
| const kValue = this.host.getSymbolName(sym, TSNamespace.VALUE); |
| this.emitNode(kValue, 'constant'); |
| |
| const anchor = this.newAnchor(decl.name); |
| this.emitEdge(anchor, EdgeKind.DEFINES_BINDING, kType); |
| this.emitEdge(anchor, EdgeKind.DEFINES_BINDING, kValue); |
| for (const member of decl.members) { |
| this.visit(member); |
| } |
| } |
| |
| visitEnumMember(decl: ts.EnumMember) { |
| const sym = this.host.getSymbolAtLocation(decl.name); |
| if (!sym) return; |
| const kMember = this.host.getSymbolName(sym, TSNamespace.VALUE); |
| this.emitNode(kMember, 'constant'); |
| this.emitEdge(this.newAnchor(decl.name), EdgeKind.DEFINES_BINDING, kMember); |
| } |
| |
| visitExpressionMember(node: ts.Node) { |
| const sym = this.host.getSymbolAtLocation(node); |
| if (!sym) { |
| // E.g. a field of an "any". |
| return; |
| } |
| if (!sym.declarations || sym.declarations.length === 0) { |
| // An undeclared symbol, e.g. "undefined". |
| return; |
| } |
| const name = this.host.getSymbolName(sym, TSNamespace.VALUE); |
| const anchor = this.newAnchor(node); |
| this.emitEdge(anchor, EdgeKind.REF, name); |
| |
| // Emit a 'ref/call' edge to a class constructor if a new class instance |
| // is instantiated. |
| if (ts.isNewExpression(node.parent)) { |
| const classDecl = sym.declarations.find(ts.isClassDeclaration); |
| if (classDecl) { |
| const ctorSymbol = this.getCtorSymbol(classDecl); |
| if (ctorSymbol) { |
| const ctorVName = |
| this.host.getSymbolName(ctorSymbol, TSNamespace.VALUE); |
| this.emitEdge(anchor, EdgeKind.REF_CALL, ctorVName); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Emits a reference from a "this" keyword to the type of the "this" object. |
| */ |
| visitThisKeyword(keyword: ts.ThisExpression) { |
| const sym = this.host.getSymbolAtLocation(keyword); |
| if (!sym) { |
| // "this" refers to an object with no particular type, e.g. |
| // let obj = { |
| // foo() { this.foo(); } |
| // }; |
| return; |
| } |
| if (!sym.declarations || sym.declarations.length === 0) { |
| // "this" keyword is `globalThis`, which has no declarations. |
| return; |
| } |
| |
| const type = this.host.getSymbolName(sym, TSNamespace.TYPE); |
| const thisAnchor = this.newAnchor(keyword); |
| this.emitEdge(thisAnchor, EdgeKind.REF, type); |
| } |
| |
| /** |
| * visitJSDoc attempts to attach a 'doc' node to a given target, by looking |
| * for JSDoc comments. |
| */ |
| visitJSDoc(node: ts.Node, target: VName) { |
| const text = node.getFullText(); |
| const comments = ts.getLeadingCommentRanges(text, 0); |
| if (!comments) return; |
| |
| let jsdoc: string|undefined; |
| for (const commentRange of comments) { |
| if (commentRange.kind !== ts.SyntaxKind.MultiLineCommentTrivia) continue; |
| const comment = |
| text.substring(commentRange.pos + 2, commentRange.end - 2); |
| if (!comment.startsWith('*')) { |
| // Not a JSDoc comment. |
| continue; |
| } |
| // Strip the ' * ' bits that start lines within the comment. |
| jsdoc = comment.replace(/^[ \t]*\* ?/mg, ''); |
| break; |
| } |
| if (jsdoc === undefined) return; |
| |
| // Strip leading and trailing whitespace. |
| jsdoc = jsdoc.replace(/^\s+/, '').replace(/\s+$/, ''); |
| const doc = this.newVName(target.signature + '#doc', target.path); |
| this.emitNode(doc, 'doc'); |
| this.emitEdge(doc, EdgeKind.DOCUMENTS, target); |
| this.emitFact(doc, FactName.TEXT, jsdoc); |
| } |
| |
| /** visit is the main dispatch for visiting AST nodes. */ |
| visit(node: ts.Node): void { |
| switch (node.kind) { |
| case ts.SyntaxKind.ImportDeclaration: |
| case ts.SyntaxKind.ImportEqualsDeclaration: |
| return this.visitImportDeclaration( |
| node as ts.ImportDeclaration | ts.ImportEqualsDeclaration); |
| case ts.SyntaxKind.ExportAssignment: |
| return this.visitExportAssignment(node as ts.ExportAssignment); |
| case ts.SyntaxKind.ExportDeclaration: |
| return this.visitExportDeclaration(node as ts.ExportDeclaration); |
| case ts.SyntaxKind.VariableStatement: |
| return this.visitVariableStatement(node as ts.VariableStatement); |
| case ts.SyntaxKind.VariableDeclaration: |
| this.visitVariableDeclaration(node as ts.VariableDeclaration); |
| return; |
| case ts.SyntaxKind.PropertyAssignment: // property in object literal |
| case ts.SyntaxKind.PropertyDeclaration: |
| case ts.SyntaxKind.PropertySignature: |
| case ts.SyntaxKind.ShorthandPropertyAssignment: |
| const vname = |
| this.visitVariableDeclaration(node as ts.PropertyDeclaration); |
| if (vname) this.visitJSDoc(node, vname); |
| return; |
| case ts.SyntaxKind.ArrowFunction: |
| case ts.SyntaxKind.Constructor: |
| case ts.SyntaxKind.FunctionDeclaration: |
| case ts.SyntaxKind.MethodDeclaration: |
| case ts.SyntaxKind.MethodSignature: |
| case ts.SyntaxKind.GetAccessor: |
| case ts.SyntaxKind.SetAccessor: |
| return this.visitFunctionLikeDeclaration( |
| node as ts.FunctionLikeDeclaration); |
| case ts.SyntaxKind.ClassDeclaration: |
| return this.visitClassDeclaration(node as ts.ClassDeclaration); |
| case ts.SyntaxKind.InterfaceDeclaration: |
| return this.visitInterfaceDeclaration(node as ts.InterfaceDeclaration); |
| case ts.SyntaxKind.TypeAliasDeclaration: |
| return this.visitTypeAliasDeclaration(node as ts.TypeAliasDeclaration); |
| case ts.SyntaxKind.EnumDeclaration: |
| return this.visitEnumDeclaration(node as ts.EnumDeclaration); |
| case ts.SyntaxKind.EnumMember: |
| return this.visitEnumMember(node as ts.EnumMember); |
| case ts.SyntaxKind.TypeReference: |
| this.visitType(node as ts.TypeNode); |
| return; |
| case ts.SyntaxKind.BindingElement: |
| this.visitVariableDeclaration(node as ts.BindingElement); |
| return; |
| case ts.SyntaxKind.JsxAttribute: |
| this.visitVariableDeclaration(node as ts.JsxAttribute); |
| return; |
| case ts.SyntaxKind.Identifier: |
| case ts.SyntaxKind.StringLiteral: |
| case ts.SyntaxKind.NumericLiteral: |
| // Assume that this identifer is occurring as part of an |
| // expression; we handle identifiers that occur in other |
| // circumstances (e.g. in a type) separately in visitType. |
| this.visitExpressionMember(node); |
| return; |
| case ts.SyntaxKind.ThisKeyword: |
| return this.visitThisKeyword(node as ts.ThisExpression); |
| case ts.SyntaxKind.ModuleDeclaration: |
| return this.visitModuleDeclaration(node as ts.ModuleDeclaration); |
| default: |
| // Use default recursive processing. |
| return ts.forEachChild(node, n => this.visit(n)); |
| } |
| } |
| |
| /** index is the main entry point, starting the recursive visit. */ |
| index() { |
| this.emitFact(this.kFile, FactName.NODE_KIND, 'file'); |
| this.emitFact(this.kFile, FactName.TEXT, this.file.text); |
| |
| this.emitModuleAnchor(this.file); |
| |
| ts.forEachChild(this.file, n => this.visit(n)); |
| } |
| } |
| |
| /** |
| * index indexes a TypeScript program, producing Kythe JSON objects for the |
| * source files in the specified paths. |
| * |
| * (A ts.Program is a configured collection of parsed source files, but |
| * the caller must specify the source files within the program that they want |
| * Kythe output for, because e.g. the standard library is contained within |
| * the Program and we only want to process it once.) |
| * |
| * @param compilationUnit A VName for the entire compilation, containing e.g. |
| * corpus name. |
| * @param pathVNames A map of file path to path-specific VName. |
| * @param emit If provided, a function that receives objects as they are |
| * emitted; otherwise, they are printed to stdout. |
| * @param plugins If provided, a list of plugin indexers to run after the |
| * TypeScript program has been indexed. |
| * @param readFile If provided, a function that reads a file as bytes to a |
| * Node Buffer. It'd be nice to just reuse program.getSourceFile but |
| * unfortunately that returns a (Unicode) string and we need to get at |
| * each file's raw bytes for UTF-8<->UTF-16 conversions. |
| */ |
| export function index( |
| vname: VName, pathVNames: Map<string, VName>, paths: string[], |
| program: ts.Program, emit?: (obj: {}) => void, plugins?: Plugin[], |
| readFile: (path: string) => Buffer = fs.readFileSync): ts.Diagnostic[] { |
| // Note: we only call getPreEmitDiagnostics (which causes type checking to |
| // happen) on the input paths as provided in paths. This means we don't |
| // e.g. type-check the standard library unless we were explicitly told to. |
| const diags: ts.Diagnostic[] = []; |
| for (const path of paths) { |
| for (const diag of ts.getPreEmitDiagnostics( |
| program, program.getSourceFile(path))) { |
| diags.push(diag); |
| } |
| } |
| // Note: don't abort if there are diagnostics. This allows us to |
| // index programs with errors. We return these diagnostics at the end |
| // so the caller can act on them if it wants. |
| |
| const indexingContext = |
| new StandardIndexerContext(vname, pathVNames, paths, program, readFile); |
| if (emit != null) { |
| indexingContext.emit = emit; |
| } |
| |
| for (const path of paths) { |
| const sourceFile = program.getSourceFile(path); |
| if (!sourceFile) { |
| throw new Error(`requested indexing ${path} not found in program`); |
| } |
| const visitor = new Visitor( |
| indexingContext, |
| sourceFile, |
| ); |
| visitor.index(); |
| } |
| |
| if (plugins) { |
| for (const plugin of plugins) { |
| try { |
| plugin.index(indexingContext); |
| } catch (err) { |
| console.error(`Plugin ${plugin.name} errored: ${err}`); |
| } |
| } |
| } |
| |
| return diags; |
| } |
| |
| /** |
| * loadTsConfig loads a tsconfig.json from a path, throwing on any errors |
| * like "file not found" or parse errors. |
| */ |
| export function loadTsConfig( |
| tsconfigPath: string, projectPath: string, |
| host: ts.ParseConfigHost = ts.sys): ts.ParsedCommandLine { |
| projectPath = path.resolve(projectPath); |
| const {config: json, error} = ts.readConfigFile(tsconfigPath, host.readFile); |
| if (error) { |
| throw new Error(ts.formatDiagnostics([error], ts.createCompilerHost({}))); |
| } |
| const config = ts.parseJsonConfigFileContent(json, host, projectPath); |
| if (config.errors.length > 0) { |
| throw new Error( |
| ts.formatDiagnostics(config.errors, ts.createCompilerHost({}))); |
| } |
| return config; |
| } |
| |
| function main(argv: string[]) { |
| if (argv.length < 1) { |
| console.error('usage: indexer path/to/tsconfig.json [PATH...]'); |
| return 1; |
| } |
| |
| const config = loadTsConfig(argv[0], path.dirname(argv[0])); |
| let inPaths = argv.slice(1); |
| if (inPaths.length === 0) { |
| inPaths = config.fileNames; |
| } |
| |
| // This program merely demonstrates the API, so use a fake corpus/root/etc. |
| const compilationUnit: VName = { |
| corpus: 'corpus', |
| root: '', |
| path: '', |
| signature: '', |
| language: '', |
| }; |
| const program = ts.createProgram(inPaths, config.options); |
| index(compilationUnit, new Map(), inPaths, program); |
| return 0; |
| } |
| |
| if (require.main === module) { |
| // Note: do not use process.exit(), because that does not ensure that |
| // process.stdout has been flushed(!). |
| process.exitCode = main(process.argv.slice(2)); |
| } |