TypeScript/src/harness/typeWriter.ts
2024-08-13 14:16:53 -07:00

339 lines
14 KiB
TypeScript

import * as ts from "./_namespaces/ts.js";
import {
createPrinter,
createTextWriter,
memoize,
} from "./_namespaces/ts.js";
export interface TypeWriterTypeResult {
line: number;
syntaxKind: number;
sourceText: string;
type: string;
underline?: string;
}
export interface TypeWriterSymbolResult {
line: number;
syntaxKind: number;
sourceText: string;
symbol: string;
}
export interface TypeWriterResult {
line: number;
syntaxKind: number;
sourceText: string;
symbol?: string;
type?: string;
underline?: string;
}
function* forEachASTNode(node: ts.Node) {
const work = [node];
while (work.length) {
const elem = work.pop()!;
yield elem;
const resChildren: ts.Node[] = [];
// push onto work queue in reverse order to maintain preorder traversal
ts.forEachChild(elem, c => {
resChildren.unshift(c);
});
work.push(...resChildren);
}
}
function nodeIsFullySynthetic(node: ts.Node) {
return ts.nodeIsSynthesized(node) && !ts.getParseTreeNode(node);
}
const createSyntheticNodeUnderliningPrinter = memoize((): { printer: ts.Printer; writer: ts.EmitTextWriter; underliner: ts.EmitTextWriter; reset(): void; } => {
let underlining = false;
const printer = createPrinter({ removeComments: true }, {
onEmitNode: (hint, node, cb) => {
if (nodeIsFullySynthetic(node) !== underlining) {
// either node is synthetic and underlining needs to be enabled, or node is not synthetic and
// underlining needs to be disabled
underlining = !underlining;
const result = cb(hint, node);
underlining = !underlining;
return result;
}
// underlining does not need to change
return cb(hint, node);
},
});
const baseWriter = createTextWriter("");
const underliner = createTextWriter("");
return {
printer,
writer: {
write(s: string): void {
baseWriter.write(s);
underliner.write(underlineFor(s));
},
writeTrailingSemicolon(text: string): void {
baseWriter.writeTrailingSemicolon(text);
underliner.writeTrailingSemicolon(underlineFor(text));
},
writeComment(text: string): void {
baseWriter.writeComment(text);
underliner.writeComment(underlineFor(text));
},
getText(): string {
return baseWriter.getText();
},
rawWrite(s: string): void {
baseWriter.rawWrite(s);
underliner.rawWrite(underlineFor(s));
},
writeLiteral(s: string): void {
baseWriter.writeLiteral(s);
underliner.writeLiteral(underlineFor(s));
},
getTextPos(): number {
return baseWriter.getTextPos();
},
getLine(): number {
return baseWriter.getLine();
},
getColumn(): number {
return baseWriter.getColumn();
},
getIndent(): number {
return baseWriter.getIndent();
},
isAtStartOfLine(): boolean {
return baseWriter.isAtStartOfLine();
},
hasTrailingComment(): boolean {
return baseWriter.hasTrailingComment();
},
hasTrailingWhitespace(): boolean {
return baseWriter.hasTrailingWhitespace();
},
writeKeyword(text: string): void {
baseWriter.writeKeyword(text);
underliner.writeKeyword(underlineFor(text));
},
writeOperator(text: string): void {
baseWriter.writeOperator(text);
underliner.writeOperator(underlineFor(text));
},
writePunctuation(text: string): void {
baseWriter.writePunctuation(text);
underliner.writePunctuation(underlineFor(text));
},
writeSpace(text: string): void {
baseWriter.writeSpace(text);
underliner.writeSpace(underlineFor(text));
},
writeStringLiteral(text: string): void {
baseWriter.writeStringLiteral(text);
underliner.writeStringLiteral(underlineFor(text));
},
writeParameter(text: string): void {
baseWriter.writeParameter(text);
underliner.writeParameter(underlineFor(text));
},
writeProperty(text: string): void {
baseWriter.writeProperty(text);
underliner.writeProperty(underlineFor(text));
},
writeSymbol(text: string, symbol: ts.Symbol): void {
baseWriter.writeSymbol(text, symbol);
underliner.writeSymbol(underlineFor(text), symbol);
},
writeLine(force?: boolean | undefined): void {
baseWriter.writeLine(force);
underliner.writeLine(force);
},
increaseIndent(): void {
baseWriter.increaseIndent();
underliner.increaseIndent();
},
decreaseIndent(): void {
baseWriter.decreaseIndent();
underliner.decreaseIndent();
},
clear(): void {
baseWriter.clear();
underliner.clear();
},
},
underliner,
reset() {
underlining = false;
baseWriter.clear();
underliner.clear();
},
};
function underlineFor(s: string) {
return s.length === 0 ? s : (underlining ? "^" : " ").repeat(s.length);
}
});
export class TypeWriterWalker {
currentSourceFile!: ts.SourceFile;
private checker: ts.TypeChecker;
constructor(private program: ts.Program, private hadErrorBaseline: boolean) {
// Consider getting both the diagnostics checker and the non-diagnostics checker to verify
// they are consistent.
this.checker = program.getTypeChecker();
}
public *getSymbols(fileName: string): IterableIterator<TypeWriterSymbolResult> {
const sourceFile = this.program.getSourceFile(fileName)!;
this.currentSourceFile = sourceFile;
const gen = this.visitNode(sourceFile, /*isSymbolWalk*/ true);
yield* gen as IterableIterator<TypeWriterSymbolResult>;
}
public *getTypes(fileName: string): IterableIterator<TypeWriterTypeResult> {
const sourceFile = this.program.getSourceFile(fileName)!;
this.currentSourceFile = sourceFile;
const gen = this.visitNode(sourceFile, /*isSymbolWalk*/ false);
yield* gen as IterableIterator<TypeWriterTypeResult>;
}
private *visitNode(node: ts.Node, isSymbolWalk: boolean): IterableIterator<TypeWriterResult> {
const gen = forEachASTNode(node);
for (const node of gen) {
if (ts.isExpressionNode(node) || node.kind === ts.SyntaxKind.Identifier || ts.isDeclarationName(node)) {
const result = this.writeTypeOrSymbol(node, isSymbolWalk);
if (result) {
yield result;
}
}
}
}
private isImportStatementName(node: ts.Node) {
if (ts.isImportSpecifier(node.parent) && (node.parent.name === node || node.parent.propertyName === node)) return true;
if (ts.isImportClause(node.parent) && node.parent.name === node) return true;
if (ts.isImportEqualsDeclaration(node.parent) && node.parent.name === node) return true;
return false;
}
private isExportStatementName(node: ts.Node) {
if (ts.isExportAssignment(node.parent) && node.parent.expression === node) return true;
if (ts.isExportSpecifier(node.parent) && (node.parent.name === node || node.parent.propertyName === node)) return true;
return false;
}
private isIntrinsicJsxTag(node: ts.Node) {
const p = node.parent;
if (!(ts.isJsxOpeningElement(p) || ts.isJsxClosingElement(p) || ts.isJsxSelfClosingElement(p))) return false;
if (p.tagName !== node) return false;
return ts.isIntrinsicJsxName(node.getText());
}
private writeTypeOrSymbol(node: ts.Node, isSymbolWalk: boolean): TypeWriterResult | undefined {
const actualPos = ts.skipTrivia(this.currentSourceFile.text, node.pos);
const lineAndCharacter = this.currentSourceFile.getLineAndCharacterOfPosition(actualPos);
const sourceText = ts.getSourceTextOfNodeFromSourceFile(this.currentSourceFile, node);
if (!isSymbolWalk) {
// Don't try to get the type of something that's already a type.
// Exception for `T` in `type T = something` because that may evaluate to some interesting type.
if (ts.isPartOfTypeNode(node) || ts.isIdentifier(node) && !(ts.getMeaningFromDeclaration(node.parent) & ts.SemanticMeaning.Value) && !(ts.isTypeAliasDeclaration(node.parent) && node.parent.name === node)) {
return undefined;
}
// Workaround to ensure we output 'C' instead of 'typeof C' for base class expressions
// let type = this.checker.getTypeAtLocation(node);
let type = ts.isExpressionWithTypeArgumentsInClassExtendsClause(node.parent) ? this.checker.getTypeAtLocation(node.parent) : undefined;
if (!type || type.flags & ts.TypeFlags.Any) type = this.checker.getTypeAtLocation(node);
// Distinguish `errorType`s from `any`s; but only if the file has no errors.
// Additionally,
// * the LHS of a qualified name
// * a binding pattern name
// * labels
// * the "global" in "declare global"
// * the "target" in "new.target"
// * names in import statements
// * type-only names in export statements
// * and intrinsic jsx tag names
// return `error`s via `getTypeAtLocation`
// But this is generally expected, so we don't call those out, either
let typeString: string;
let underline: string | undefined;
if (
!this.hadErrorBaseline &&
type.flags & ts.TypeFlags.Any &&
!ts.isBindingElement(node.parent) &&
!ts.isPropertyAccessOrQualifiedName(node.parent) &&
!ts.isLabelName(node) &&
!(ts.isModuleDeclaration(node.parent) && ts.isGlobalScopeAugmentation(node.parent)) &&
!ts.isMetaProperty(node.parent) &&
!this.isImportStatementName(node) &&
!this.isExportStatementName(node) &&
!this.isIntrinsicJsxTag(node)
) {
typeString = (type as ts.IntrinsicType).intrinsicName;
}
else {
const typeFormatFlags = ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.AllowUniqueESSymbolType | ts.TypeFormatFlags.GenerateNamesForShadowedTypeParams;
let typeNode = this.checker.typeToTypeNode(type, node.parent, (typeFormatFlags & ts.TypeFormatFlags.NodeBuilderFlagsMask) | ts.NodeBuilderFlags.IgnoreErrors, ts.InternalNodeBuilderFlags.AllowUnresolvedNames)!;
if (ts.isIdentifier(node) && ts.isTypeAliasDeclaration(node.parent) && node.parent.name === node && ts.isIdentifier(typeNode) && ts.idText(typeNode) === ts.idText(node)) {
// 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
// the name of the type alias, recreate the type string without reusing the alias name
typeNode = this.checker.typeToTypeNode(type, node.parent, ((typeFormatFlags | ts.TypeFormatFlags.InTypeAlias) & ts.TypeFormatFlags.NodeBuilderFlagsMask) | ts.NodeBuilderFlags.IgnoreErrors)!;
}
const { printer, writer, underliner, reset } = createSyntheticNodeUnderliningPrinter();
printer.writeNode(ts.EmitHint.Unspecified, typeNode, this.currentSourceFile, writer);
typeString = writer.getText();
underline = underliner.getText();
reset();
}
return {
line: lineAndCharacter.line,
syntaxKind: node.kind,
sourceText,
type: typeString,
underline,
};
}
const symbol = this.checker.getSymbolAtLocation(node);
if (!symbol) {
return;
}
let symbolString = "Symbol(" + this.checker.symbolToString(symbol, node.parent);
if (symbol.declarations) {
let count = 0;
for (const declaration of symbol.declarations) {
if (count >= 5) {
symbolString += ` ... and ${symbol.declarations.length - count} more`;
break;
}
count++;
symbolString += ", ";
if ((declaration as any).__symbolTestOutputCache) {
symbolString += (declaration as any).__symbolTestOutputCache;
continue;
}
const declSourceFile = declaration.getSourceFile();
const declLineAndCharacter = declSourceFile.getLineAndCharacterOfPosition(declaration.pos);
const fileName = ts.getBaseFileName(declSourceFile.fileName);
const isLibFile = /lib.*\.d\.ts/i.test(fileName);
const declText = `Decl(${fileName}, ${isLibFile ? "--" : declLineAndCharacter.line}, ${isLibFile ? "--" : declLineAndCharacter.character})`;
symbolString += declText;
(declaration as any).__symbolTestOutputCache = declText;
}
}
symbolString += ")";
return {
line: lineAndCharacter.line,
syntaxKind: node.kind,
sourceText,
symbol: symbolString,
};
}
}