/
githubmirror
/
TypeScript
Обзор
Документация
Войти
/
githubmirror
/
TypeScript
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/harness/typeWriter.ts
342 строки
14 KB
Jake Bailey
Hide omitted expression types in type baselines (#63081)
03 фев 2026, 18:56
Не верифицирован
03 фев 2026, 18:56
43db4c1
Код
Авторство
О чём код?
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; } if (ts.isOmittedExpression(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, }; } }