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