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