• 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 => { 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}