1import * as ts from "./_namespaces/ts"; 2 3export interface TypeWriterTypeResult { 4 line: number; 5 syntaxKind: number; 6 sourceText: string; 7 type: string; 8} 9 10export interface TypeWriterSymbolResult { 11 line: number; 12 syntaxKind: number; 13 sourceText: string; 14 symbol: string; 15} 16 17export interface TypeWriterResult { 18 line: number; 19 syntaxKind: number; 20 sourceText: string; 21 symbol?: string; 22 type?: string; 23} 24 25function* forEachASTNode(node: ts.Node) { 26 const work = [node]; 27 while (work.length) { 28 const elem = work.pop()!; 29 yield elem; 30 31 const resChildren: ts.Node[] = []; 32 // push onto work queue in reverse order to maintain preorder traversal 33 ts.forEachChild(elem, c => { 34 resChildren.unshift(c); 35 }); 36 work.push(...resChildren); 37 } 38} 39 40export class TypeWriterWalker { 41 currentSourceFile!: ts.SourceFile; 42 43 private checker: ts.TypeChecker; 44 45 constructor(private program: ts.Program, private hadErrorBaseline: boolean) { 46 // Consider getting both the diagnostics checker and the non-diagnostics checker to verify 47 // they are consistent. 48 this.checker = program.getTypeChecker(); 49 } 50 51 public *getSymbols(fileName: string): IterableIterator<TypeWriterSymbolResult> { 52 const sourceFile = this.program.getSourceFile(fileName)!; 53 this.currentSourceFile = sourceFile; 54 const gen = this.visitNode(sourceFile, /*isSymbolWalk*/ true); 55 for (let {done, value} = gen.next(); !done; { done, value } = gen.next()) { 56 yield value as TypeWriterSymbolResult; 57 } 58 } 59 60 public *getTypes(fileName: string): IterableIterator<TypeWriterTypeResult> { 61 const sourceFile = this.program.getSourceFile(fileName)!; 62 this.currentSourceFile = sourceFile; 63 const gen = this.visitNode(sourceFile, /*isSymbolWalk*/ false); 64 for (let {done, value} = gen.next(); !done; { done, value } = gen.next()) { 65 yield value as TypeWriterTypeResult; 66 } 67 } 68 69 private *visitNode(node: ts.Node, isSymbolWalk: boolean): IterableIterator<TypeWriterResult> { 70 const gen = forEachASTNode(node); 71 let res = gen.next(); 72 for (; !res.done; res = gen.next()) { 73 const {value: node} = res; 74 if (ts.isExpressionNode(node) || node.kind === ts.SyntaxKind.Identifier || ts.isDeclarationName(node)) { 75 const result = this.writeTypeOrSymbol(node, isSymbolWalk); 76 if (result) { 77 yield result; 78 } 79 } 80 } 81 } 82 83 private isImportStatementName(node: ts.Node) { 84 if (ts.isImportSpecifier(node.parent) && (node.parent.name === node || node.parent.propertyName === node)) return true; 85 if (ts.isImportClause(node.parent) && node.parent.name === node) return true; 86 if (ts.isImportEqualsDeclaration(node.parent) && node.parent.name === node) return true; 87 return false; 88 } 89 90 private isExportStatementName(node: ts.Node) { 91 if (ts.isExportAssignment(node.parent) && node.parent.expression === node) return true; 92 if (ts.isExportSpecifier(node.parent) && (node.parent.name === node || node.parent.propertyName === node)) return true; 93 return false; 94 } 95 96 private isIntrinsicJsxTag(node: ts.Node) { 97 const p = node.parent; 98 if (!(ts.isJsxOpeningElement(p) || ts.isJsxClosingElement(p) || ts.isJsxSelfClosingElement(p))) return false; 99 if (p.tagName !== node) return false; 100 return ts.isIntrinsicJsxName(node.getText()); 101 } 102 103 private writeTypeOrSymbol(node: ts.Node, isSymbolWalk: boolean): TypeWriterResult | undefined { 104 const actualPos = ts.skipTrivia(this.currentSourceFile.text, node.pos); 105 const lineAndCharacter = this.currentSourceFile.getLineAndCharacterOfPosition(actualPos); 106 const sourceText = ts.getSourceTextOfNodeFromSourceFile(this.currentSourceFile, node); 107 108 if (!isSymbolWalk) { 109 // Don't try to get the type of something that's already a type. 110 // Exception for `T` in `type T = something` because that may evaluate to some interesting type. 111 if (ts.isPartOfTypeNode(node) || ts.isIdentifier(node) && !(ts.getMeaningFromDeclaration(node.parent) & ts.SemanticMeaning.Value) && !(ts.isTypeAliasDeclaration(node.parent) && node.parent.name === node)) { 112 return undefined; 113 } 114 115 // Workaround to ensure we output 'C' instead of 'typeof C' for base class expressions 116 // let type = this.checker.getTypeAtLocation(node); 117 let type = ts.isExpressionWithTypeArgumentsInClassExtendsClause(node.parent) ? this.checker.getTypeAtLocation(node.parent) : undefined; 118 if (!type || type.flags & ts.TypeFlags.Any) type = this.checker.getTypeAtLocation(node); 119 // Distinguish `errorType`s from `any`s; but only if the file has no errors. 120 // Additionally, 121 // * the LHS of a qualified name 122 // * a binding pattern name 123 // * labels 124 // * the "global" in "declare global" 125 // * the "target" in "new.target" 126 // * names in import statements 127 // * type-only names in export statements 128 // * and intrinsic jsx tag names 129 // return `error`s via `getTypeAtLocation` 130 // But this is generally expected, so we don't call those out, either 131 let typeString: string; 132 if (!this.hadErrorBaseline && 133 type.flags & ts.TypeFlags.Any && 134 !ts.isBindingElement(node.parent) && 135 !ts.isPropertyAccessOrQualifiedName(node.parent) && 136 !ts.isLabelName(node) && 137 !(ts.isModuleDeclaration(node.parent) && ts.isGlobalScopeAugmentation(node.parent)) && 138 !ts.isMetaProperty(node.parent) && 139 !this.isImportStatementName(node) && 140 !this.isExportStatementName(node) && 141 !this.isIntrinsicJsxTag(node)) { 142 typeString = (type as ts.IntrinsicType).intrinsicName; 143 } 144 else { 145 typeString = this.checker.typeToString(type, node.parent, ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.AllowUniqueESSymbolType); 146 if (ts.isIdentifier(node) && ts.isTypeAliasDeclaration(node.parent) && node.parent.name === node && typeString === ts.idText(node)) { 147 // for a complex type alias `type T = ...`, showing "T : T" isn't very helpful for type tests. When the type produced is the same as 148 // the name of the type alias, recreate the type string without reusing the alias name 149 typeString = this.checker.typeToString(type, node.parent, ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.AllowUniqueESSymbolType | ts.TypeFormatFlags.InTypeAlias); 150 } 151 } 152 return { 153 line: lineAndCharacter.line, 154 syntaxKind: node.kind, 155 sourceText, 156 type: typeString 157 }; 158 } 159 const symbol = this.checker.getSymbolAtLocation(node); 160 if (!symbol) { 161 return; 162 } 163 let symbolString = "Symbol(" + this.checker.symbolToString(symbol, node.parent); 164 if (symbol.declarations) { 165 let count = 0; 166 for (const declaration of symbol.declarations) { 167 if (count >= 5) { 168 symbolString += ` ... and ${symbol.declarations.length - count} more`; 169 break; 170 } 171 count++; 172 symbolString += ", "; 173 if ((declaration as any).__symbolTestOutputCache) { 174 symbolString += (declaration as any).__symbolTestOutputCache; 175 continue; 176 } 177 const declSourceFile = declaration.getSourceFile(); 178 const declLineAndCharacter = declSourceFile.getLineAndCharacterOfPosition(declaration.pos); 179 const fileName = ts.getBaseFileName(declSourceFile.fileName); 180 const isLibFile = /lib(.*)\.d\.ts/i.test(fileName); 181 const declText = `Decl(${ fileName }, ${ isLibFile ? "--" : declLineAndCharacter.line }, ${ isLibFile ? "--" : declLineAndCharacter.character })`; 182 symbolString += declText; 183 (declaration as any).__symbolTestOutputCache = declText; 184 } 185 } 186 symbolString += ")"; 187 return { 188 line: lineAndCharacter.line, 189 syntaxKind: node.kind, 190 sourceText, 191 symbol: symbolString 192 }; 193 } 194}