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 => { 33 resChildren.unshift(c); 34 }); 35 work.push(...resChildren); 36 } 37 } 38 39 export class TypeWriterWalker { 40 currentSourceFile!: ts.SourceFile; 41 42 private checker: ts.TypeChecker; 43 44 constructor(private program: ts.Program, private hadErrorBaseline: boolean) { 45 // Consider getting both the diagnostics checker and the non-diagnostics checker to verify 46 // they are consistent. 47 this.checker = 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 // Distinguish `errorType`s from `any`s; but only if the file has no errors. 119 // Additionally, 120 // * the LHS of a qualified name 121 // * a binding pattern name 122 // * labels 123 // * the "global" in "declare global" 124 // * the "target" in "new.target" 125 // * names in import statements 126 // * type-only names in export statements 127 // * and intrinsic jsx tag names 128 // return `error`s via `getTypeAtLocation` 129 // But this is generally expected, so we don't call those out, either 130 let typeString: string; 131 if (!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 typeString = (type as ts.IntrinsicType).intrinsicName; 142 } 143 else { 144 typeString = this.checker.typeToString(type, node.parent, ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.AllowUniqueESSymbolType); 145 if (ts.isIdentifier(node) && ts.isTypeAliasDeclaration(node.parent) && node.parent.name === node && typeString === ts.idText(node)) { 146 // 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 147 // the name of the type alias, recreate the type string without reusing the alias name 148 typeString = this.checker.typeToString(type, node.parent, ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.AllowUniqueESSymbolType | ts.TypeFormatFlags.InTypeAlias); 149 } 150 } 151 return { 152 line: lineAndCharacter.line, 153 syntaxKind: node.kind, 154 sourceText, 155 type: typeString 156 }; 157 } 158 const symbol = this.checker.getSymbolAtLocation(node); 159 if (!symbol) { 160 return; 161 } 162 let symbolString = "Symbol(" + this.checker.symbolToString(symbol, node.parent); 163 if (symbol.declarations) { 164 let count = 0; 165 for (const declaration of symbol.declarations) { 166 if (count >= 5) { 167 symbolString += ` ... and ${symbol.declarations.length - count} more`; 168 break; 169 } 170 count++; 171 symbolString += ", "; 172 if ((declaration as any).__symbolTestOutputCache) { 173 symbolString += (declaration as any).__symbolTestOutputCache; 174 continue; 175 } 176 const declSourceFile = declaration.getSourceFile(); 177 const declLineAndCharacter = declSourceFile.getLineAndCharacterOfPosition(declaration.pos); 178 const fileName = ts.getBaseFileName(declSourceFile.fileName); 179 const isLibFile = /lib(.*)\.d\.ts/i.test(fileName); 180 const declText = `Decl(${ fileName }, ${ isLibFile ? "--" : declLineAndCharacter.line }, ${ isLibFile ? "--" : declLineAndCharacter.character })`; 181 symbolString += declText; 182 (declaration as any).__symbolTestOutputCache = declText; 183 } 184 } 185 symbolString += ")"; 186 return { 187 line: lineAndCharacter.line, 188 syntaxKind: node.kind, 189 sourceText, 190 symbol: symbolString 191 }; 192 } 193 } 194}