mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-12-16 19:09:51 -06:00
1177 lines
53 KiB
TypeScript
1177 lines
53 KiB
TypeScript
import * as ts from "./_namespaces/ts";
|
|
import {
|
|
AnyFunction,
|
|
AssertionLevel,
|
|
BigIntLiteralType,
|
|
CheckMode,
|
|
compareValues,
|
|
EmitFlags,
|
|
every,
|
|
FlowFlags,
|
|
FlowLabel,
|
|
FlowNode,
|
|
FlowNodeBase,
|
|
FlowSwitchClause,
|
|
getEffectiveModifierFlagsNoCache,
|
|
getEmitFlags,
|
|
getOwnKeys,
|
|
getParseTreeNode,
|
|
getSourceFileOfNode,
|
|
getSourceTextOfNodeFromSourceFile,
|
|
hasProperty,
|
|
idText,
|
|
IntrinsicType,
|
|
isArrayTypeNode,
|
|
isBigIntLiteral,
|
|
isCallSignatureDeclaration,
|
|
isConditionalTypeNode,
|
|
isConstructorDeclaration,
|
|
isConstructorTypeNode,
|
|
isConstructSignatureDeclaration,
|
|
isDefaultClause,
|
|
isFunctionTypeNode,
|
|
isGeneratedIdentifier,
|
|
isGetAccessorDeclaration,
|
|
isIdentifier,
|
|
isImportTypeNode,
|
|
isIndexedAccessTypeNode,
|
|
isIndexSignatureDeclaration,
|
|
isInferTypeNode,
|
|
isIntersectionTypeNode,
|
|
isLiteralTypeNode,
|
|
isMappedTypeNode,
|
|
isNamedTupleMember,
|
|
isNumericLiteral,
|
|
isOptionalTypeNode,
|
|
isParameter,
|
|
isParenthesizedTypeNode,
|
|
isParseTreeNode,
|
|
isPrivateIdentifier,
|
|
isRestTypeNode,
|
|
isSetAccessorDeclaration,
|
|
isStringLiteral,
|
|
isThisTypeNode,
|
|
isTupleTypeNode,
|
|
isTypeLiteralNode,
|
|
isTypeOperatorNode,
|
|
isTypeParameterDeclaration,
|
|
isTypePredicateNode,
|
|
isTypeQueryNode,
|
|
isTypeReferenceNode,
|
|
isUnionTypeNode,
|
|
LiteralType,
|
|
map,
|
|
MatchingKeys,
|
|
ModifierFlags,
|
|
Node,
|
|
NodeArray,
|
|
NodeFlags,
|
|
nodeIsSynthesized,
|
|
noop,
|
|
objectAllocator,
|
|
ObjectFlags,
|
|
ObjectType,
|
|
RelationComparisonResult,
|
|
ScriptKind,
|
|
Signature,
|
|
SignatureCheckMode,
|
|
SignatureFlags,
|
|
SnippetKind,
|
|
SortedReadonlyArray,
|
|
stableSort,
|
|
Symbol,
|
|
SymbolFlags,
|
|
symbolName,
|
|
SyntaxKind,
|
|
TransformFlags,
|
|
Type,
|
|
TypeFacts,
|
|
TypeFlags,
|
|
TypeMapKind,
|
|
TypeMapper,
|
|
unescapeLeadingUnderscores,
|
|
VarianceFlags,
|
|
zipWith,
|
|
} from "./_namespaces/ts";
|
|
|
|
/** @internal */
|
|
export enum LogLevel {
|
|
Off,
|
|
Error,
|
|
Warning,
|
|
Info,
|
|
Verbose
|
|
}
|
|
|
|
/** @internal */
|
|
export interface LoggingHost {
|
|
log(level: LogLevel, s: string): void;
|
|
}
|
|
|
|
/** @internal */
|
|
export namespace Debug {
|
|
/* eslint-disable prefer-const */
|
|
let currentAssertionLevel = AssertionLevel.None;
|
|
export let currentLogLevel = LogLevel.Warning;
|
|
export let isDebugging = false;
|
|
export let loggingHost: LoggingHost | undefined;
|
|
/* eslint-enable prefer-const */
|
|
|
|
type AssertionKeys = MatchingKeys<typeof Debug, AnyFunction>;
|
|
|
|
export function shouldLog(level: LogLevel): boolean {
|
|
return currentLogLevel <= level;
|
|
}
|
|
|
|
function logMessage(level: LogLevel, s: string): void {
|
|
if (loggingHost && shouldLog(level)) {
|
|
loggingHost.log(level, s);
|
|
}
|
|
}
|
|
|
|
export function log(s: string): void {
|
|
logMessage(LogLevel.Info, s);
|
|
}
|
|
|
|
export namespace log {
|
|
export function error(s: string): void {
|
|
logMessage(LogLevel.Error, s);
|
|
}
|
|
|
|
export function warn(s: string): void {
|
|
logMessage(LogLevel.Warning, s);
|
|
}
|
|
|
|
export function log(s: string): void {
|
|
logMessage(LogLevel.Info, s);
|
|
}
|
|
|
|
export function trace(s: string): void {
|
|
logMessage(LogLevel.Verbose, s);
|
|
}
|
|
}
|
|
|
|
const assertionCache: Partial<Record<AssertionKeys, { level: AssertionLevel, assertion: AnyFunction }>> = {};
|
|
|
|
export function getAssertionLevel() {
|
|
return currentAssertionLevel;
|
|
}
|
|
|
|
export function setAssertionLevel(level: AssertionLevel) {
|
|
const prevAssertionLevel = currentAssertionLevel;
|
|
currentAssertionLevel = level;
|
|
|
|
if (level > prevAssertionLevel) {
|
|
// restore assertion functions for the current assertion level (see `shouldAssertFunction`).
|
|
for (const key of getOwnKeys(assertionCache) as AssertionKeys[]) {
|
|
const cachedFunc = assertionCache[key];
|
|
if (cachedFunc !== undefined && Debug[key] !== cachedFunc.assertion && level >= cachedFunc.level) {
|
|
(Debug as any)[key] = cachedFunc;
|
|
assertionCache[key] = undefined;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function shouldAssert(level: AssertionLevel): boolean {
|
|
return currentAssertionLevel >= level;
|
|
}
|
|
|
|
/**
|
|
* Tests whether an assertion function should be executed. If it shouldn't, it is cached and replaced with `ts.noop`.
|
|
* Replaced assertion functions are restored when `Debug.setAssertionLevel` is set to a high enough level.
|
|
* @param level The minimum assertion level required.
|
|
* @param name The name of the current assertion function.
|
|
*/
|
|
function shouldAssertFunction<K extends AssertionKeys>(level: AssertionLevel, name: K): boolean {
|
|
if (!shouldAssert(level)) {
|
|
assertionCache[name] = { level, assertion: Debug[name] };
|
|
(Debug as any)[name] = noop;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export function fail(message?: string, stackCrawlMark?: AnyFunction): never {
|
|
// eslint-disable-next-line no-debugger
|
|
debugger;
|
|
const e = new Error(message ? `Debug Failure. ${message}` : "Debug Failure.");
|
|
if ((Error as any).captureStackTrace) {
|
|
(Error as any).captureStackTrace(e, stackCrawlMark || fail);
|
|
}
|
|
throw e;
|
|
}
|
|
|
|
export function failBadSyntaxKind(node: Node, message?: string, stackCrawlMark?: AnyFunction): never {
|
|
return fail(
|
|
`${message || "Unexpected node."}\r\nNode ${formatSyntaxKind(node.kind)} was unexpected.`,
|
|
stackCrawlMark || failBadSyntaxKind);
|
|
}
|
|
|
|
export function assert(expression: unknown, message?: string, verboseDebugInfo?: string | (() => string), stackCrawlMark?: AnyFunction): asserts expression {
|
|
if (!expression) {
|
|
message = message ? `False expression: ${message}` : "False expression.";
|
|
if (verboseDebugInfo) {
|
|
message += "\r\nVerbose Debug Information: " + (typeof verboseDebugInfo === "string" ? verboseDebugInfo : verboseDebugInfo());
|
|
}
|
|
fail(message, stackCrawlMark || assert);
|
|
}
|
|
}
|
|
|
|
export function assertEqual<T>(a: T, b: T, msg?: string, msg2?: string, stackCrawlMark?: AnyFunction): void {
|
|
if (a !== b) {
|
|
const message = msg ? msg2 ? `${msg} ${msg2}` : msg : "";
|
|
fail(`Expected ${a} === ${b}. ${message}`, stackCrawlMark || assertEqual);
|
|
}
|
|
}
|
|
|
|
export function assertLessThan(a: number, b: number, msg?: string, stackCrawlMark?: AnyFunction): void {
|
|
if (a >= b) {
|
|
fail(`Expected ${a} < ${b}. ${msg || ""}`, stackCrawlMark || assertLessThan);
|
|
}
|
|
}
|
|
|
|
export function assertLessThanOrEqual(a: number, b: number, stackCrawlMark?: AnyFunction): void {
|
|
if (a > b) {
|
|
fail(`Expected ${a} <= ${b}`, stackCrawlMark || assertLessThanOrEqual);
|
|
}
|
|
}
|
|
|
|
export function assertGreaterThanOrEqual(a: number, b: number, stackCrawlMark?: AnyFunction): void {
|
|
if (a < b) {
|
|
fail(`Expected ${a} >= ${b}`, stackCrawlMark || assertGreaterThanOrEqual);
|
|
}
|
|
}
|
|
|
|
export function assertIsDefined<T>(value: T, message?: string, stackCrawlMark?: AnyFunction): asserts value is NonNullable<T> {
|
|
// eslint-disable-next-line no-null/no-null
|
|
if (value === undefined || value === null) {
|
|
fail(message, stackCrawlMark || assertIsDefined);
|
|
}
|
|
}
|
|
|
|
export function checkDefined<T>(value: T | null | undefined, message?: string, stackCrawlMark?: AnyFunction): T {
|
|
assertIsDefined(value, message, stackCrawlMark || checkDefined);
|
|
return value;
|
|
}
|
|
|
|
export function assertEachIsDefined<T extends Node>(value: NodeArray<T>, message?: string, stackCrawlMark?: AnyFunction): asserts value is NodeArray<T>;
|
|
export function assertEachIsDefined<T>(value: readonly T[], message?: string, stackCrawlMark?: AnyFunction): asserts value is readonly NonNullable<T>[];
|
|
export function assertEachIsDefined<T>(value: readonly T[], message?: string, stackCrawlMark?: AnyFunction) {
|
|
for (const v of value) {
|
|
assertIsDefined(v, message, stackCrawlMark || assertEachIsDefined);
|
|
}
|
|
}
|
|
|
|
export function checkEachDefined<T, A extends readonly T[]>(value: A, message?: string, stackCrawlMark?: AnyFunction): A {
|
|
assertEachIsDefined(value, message, stackCrawlMark || checkEachDefined);
|
|
return value;
|
|
}
|
|
|
|
export function assertNever(member: never, message = "Illegal value:", stackCrawlMark?: AnyFunction): never {
|
|
const detail = typeof member === "object" && hasProperty(member, "kind") && hasProperty(member, "pos") ? "SyntaxKind: " + formatSyntaxKind((member as Node).kind) : JSON.stringify(member);
|
|
return fail(`${message} ${detail}`, stackCrawlMark || assertNever);
|
|
}
|
|
|
|
export function assertEachNode<T extends Node, U extends T>(nodes: NodeArray<T>, test: (node: T) => node is U, message?: string, stackCrawlMark?: AnyFunction): asserts nodes is NodeArray<U>;
|
|
export function assertEachNode<T extends Node, U extends T>(nodes: readonly T[], test: (node: T) => node is U, message?: string, stackCrawlMark?: AnyFunction): asserts nodes is readonly U[];
|
|
export function assertEachNode<T extends Node, U extends T>(nodes: NodeArray<T> | undefined, test: (node: T) => node is U, message?: string, stackCrawlMark?: AnyFunction): asserts nodes is NodeArray<U> | undefined;
|
|
export function assertEachNode<T extends Node, U extends T>(nodes: readonly T[] | undefined, test: (node: T) => node is U, message?: string, stackCrawlMark?: AnyFunction): asserts nodes is readonly U[] | undefined;
|
|
export function assertEachNode(nodes: readonly Node[], test: ((node: Node) => boolean) | undefined, message?: string, stackCrawlMark?: AnyFunction): void;
|
|
export function assertEachNode(nodes: readonly Node[] | undefined, test: ((node: Node) => boolean) | undefined, message?: string, stackCrawlMark?: AnyFunction) {
|
|
if (shouldAssertFunction(AssertionLevel.Normal, "assertEachNode")) {
|
|
assert(
|
|
test === undefined || every(nodes, test),
|
|
message || "Unexpected node.",
|
|
() => `Node array did not pass test '${getFunctionName(test!)}'.`,
|
|
stackCrawlMark || assertEachNode);
|
|
}
|
|
}
|
|
|
|
export function assertNode<T extends Node, U extends T>(node: T | undefined, test: (node: T) => node is U, message?: string, stackCrawlMark?: AnyFunction): asserts node is U;
|
|
export function assertNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string, stackCrawlMark?: AnyFunction): void;
|
|
export function assertNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string, stackCrawlMark?: AnyFunction) {
|
|
if (shouldAssertFunction(AssertionLevel.Normal, "assertNode")) {
|
|
assert(
|
|
node !== undefined && (test === undefined || test(node)),
|
|
message || "Unexpected node.",
|
|
() => `Node ${formatSyntaxKind(node?.kind)} did not pass test '${getFunctionName(test!)}'.`,
|
|
stackCrawlMark || assertNode);
|
|
}
|
|
}
|
|
|
|
export function assertNotNode<T extends Node, U extends T>(node: T | undefined, test: (node: Node) => node is U, message?: string, stackCrawlMark?: AnyFunction): asserts node is Exclude<T, U>;
|
|
export function assertNotNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string, stackCrawlMark?: AnyFunction): void;
|
|
export function assertNotNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string, stackCrawlMark?: AnyFunction) {
|
|
if (shouldAssertFunction(AssertionLevel.Normal, "assertNotNode")) {
|
|
assert(
|
|
node === undefined || test === undefined || !test(node),
|
|
message || "Unexpected node.",
|
|
() => `Node ${formatSyntaxKind(node!.kind)} should not have passed test '${getFunctionName(test!)}'.`,
|
|
stackCrawlMark || assertNotNode);
|
|
}
|
|
}
|
|
|
|
export function assertOptionalNode<T extends Node, U extends T>(node: T, test: (node: T) => node is U, message?: string, stackCrawlMark?: AnyFunction): asserts node is U;
|
|
export function assertOptionalNode<T extends Node, U extends T>(node: T | undefined, test: (node: T) => node is U, message?: string, stackCrawlMark?: AnyFunction): asserts node is U | undefined;
|
|
export function assertOptionalNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string, stackCrawlMark?: AnyFunction): void;
|
|
export function assertOptionalNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string, stackCrawlMark?: AnyFunction) {
|
|
if (shouldAssertFunction(AssertionLevel.Normal, "assertOptionalNode")) {
|
|
assert(
|
|
test === undefined || node === undefined || test(node),
|
|
message || "Unexpected node.",
|
|
() => `Node ${formatSyntaxKind(node?.kind)} did not pass test '${getFunctionName(test!)}'.`,
|
|
stackCrawlMark || assertOptionalNode);
|
|
}
|
|
}
|
|
|
|
export function assertOptionalToken<T extends Node, K extends SyntaxKind>(node: T, kind: K, message?: string, stackCrawlMark?: AnyFunction): asserts node is Extract<T, { readonly kind: K }>;
|
|
export function assertOptionalToken<T extends Node, K extends SyntaxKind>(node: T | undefined, kind: K, message?: string, stackCrawlMark?: AnyFunction): asserts node is Extract<T, { readonly kind: K }> | undefined;
|
|
export function assertOptionalToken(node: Node | undefined, kind: SyntaxKind | undefined, message?: string, stackCrawlMark?: AnyFunction): void;
|
|
export function assertOptionalToken(node: Node | undefined, kind: SyntaxKind | undefined, message?: string, stackCrawlMark?: AnyFunction) {
|
|
if (shouldAssertFunction(AssertionLevel.Normal, "assertOptionalToken")) {
|
|
assert(
|
|
kind === undefined || node === undefined || node.kind === kind,
|
|
message || "Unexpected node.",
|
|
() => `Node ${formatSyntaxKind(node?.kind)} was not a '${formatSyntaxKind(kind)}' token.`,
|
|
stackCrawlMark || assertOptionalToken);
|
|
}
|
|
}
|
|
|
|
export function assertMissingNode(node: Node | undefined, message?: string, stackCrawlMark?: AnyFunction): asserts node is undefined;
|
|
export function assertMissingNode(node: Node | undefined, message?: string, stackCrawlMark?: AnyFunction) {
|
|
if (shouldAssertFunction(AssertionLevel.Normal, "assertMissingNode")) {
|
|
assert(
|
|
node === undefined,
|
|
message || "Unexpected node.",
|
|
() => `Node ${formatSyntaxKind(node!.kind)} was unexpected'.`,
|
|
stackCrawlMark || assertMissingNode);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Asserts a value has the specified type in typespace only (does not perform a runtime assertion).
|
|
* This is useful in cases where we switch on `node.kind` and can be reasonably sure the type is accurate, and
|
|
* as a result can reduce the number of unnecessary casts.
|
|
*/
|
|
export function type<T>(value: unknown): asserts value is T;
|
|
export function type(_value: unknown) { }
|
|
|
|
export function getFunctionName(func: AnyFunction) {
|
|
if (typeof func !== "function") {
|
|
return "";
|
|
}
|
|
else if (hasProperty(func, "name")) {
|
|
return (func as any).name;
|
|
}
|
|
else {
|
|
const text = Function.prototype.toString.call(func);
|
|
const match = /^function\s+([\w$]+)\s*\(/.exec(text);
|
|
return match ? match[1] : "";
|
|
}
|
|
}
|
|
|
|
export function formatSymbol(symbol: Symbol): string {
|
|
return `{ name: ${unescapeLeadingUnderscores(symbol.escapedName)}; flags: ${formatSymbolFlags(symbol.flags)}; declarations: ${map(symbol.declarations, node => formatSyntaxKind(node.kind))} }`;
|
|
}
|
|
|
|
/**
|
|
* Formats an enum value as a string for debugging and debug assertions.
|
|
*/
|
|
export function formatEnum(value = 0, enumObject: any, isFlags?: boolean) {
|
|
const members = getEnumMembers(enumObject);
|
|
if (value === 0) {
|
|
return members.length > 0 && members[0][0] === 0 ? members[0][1] : "0";
|
|
}
|
|
if (isFlags) {
|
|
const result: string[] = [];
|
|
let remainingFlags = value;
|
|
for (const [enumValue, enumName] of members) {
|
|
if (enumValue > value) {
|
|
break;
|
|
}
|
|
if (enumValue !== 0 && enumValue & value) {
|
|
result.push(enumName);
|
|
remainingFlags &= ~enumValue;
|
|
}
|
|
}
|
|
if (remainingFlags === 0) {
|
|
return result.join("|");
|
|
}
|
|
}
|
|
else {
|
|
for (const [enumValue, enumName] of members) {
|
|
if (enumValue === value) {
|
|
return enumName;
|
|
}
|
|
}
|
|
}
|
|
return value.toString();
|
|
}
|
|
|
|
const enumMemberCache = new Map<any, SortedReadonlyArray<[number, string]>>();
|
|
|
|
function getEnumMembers(enumObject: any) {
|
|
// Assuming enum objects do not change at runtime, we can cache the enum members list
|
|
// to reuse later. This saves us from reconstructing this each and every time we call
|
|
// a formatting function (which can be expensive for large enums like SyntaxKind).
|
|
const existing = enumMemberCache.get(enumObject);
|
|
if (existing) {
|
|
return existing;
|
|
}
|
|
|
|
const result: [number, string][] = [];
|
|
for (const name in enumObject) {
|
|
const value = enumObject[name];
|
|
if (typeof value === "number") {
|
|
result.push([value, name]);
|
|
}
|
|
}
|
|
|
|
const sorted = stableSort<[number, string]>(result, (x, y) => compareValues(x[0], y[0]));
|
|
enumMemberCache.set(enumObject, sorted);
|
|
return sorted;
|
|
}
|
|
|
|
export function formatSyntaxKind(kind: SyntaxKind | undefined): string {
|
|
return formatEnum(kind, (ts as any).SyntaxKind, /*isFlags*/ false);
|
|
}
|
|
|
|
export function formatSnippetKind(kind: SnippetKind | undefined): string {
|
|
return formatEnum(kind, (ts as any).SnippetKind, /*isFlags*/ false);
|
|
}
|
|
|
|
export function formatScriptKind(kind: ScriptKind | undefined): string {
|
|
return formatEnum(kind, (ts as any).ScriptKind, /*isFlags*/ false);
|
|
}
|
|
|
|
export function formatNodeFlags(flags: NodeFlags | undefined): string {
|
|
return formatEnum(flags, (ts as any).NodeFlags, /*isFlags*/ true);
|
|
}
|
|
|
|
export function formatModifierFlags(flags: ModifierFlags | undefined): string {
|
|
return formatEnum(flags, (ts as any).ModifierFlags, /*isFlags*/ true);
|
|
}
|
|
|
|
export function formatTransformFlags(flags: TransformFlags | undefined): string {
|
|
return formatEnum(flags, (ts as any).TransformFlags, /*isFlags*/ true);
|
|
}
|
|
|
|
export function formatEmitFlags(flags: EmitFlags | undefined): string {
|
|
return formatEnum(flags, (ts as any).EmitFlags, /*isFlags*/ true);
|
|
}
|
|
|
|
export function formatSymbolFlags(flags: SymbolFlags | undefined): string {
|
|
return formatEnum(flags, (ts as any).SymbolFlags, /*isFlags*/ true);
|
|
}
|
|
|
|
export function formatTypeFlags(flags: TypeFlags | undefined): string {
|
|
return formatEnum(flags, (ts as any).TypeFlags, /*isFlags*/ true);
|
|
}
|
|
|
|
export function formatSignatureFlags(flags: SignatureFlags | undefined): string {
|
|
return formatEnum(flags, (ts as any).SignatureFlags, /*isFlags*/ true);
|
|
}
|
|
|
|
export function formatObjectFlags(flags: ObjectFlags | undefined): string {
|
|
return formatEnum(flags, (ts as any).ObjectFlags, /*isFlags*/ true);
|
|
}
|
|
|
|
export function formatFlowFlags(flags: FlowFlags | undefined): string {
|
|
return formatEnum(flags, (ts as any).FlowFlags, /*isFlags*/ true);
|
|
}
|
|
|
|
export function formatRelationComparisonResult(result: RelationComparisonResult | undefined): string {
|
|
return formatEnum(result, (ts as any).RelationComparisonResult, /*isFlags*/ true);
|
|
}
|
|
|
|
export function formatCheckMode(mode: CheckMode | undefined): string {
|
|
return formatEnum(mode, (ts as any).CheckMode, /*isFlags*/ true);
|
|
}
|
|
|
|
export function formatSignatureCheckMode(mode: SignatureCheckMode | undefined): string {
|
|
return formatEnum(mode, (ts as any).SignatureCheckMode, /*isFlags*/ true);
|
|
}
|
|
|
|
export function formatTypeFacts(facts: TypeFacts | undefined): string {
|
|
return formatEnum(facts, (ts as any).TypeFacts, /*isFlags*/ true);
|
|
}
|
|
|
|
let isDebugInfoEnabled = false;
|
|
|
|
let flowNodeProto: FlowNodeBase | undefined;
|
|
|
|
function attachFlowNodeDebugInfoWorker(flowNode: FlowNodeBase) {
|
|
if (!("__debugFlowFlags" in flowNode)) { // eslint-disable-line local/no-in-operator
|
|
Object.defineProperties(flowNode, {
|
|
// for use with vscode-js-debug's new customDescriptionGenerator in launch.json
|
|
__tsDebuggerDisplay: {
|
|
value(this: FlowNodeBase) {
|
|
const flowHeader =
|
|
this.flags & FlowFlags.Start ? "FlowStart" :
|
|
this.flags & FlowFlags.BranchLabel ? "FlowBranchLabel" :
|
|
this.flags & FlowFlags.LoopLabel ? "FlowLoopLabel" :
|
|
this.flags & FlowFlags.Assignment ? "FlowAssignment" :
|
|
this.flags & FlowFlags.TrueCondition ? "FlowTrueCondition" :
|
|
this.flags & FlowFlags.FalseCondition ? "FlowFalseCondition" :
|
|
this.flags & FlowFlags.SwitchClause ? "FlowSwitchClause" :
|
|
this.flags & FlowFlags.ArrayMutation ? "FlowArrayMutation" :
|
|
this.flags & FlowFlags.Call ? "FlowCall" :
|
|
this.flags & FlowFlags.ReduceLabel ? "FlowReduceLabel" :
|
|
this.flags & FlowFlags.Unreachable ? "FlowUnreachable" :
|
|
"UnknownFlow";
|
|
const remainingFlags = this.flags & ~(FlowFlags.Referenced - 1);
|
|
return `${flowHeader}${remainingFlags ? ` (${formatFlowFlags(remainingFlags)})`: ""}`;
|
|
}
|
|
},
|
|
__debugFlowFlags: { get(this: FlowNodeBase) { return formatEnum(this.flags, (ts as any).FlowFlags, /*isFlags*/ true); } },
|
|
__debugToString: { value(this: FlowNodeBase) { return formatControlFlowGraph(this); } }
|
|
});
|
|
}
|
|
}
|
|
|
|
export function attachFlowNodeDebugInfo(flowNode: FlowNodeBase) {
|
|
if (isDebugInfoEnabled) {
|
|
if (typeof Object.setPrototypeOf === "function") {
|
|
// if we're in es2015, attach the method to a shared prototype for `FlowNode`
|
|
// so the method doesn't show up in the watch window.
|
|
if (!flowNodeProto) {
|
|
flowNodeProto = Object.create(Object.prototype) as FlowNodeBase;
|
|
attachFlowNodeDebugInfoWorker(flowNodeProto);
|
|
}
|
|
Object.setPrototypeOf(flowNode, flowNodeProto);
|
|
}
|
|
else {
|
|
// not running in an es2015 environment, attach the method directly.
|
|
attachFlowNodeDebugInfoWorker(flowNode);
|
|
}
|
|
}
|
|
}
|
|
|
|
let nodeArrayProto: NodeArray<Node> | undefined;
|
|
|
|
function attachNodeArrayDebugInfoWorker(array: NodeArray<Node>) {
|
|
if (!("__tsDebuggerDisplay" in array)) { // eslint-disable-line local/no-in-operator
|
|
Object.defineProperties(array, {
|
|
__tsDebuggerDisplay: {
|
|
value(this: NodeArray<Node>, defaultValue: string) {
|
|
// An `Array` with extra properties is rendered as `[A, B, prop1: 1, prop2: 2]`. Most of
|
|
// these aren't immediately useful so we trim off the `prop1: ..., prop2: ...` part from the
|
|
// formatted string.
|
|
// This regex can trigger slow backtracking because of overlapping potential captures.
|
|
// We don't care, this is debug code that's only enabled with a debugger attached -
|
|
// we're just taking note of it for anyone checking regex performance in the future.
|
|
defaultValue = String(defaultValue).replace(/(?:,[\s\w\d_]+:[^,]+)+\]$/, "]");
|
|
return `NodeArray ${defaultValue}`;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
export function attachNodeArrayDebugInfo(array: NodeArray<Node>) {
|
|
if (isDebugInfoEnabled) {
|
|
if (typeof Object.setPrototypeOf === "function") {
|
|
// if we're in es2015, attach the method to a shared prototype for `NodeArray`
|
|
// so the method doesn't show up in the watch window.
|
|
if (!nodeArrayProto) {
|
|
nodeArrayProto = Object.create(Array.prototype) as NodeArray<Node>;
|
|
attachNodeArrayDebugInfoWorker(nodeArrayProto);
|
|
}
|
|
Object.setPrototypeOf(array, nodeArrayProto);
|
|
}
|
|
else {
|
|
// not running in an es2015 environment, attach the method directly.
|
|
attachNodeArrayDebugInfoWorker(array);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Injects debug information into frequently used types.
|
|
*/
|
|
export function enableDebugInfo() {
|
|
if (isDebugInfoEnabled) return;
|
|
|
|
// avoid recomputing
|
|
const weakTypeTextMap = new WeakMap<Type, string>();
|
|
const weakNodeTextMap = new WeakMap<Node, string>();
|
|
|
|
// Add additional properties in debug mode to assist with debugging.
|
|
Object.defineProperties(objectAllocator.getSymbolConstructor().prototype, {
|
|
// for use with vscode-js-debug's new customDescriptionGenerator in launch.json
|
|
__tsDebuggerDisplay: {
|
|
value(this: Symbol) {
|
|
const symbolHeader =
|
|
this.flags & SymbolFlags.Transient ? "TransientSymbol" :
|
|
"Symbol";
|
|
const remainingSymbolFlags = this.flags & ~SymbolFlags.Transient;
|
|
return `${symbolHeader} '${symbolName(this)}'${remainingSymbolFlags ? ` (${formatSymbolFlags(remainingSymbolFlags)})` : ""}`;
|
|
}
|
|
},
|
|
__debugFlags: { get(this: Symbol) { return formatSymbolFlags(this.flags); } }
|
|
});
|
|
|
|
Object.defineProperties(objectAllocator.getTypeConstructor().prototype, {
|
|
// for use with vscode-js-debug's new customDescriptionGenerator in launch.json
|
|
__tsDebuggerDisplay: {
|
|
value(this: Type) {
|
|
const typeHeader =
|
|
this.flags & TypeFlags.Nullable ? "NullableType" :
|
|
this.flags & TypeFlags.StringOrNumberLiteral ? `LiteralType ${JSON.stringify((this as LiteralType).value)}` :
|
|
this.flags & TypeFlags.BigIntLiteral ? `LiteralType ${(this as BigIntLiteralType).value.negative ? "-" : ""}${(this as BigIntLiteralType).value.base10Value}n` :
|
|
this.flags & TypeFlags.UniqueESSymbol ? "UniqueESSymbolType" :
|
|
this.flags & TypeFlags.Enum ? "EnumType" :
|
|
this.flags & TypeFlags.Intrinsic ? `IntrinsicType ${(this as IntrinsicType).intrinsicName}` :
|
|
this.flags & TypeFlags.Union ? "UnionType" :
|
|
this.flags & TypeFlags.Intersection ? "IntersectionType" :
|
|
this.flags & TypeFlags.Index ? "IndexType" :
|
|
this.flags & TypeFlags.IndexedAccess ? "IndexedAccessType" :
|
|
this.flags & TypeFlags.Conditional ? "ConditionalType" :
|
|
this.flags & TypeFlags.Substitution ? "SubstitutionType" :
|
|
this.flags & TypeFlags.TypeParameter ? "TypeParameter" :
|
|
this.flags & TypeFlags.Object ?
|
|
(this as ObjectType).objectFlags & ObjectFlags.ClassOrInterface ? "InterfaceType" :
|
|
(this as ObjectType).objectFlags & ObjectFlags.Reference ? "TypeReference" :
|
|
(this as ObjectType).objectFlags & ObjectFlags.Tuple ? "TupleType" :
|
|
(this as ObjectType).objectFlags & ObjectFlags.Anonymous ? "AnonymousType" :
|
|
(this as ObjectType).objectFlags & ObjectFlags.Mapped ? "MappedType" :
|
|
(this as ObjectType).objectFlags & ObjectFlags.ReverseMapped ? "ReverseMappedType" :
|
|
(this as ObjectType).objectFlags & ObjectFlags.EvolvingArray ? "EvolvingArrayType" :
|
|
"ObjectType" :
|
|
"Type";
|
|
const remainingObjectFlags = this.flags & TypeFlags.Object ? (this as ObjectType).objectFlags & ~ObjectFlags.ObjectTypeKindMask : 0;
|
|
return `${typeHeader}${this.symbol ? ` '${symbolName(this.symbol)}'` : ""}${remainingObjectFlags ? ` (${formatObjectFlags(remainingObjectFlags)})` : ""}`;
|
|
}
|
|
},
|
|
__debugFlags: { get(this: Type) { return formatTypeFlags(this.flags); } },
|
|
__debugObjectFlags: { get(this: Type) { return this.flags & TypeFlags.Object ? formatObjectFlags((this as ObjectType).objectFlags) : ""; } },
|
|
__debugTypeToString: {
|
|
value(this: Type) {
|
|
// avoid recomputing
|
|
let text = weakTypeTextMap.get(this);
|
|
if (text === undefined) {
|
|
text = this.checker.typeToString(this);
|
|
weakTypeTextMap.set(this, text);
|
|
}
|
|
return text;
|
|
}
|
|
},
|
|
});
|
|
|
|
Object.defineProperties(objectAllocator.getSignatureConstructor().prototype, {
|
|
__debugFlags: { get(this: Signature) { return formatSignatureFlags(this.flags); } },
|
|
__debugSignatureToString: { value(this: Signature) { return this.checker?.signatureToString(this); } }
|
|
});
|
|
|
|
const nodeConstructors = [
|
|
objectAllocator.getNodeConstructor(),
|
|
objectAllocator.getIdentifierConstructor(),
|
|
objectAllocator.getTokenConstructor(),
|
|
objectAllocator.getSourceFileConstructor()
|
|
];
|
|
|
|
for (const ctor of nodeConstructors) {
|
|
if (!hasProperty(ctor.prototype, "__debugKind")) {
|
|
Object.defineProperties(ctor.prototype, {
|
|
// for use with vscode-js-debug's new customDescriptionGenerator in launch.json
|
|
__tsDebuggerDisplay: {
|
|
value(this: Node) {
|
|
const nodeHeader =
|
|
isGeneratedIdentifier(this) ? "GeneratedIdentifier" :
|
|
isIdentifier(this) ? `Identifier '${idText(this)}'` :
|
|
isPrivateIdentifier(this) ? `PrivateIdentifier '${idText(this)}'` :
|
|
isStringLiteral(this) ? `StringLiteral ${JSON.stringify(this.text.length < 10 ? this.text : this.text.slice(10) + "...")}` :
|
|
isNumericLiteral(this) ? `NumericLiteral ${this.text}` :
|
|
isBigIntLiteral(this) ? `BigIntLiteral ${this.text}n` :
|
|
isTypeParameterDeclaration(this) ? "TypeParameterDeclaration" :
|
|
isParameter(this) ? "ParameterDeclaration" :
|
|
isConstructorDeclaration(this) ? "ConstructorDeclaration" :
|
|
isGetAccessorDeclaration(this) ? "GetAccessorDeclaration" :
|
|
isSetAccessorDeclaration(this) ? "SetAccessorDeclaration" :
|
|
isCallSignatureDeclaration(this) ? "CallSignatureDeclaration" :
|
|
isConstructSignatureDeclaration(this) ? "ConstructSignatureDeclaration" :
|
|
isIndexSignatureDeclaration(this) ? "IndexSignatureDeclaration" :
|
|
isTypePredicateNode(this) ? "TypePredicateNode" :
|
|
isTypeReferenceNode(this) ? "TypeReferenceNode" :
|
|
isFunctionTypeNode(this) ? "FunctionTypeNode" :
|
|
isConstructorTypeNode(this) ? "ConstructorTypeNode" :
|
|
isTypeQueryNode(this) ? "TypeQueryNode" :
|
|
isTypeLiteralNode(this) ? "TypeLiteralNode" :
|
|
isArrayTypeNode(this) ? "ArrayTypeNode" :
|
|
isTupleTypeNode(this) ? "TupleTypeNode" :
|
|
isOptionalTypeNode(this) ? "OptionalTypeNode" :
|
|
isRestTypeNode(this) ? "RestTypeNode" :
|
|
isUnionTypeNode(this) ? "UnionTypeNode" :
|
|
isIntersectionTypeNode(this) ? "IntersectionTypeNode" :
|
|
isConditionalTypeNode(this) ? "ConditionalTypeNode" :
|
|
isInferTypeNode(this) ? "InferTypeNode" :
|
|
isParenthesizedTypeNode(this) ? "ParenthesizedTypeNode" :
|
|
isThisTypeNode(this) ? "ThisTypeNode" :
|
|
isTypeOperatorNode(this) ? "TypeOperatorNode" :
|
|
isIndexedAccessTypeNode(this) ? "IndexedAccessTypeNode" :
|
|
isMappedTypeNode(this) ? "MappedTypeNode" :
|
|
isLiteralTypeNode(this) ? "LiteralTypeNode" :
|
|
isNamedTupleMember(this) ? "NamedTupleMember" :
|
|
isImportTypeNode(this) ? "ImportTypeNode" :
|
|
formatSyntaxKind(this.kind);
|
|
return `${nodeHeader}${this.flags ? ` (${formatNodeFlags(this.flags)})` : ""}`;
|
|
}
|
|
},
|
|
__debugKind: { get(this: Node) { return formatSyntaxKind(this.kind); } },
|
|
__debugNodeFlags: { get(this: Node) { return formatNodeFlags(this.flags); } },
|
|
__debugModifierFlags: { get(this: Node) { return formatModifierFlags(getEffectiveModifierFlagsNoCache(this)); } },
|
|
__debugTransformFlags: { get(this: Node) { return formatTransformFlags(this.transformFlags); } },
|
|
__debugIsParseTreeNode: { get(this: Node) { return isParseTreeNode(this); } },
|
|
__debugEmitFlags: { get(this: Node) { return formatEmitFlags(getEmitFlags(this)); } },
|
|
__debugGetText: {
|
|
value(this: Node, includeTrivia?: boolean) {
|
|
if (nodeIsSynthesized(this)) return "";
|
|
// avoid recomputing
|
|
let text = weakNodeTextMap.get(this);
|
|
if (text === undefined) {
|
|
const parseNode = getParseTreeNode(this);
|
|
const sourceFile = parseNode && getSourceFileOfNode(parseNode);
|
|
text = sourceFile ? getSourceTextOfNodeFromSourceFile(sourceFile, parseNode, includeTrivia) : "";
|
|
weakNodeTextMap.set(this, text);
|
|
}
|
|
return text;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
isDebugInfoEnabled = true;
|
|
}
|
|
|
|
export function formatVariance(varianceFlags: VarianceFlags) {
|
|
const variance = varianceFlags & VarianceFlags.VarianceMask;
|
|
let result =
|
|
variance === VarianceFlags.Invariant ? "in out" :
|
|
variance === VarianceFlags.Bivariant ? "[bivariant]" :
|
|
variance === VarianceFlags.Contravariant ? "in" :
|
|
variance === VarianceFlags.Covariant ? "out" :
|
|
variance === VarianceFlags.Independent ? "[independent]" : "";
|
|
if (varianceFlags & VarianceFlags.Unmeasurable) {
|
|
result += " (unmeasurable)";
|
|
}
|
|
else if (varianceFlags & VarianceFlags.Unreliable) {
|
|
result += " (unreliable)";
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export type DebugType = Type & { __debugTypeToString(): string }; // eslint-disable-line @typescript-eslint/naming-convention
|
|
export class DebugTypeMapper {
|
|
declare kind: TypeMapKind;
|
|
__debugToString(): string { // eslint-disable-line @typescript-eslint/naming-convention
|
|
type<TypeMapper>(this);
|
|
switch (this.kind) {
|
|
case TypeMapKind.Function: return this.debugInfo?.() || "(function mapper)";
|
|
case TypeMapKind.Simple: return `${(this.source as DebugType).__debugTypeToString()} -> ${(this.target as DebugType).__debugTypeToString()}`;
|
|
case TypeMapKind.Array: return zipWith<DebugType, DebugType | string, unknown>(
|
|
this.sources as readonly DebugType[],
|
|
this.targets as readonly DebugType[] || map(this.sources, () => "any"),
|
|
(s, t) => `${s.__debugTypeToString()} -> ${typeof t === "string" ? t : t.__debugTypeToString()}`).join(", ");
|
|
case TypeMapKind.Deferred: return zipWith(
|
|
this.sources,
|
|
this.targets,
|
|
(s, t) => `${(s as DebugType).__debugTypeToString()} -> ${(t() as DebugType).__debugTypeToString()}`).join(", ");
|
|
case TypeMapKind.Merged:
|
|
case TypeMapKind.Composite: return `m1: ${(this.mapper1 as unknown as DebugTypeMapper).__debugToString().split("\n").join("\n ")}
|
|
m2: ${(this.mapper2 as unknown as DebugTypeMapper).__debugToString().split("\n").join("\n ")}`;
|
|
default: return assertNever(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function attachDebugPrototypeIfDebug(mapper: TypeMapper): TypeMapper {
|
|
if (isDebugging) {
|
|
return Object.setPrototypeOf(mapper, DebugTypeMapper.prototype);
|
|
}
|
|
return mapper;
|
|
}
|
|
|
|
export function printControlFlowGraph(flowNode: FlowNode) {
|
|
return console.log(formatControlFlowGraph(flowNode));
|
|
}
|
|
|
|
export function formatControlFlowGraph(flowNode: FlowNode) {
|
|
let nextDebugFlowId = -1;
|
|
|
|
function getDebugFlowNodeId(f: FlowNode) {
|
|
if (!f.id) {
|
|
f.id = nextDebugFlowId;
|
|
nextDebugFlowId--;
|
|
}
|
|
return f.id;
|
|
}
|
|
|
|
const enum BoxCharacter {
|
|
lr = "─",
|
|
ud = "│",
|
|
dr = "╭",
|
|
dl = "╮",
|
|
ul = "╯",
|
|
ur = "╰",
|
|
udr = "├",
|
|
udl = "┤",
|
|
dlr = "┬",
|
|
ulr = "┴",
|
|
udlr = "╫",
|
|
}
|
|
|
|
const enum Connection {
|
|
None = 0,
|
|
Up = 1 << 0,
|
|
Down = 1 << 1,
|
|
Left = 1 << 2,
|
|
Right = 1 << 3,
|
|
|
|
UpDown = Up | Down,
|
|
LeftRight = Left | Right,
|
|
UpLeft = Up | Left,
|
|
UpRight = Up | Right,
|
|
DownLeft = Down | Left,
|
|
DownRight = Down | Right,
|
|
UpDownLeft = UpDown | Left,
|
|
UpDownRight = UpDown | Right,
|
|
UpLeftRight = Up | LeftRight,
|
|
DownLeftRight = Down | LeftRight,
|
|
UpDownLeftRight = UpDown | LeftRight,
|
|
|
|
NoChildren = 1 << 4,
|
|
}
|
|
|
|
interface FlowGraphNode {
|
|
id: number;
|
|
flowNode: FlowNode;
|
|
edges: FlowGraphEdge[];
|
|
text: string;
|
|
lane: number;
|
|
endLane: number;
|
|
level: number;
|
|
circular: boolean | "circularity";
|
|
}
|
|
|
|
interface FlowGraphEdge {
|
|
source: FlowGraphNode;
|
|
target: FlowGraphNode;
|
|
}
|
|
|
|
const hasAntecedentFlags =
|
|
FlowFlags.Assignment |
|
|
FlowFlags.Condition |
|
|
FlowFlags.SwitchClause |
|
|
FlowFlags.ArrayMutation |
|
|
FlowFlags.Call |
|
|
FlowFlags.ReduceLabel;
|
|
|
|
const hasNodeFlags =
|
|
FlowFlags.Start |
|
|
FlowFlags.Assignment |
|
|
FlowFlags.Call |
|
|
FlowFlags.Condition |
|
|
FlowFlags.ArrayMutation;
|
|
|
|
const links: Record<number, FlowGraphNode> = Object.create(/*o*/ null); // eslint-disable-line no-null/no-null
|
|
const nodes: FlowGraphNode[] = [];
|
|
const edges: FlowGraphEdge[] = [];
|
|
const root = buildGraphNode(flowNode, new Set());
|
|
for (const node of nodes) {
|
|
node.text = renderFlowNode(node.flowNode, node.circular);
|
|
computeLevel(node);
|
|
}
|
|
|
|
const height = computeHeight(root);
|
|
const columnWidths = computeColumnWidths(height);
|
|
computeLanes(root, 0);
|
|
return renderGraph();
|
|
|
|
function isFlowSwitchClause(f: FlowNode): f is FlowSwitchClause {
|
|
return !!(f.flags & FlowFlags.SwitchClause);
|
|
}
|
|
|
|
function hasAntecedents(f: FlowNode): f is FlowLabel & { antecedents: FlowNode[] } {
|
|
return !!(f.flags & FlowFlags.Label) && !!(f as FlowLabel).antecedents;
|
|
}
|
|
|
|
function hasAntecedent(f: FlowNode): f is Extract<FlowNode, { antecedent: FlowNode }> {
|
|
return !!(f.flags & hasAntecedentFlags);
|
|
}
|
|
|
|
function hasNode(f: FlowNode): f is Extract<FlowNode, { node?: Node }> {
|
|
return !!(f.flags & hasNodeFlags);
|
|
}
|
|
|
|
function getChildren(node: FlowGraphNode) {
|
|
const children: FlowGraphNode[] = [];
|
|
for (const edge of node.edges) {
|
|
if (edge.source === node) {
|
|
children.push(edge.target);
|
|
}
|
|
}
|
|
return children;
|
|
}
|
|
|
|
function getParents(node: FlowGraphNode) {
|
|
const parents: FlowGraphNode[] = [];
|
|
for (const edge of node.edges) {
|
|
if (edge.target === node) {
|
|
parents.push(edge.source);
|
|
}
|
|
}
|
|
return parents;
|
|
}
|
|
|
|
function buildGraphNode(flowNode: FlowNode, seen: Set<FlowNode>): FlowGraphNode {
|
|
const id = getDebugFlowNodeId(flowNode);
|
|
let graphNode = links[id];
|
|
if (graphNode && seen.has(flowNode)) {
|
|
graphNode.circular = true;
|
|
graphNode = {
|
|
id: -1,
|
|
flowNode,
|
|
edges: [],
|
|
text: "",
|
|
lane: -1,
|
|
endLane: -1,
|
|
level: -1,
|
|
circular: "circularity"
|
|
};
|
|
nodes.push(graphNode);
|
|
return graphNode;
|
|
}
|
|
seen.add(flowNode);
|
|
if (!graphNode) {
|
|
links[id] = graphNode = { id, flowNode, edges: [], text: "", lane: -1, endLane: -1, level: -1, circular: false };
|
|
nodes.push(graphNode);
|
|
if (hasAntecedents(flowNode)) {
|
|
for (const antecedent of flowNode.antecedents) {
|
|
buildGraphEdge(graphNode, antecedent, seen);
|
|
}
|
|
}
|
|
else if (hasAntecedent(flowNode)) {
|
|
buildGraphEdge(graphNode, flowNode.antecedent, seen);
|
|
}
|
|
}
|
|
seen.delete(flowNode);
|
|
return graphNode;
|
|
}
|
|
|
|
function buildGraphEdge(source: FlowGraphNode, antecedent: FlowNode, seen: Set<FlowNode>) {
|
|
const target = buildGraphNode(antecedent, seen);
|
|
const edge: FlowGraphEdge = { source, target };
|
|
edges.push(edge);
|
|
source.edges.push(edge);
|
|
target.edges.push(edge);
|
|
}
|
|
|
|
function computeLevel(node: FlowGraphNode): number {
|
|
if (node.level !== -1) {
|
|
return node.level;
|
|
}
|
|
let level = 0;
|
|
for (const parent of getParents(node)) {
|
|
level = Math.max(level, computeLevel(parent) + 1);
|
|
}
|
|
return node.level = level;
|
|
}
|
|
|
|
function computeHeight(node: FlowGraphNode): number {
|
|
let height = 0;
|
|
for (const child of getChildren(node)) {
|
|
height = Math.max(height, computeHeight(child));
|
|
}
|
|
return height + 1;
|
|
}
|
|
|
|
function computeColumnWidths(height: number) {
|
|
const columns: number[] = fill(Array(height), 0);
|
|
for (const node of nodes) {
|
|
columns[node.level] = Math.max(columns[node.level], node.text.length);
|
|
}
|
|
return columns;
|
|
}
|
|
|
|
function computeLanes(node: FlowGraphNode, lane: number) {
|
|
if (node.lane === -1) {
|
|
node.lane = lane;
|
|
node.endLane = lane;
|
|
const children = getChildren(node);
|
|
for (let i = 0; i < children.length; i++) {
|
|
if (i > 0) lane++;
|
|
const child = children[i];
|
|
computeLanes(child, lane);
|
|
if (child.endLane > node.endLane) {
|
|
lane = child.endLane;
|
|
}
|
|
}
|
|
node.endLane = lane;
|
|
}
|
|
}
|
|
|
|
function getHeader(flags: FlowFlags) {
|
|
if (flags & FlowFlags.Start) return "Start";
|
|
if (flags & FlowFlags.BranchLabel) return "Branch";
|
|
if (flags & FlowFlags.LoopLabel) return "Loop";
|
|
if (flags & FlowFlags.Assignment) return "Assignment";
|
|
if (flags & FlowFlags.TrueCondition) return "True";
|
|
if (flags & FlowFlags.FalseCondition) return "False";
|
|
if (flags & FlowFlags.SwitchClause) return "SwitchClause";
|
|
if (flags & FlowFlags.ArrayMutation) return "ArrayMutation";
|
|
if (flags & FlowFlags.Call) return "Call";
|
|
if (flags & FlowFlags.ReduceLabel) return "ReduceLabel";
|
|
if (flags & FlowFlags.Unreachable) return "Unreachable";
|
|
throw new Error();
|
|
}
|
|
|
|
function getNodeText(node: Node) {
|
|
const sourceFile = getSourceFileOfNode(node);
|
|
return getSourceTextOfNodeFromSourceFile(sourceFile, node, /*includeTrivia*/ false);
|
|
}
|
|
|
|
function renderFlowNode(flowNode: FlowNode, circular: boolean | "circularity") {
|
|
let text = getHeader(flowNode.flags);
|
|
if (circular) {
|
|
text = `${text}#${getDebugFlowNodeId(flowNode)}`;
|
|
}
|
|
if (hasNode(flowNode)) {
|
|
if (flowNode.node) {
|
|
text += ` (${getNodeText(flowNode.node)})`;
|
|
}
|
|
}
|
|
else if (isFlowSwitchClause(flowNode)) {
|
|
const clauses: string[] = [];
|
|
for (let i = flowNode.clauseStart; i < flowNode.clauseEnd; i++) {
|
|
const clause = flowNode.switchStatement.caseBlock.clauses[i];
|
|
if (isDefaultClause(clause)) {
|
|
clauses.push("default");
|
|
}
|
|
else {
|
|
clauses.push(getNodeText(clause.expression));
|
|
}
|
|
}
|
|
text += ` (${clauses.join(", ")})`;
|
|
}
|
|
return circular === "circularity" ? `Circular(${text})` : text;
|
|
}
|
|
|
|
function renderGraph() {
|
|
const columnCount = columnWidths.length;
|
|
const laneCount = nodes.reduce((x, n) => Math.max(x, n.lane), 0) + 1;
|
|
const lanes: string[] = fill(Array(laneCount), "");
|
|
const grid: (FlowGraphNode | undefined)[][] = columnWidths.map(() => Array(laneCount));
|
|
const connectors: Connection[][] = columnWidths.map(() => fill(Array(laneCount), 0));
|
|
|
|
// build connectors
|
|
for (const node of nodes) {
|
|
grid[node.level][node.lane] = node;
|
|
const children = getChildren(node);
|
|
for (let i = 0; i < children.length; i++) {
|
|
const child = children[i];
|
|
let connector: Connection = Connection.Right;
|
|
if (child.lane === node.lane) connector |= Connection.Left;
|
|
if (i > 0) connector |= Connection.Up;
|
|
if (i < children.length - 1) connector |= Connection.Down;
|
|
connectors[node.level][child.lane] |= connector;
|
|
}
|
|
if (children.length === 0) {
|
|
connectors[node.level][node.lane] |= Connection.NoChildren;
|
|
}
|
|
const parents = getParents(node);
|
|
for (let i = 0; i < parents.length; i++) {
|
|
const parent = parents[i];
|
|
let connector: Connection = Connection.Left;
|
|
if (i > 0) connector |= Connection.Up;
|
|
if (i < parents.length - 1) connector |= Connection.Down;
|
|
connectors[node.level - 1][parent.lane] |= connector;
|
|
}
|
|
}
|
|
|
|
// fill in missing connectors
|
|
for (let column = 0; column < columnCount; column++) {
|
|
for (let lane = 0; lane < laneCount; lane++) {
|
|
const left = column > 0 ? connectors[column - 1][lane] : 0;
|
|
const above = lane > 0 ? connectors[column][lane - 1] : 0;
|
|
let connector = connectors[column][lane];
|
|
if (!connector) {
|
|
if (left & Connection.Right) connector |= Connection.LeftRight;
|
|
if (above & Connection.Down) connector |= Connection.UpDown;
|
|
connectors[column][lane] = connector;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let column = 0; column < columnCount; column++) {
|
|
for (let lane = 0; lane < lanes.length; lane++) {
|
|
const connector = connectors[column][lane];
|
|
const fill = connector & Connection.Left ? BoxCharacter.lr : " ";
|
|
const node = grid[column][lane];
|
|
if (!node) {
|
|
if (column < columnCount - 1) {
|
|
writeLane(lane, repeat(fill, columnWidths[column] + 1));
|
|
}
|
|
}
|
|
else {
|
|
writeLane(lane, node.text);
|
|
if (column < columnCount - 1) {
|
|
writeLane(lane, " ");
|
|
writeLane(lane, repeat(fill, columnWidths[column] - node.text.length));
|
|
}
|
|
}
|
|
writeLane(lane, getBoxCharacter(connector));
|
|
writeLane(lane, connector & Connection.Right && column < columnCount - 1 && !grid[column + 1][lane] ? BoxCharacter.lr : " ");
|
|
}
|
|
}
|
|
|
|
return `\n${lanes.join("\n")}\n`;
|
|
|
|
function writeLane(lane: number, text: string) {
|
|
lanes[lane] += text;
|
|
}
|
|
}
|
|
|
|
function getBoxCharacter(connector: Connection) {
|
|
switch (connector) {
|
|
case Connection.UpDown: return BoxCharacter.ud;
|
|
case Connection.LeftRight: return BoxCharacter.lr;
|
|
case Connection.UpLeft: return BoxCharacter.ul;
|
|
case Connection.UpRight: return BoxCharacter.ur;
|
|
case Connection.DownLeft: return BoxCharacter.dl;
|
|
case Connection.DownRight: return BoxCharacter.dr;
|
|
case Connection.UpDownLeft: return BoxCharacter.udl;
|
|
case Connection.UpDownRight: return BoxCharacter.udr;
|
|
case Connection.UpLeftRight: return BoxCharacter.ulr;
|
|
case Connection.DownLeftRight: return BoxCharacter.dlr;
|
|
case Connection.UpDownLeftRight: return BoxCharacter.udlr;
|
|
}
|
|
return " ";
|
|
}
|
|
|
|
function fill<T>(array: T[], value: T) {
|
|
if (array.fill) {
|
|
array.fill(value);
|
|
}
|
|
else {
|
|
for (let i = 0; i < array.length; i++) {
|
|
array[i] = value;
|
|
}
|
|
}
|
|
return array;
|
|
}
|
|
|
|
function repeat(ch: string, length: number) {
|
|
if (ch.repeat) {
|
|
return length > 0 ? ch.repeat(length) : "";
|
|
}
|
|
let s = "";
|
|
while (s.length < length) {
|
|
s += ch;
|
|
}
|
|
return s;
|
|
}
|
|
}
|
|
}
|