• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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}