1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-22 23:34:47 -05:00

feat(Deno.inspect): Add sorted, trailingComma, compact and iterableLimit to InspectOptions (#6591)

This commit is contained in:
Nayeem Rahman 2020-07-11 05:52:18 +01:00 committed by GitHub
parent 40d081d3d9
commit 5ec41cbcc2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 463 additions and 185 deletions

View file

@ -2034,8 +2034,18 @@ declare namespace Deno {
| PluginPermissionDescriptor | PluginPermissionDescriptor
| HrtimePermissionDescriptor; | HrtimePermissionDescriptor;
interface InspectOptions { export interface InspectOptions {
/** Traversal depth for nested objects. Defaults to 4. */
depth?: number; depth?: number;
/** Sort Object, Set and Map entries by key. Defaults to false. */
sorted?: boolean;
/** Add a trailing comma for multiline collections. Defaults to false. */
trailingComma?: boolean;
/** Try to fit more than one entry of a collection on the same line.
* Defaults to true. */
compact?: boolean;
/** The maximum number of iterable entries to print. Defaults to 100. */
iterableLimit?: number;
} }
/** Converts the input into a string that has the same format as printed by /** Converts the input into a string that has the same format as printed by

View file

@ -3,16 +3,16 @@
import { exit } from "./ops/os.ts"; import { exit } from "./ops/os.ts";
import { core } from "./core.ts"; import { core } from "./core.ts";
import { version } from "./version.ts"; import { version } from "./version.ts";
import { stringifyArgs } from "./web/console.ts"; import { inspectArgs } from "./web/console.ts";
import { startRepl, readline } from "./ops/repl.ts"; import { startRepl, readline } from "./ops/repl.ts";
import { close } from "./ops/resources.ts"; import { close } from "./ops/resources.ts";
function replLog(...args: unknown[]): void { function replLog(...args: unknown[]): void {
core.print(stringifyArgs(args) + "\n"); core.print(inspectArgs(args) + "\n");
} }
function replError(...args: unknown[]): void { function replError(...args: unknown[]): void {
core.print(stringifyArgs(args) + "\n", true); core.print(inspectArgs(args) + "\n", true);
} }
// Error messages that allow users to continue input // Error messages that allow users to continue input

View file

@ -2,7 +2,7 @@
import { gray, green, italic, red, yellow } from "./colors.ts"; import { gray, green, italic, red, yellow } from "./colors.ts";
import { exit } from "./ops/os.ts"; import { exit } from "./ops/os.ts";
import { Console, stringifyArgs } from "./web/console.ts"; import { Console, inspectArgs } from "./web/console.ts";
import { stdout } from "./files.ts"; import { stdout } from "./files.ts";
import { exposeForTest } from "./internals.ts"; import { exposeForTest } from "./internals.ts";
import { TextEncoder } from "./web/text_encoding.ts"; import { TextEncoder } from "./web/text_encoding.ts";
@ -205,7 +205,7 @@ function reportToConsole(message: TestMessage): void {
for (const { name, error } of failures) { for (const { name, error } of failures) {
log(name); log(name);
log(stringifyArgs([error!])); log(inspectArgs([error!]));
log(""); log("");
} }

View file

@ -16,16 +16,28 @@ import {
} from "../colors.ts"; } from "../colors.ts";
type ConsoleContext = Set<unknown>; type ConsoleContext = Set<unknown>;
type InspectOptions = Partial<{
depth: number; export interface InspectOptions {
indentLevel: number; depth?: number;
}>; indentLevel?: number;
sorted?: boolean;
trailingComma?: boolean;
compact?: boolean;
iterableLimit?: number;
}
const DEFAULT_INSPECT_OPTIONS: Required<InspectOptions> = {
depth: 4,
indentLevel: 0,
sorted: false,
trailingComma: false,
compact: true,
iterableLimit: 100,
};
const DEFAULT_INDENT = " "; // Default indent string const DEFAULT_INDENT = " "; // Default indent string
const DEFAULT_MAX_DEPTH = 4; // Default depth of logging nested objects
const LINE_BREAKING_LENGTH = 80; const LINE_BREAKING_LENGTH = 80;
const MAX_ITERABLE_LENGTH = 100;
const MIN_GROUP_LENGTH = 6; const MIN_GROUP_LENGTH = 6;
const STR_ABBREVIATE_SIZE = 100; const STR_ABBREVIATE_SIZE = 100;
// Char codes // Char codes
@ -63,7 +75,7 @@ function getClassInstanceName(instance: unknown): string {
return ""; return "";
} }
function createFunctionString(value: Function, _ctx: ConsoleContext): string { function inspectFunction(value: Function, _ctx: ConsoleContext): string {
// Might be Function/AsyncFunction/GeneratorFunction // Might be Function/AsyncFunction/GeneratorFunction
const cstrName = Object.getPrototypeOf(value).constructor.name; const cstrName = Object.getPrototypeOf(value).constructor.name;
if (value.name && value.name !== "anonymous") { if (value.name && value.name !== "anonymous") {
@ -73,7 +85,7 @@ function createFunctionString(value: Function, _ctx: ConsoleContext): string {
return `[${cstrName}]`; return `[${cstrName}]`;
} }
interface IterablePrintConfig<T> { interface InspectIterableOptions<T> {
typeName: string; typeName: string;
displayName: string; displayName: string;
delims: [string, string]; delims: [string, string];
@ -81,23 +93,24 @@ interface IterablePrintConfig<T> {
entry: [unknown, T], entry: [unknown, T],
ctx: ConsoleContext, ctx: ConsoleContext,
level: number, level: number,
maxLevel: number, inspectOptions: Required<InspectOptions>,
next: () => IteratorResult<[unknown, T], unknown> next: () => IteratorResult<[unknown, T], unknown>
) => string; ) => string;
group: boolean; group: boolean;
sort: boolean;
} }
type IterableEntries<T> = Iterable<T> & { type IterableEntries<T> = Iterable<T> & {
entries(): IterableIterator<[unknown, T]>; entries(): IterableIterator<[unknown, T]>;
}; };
function createIterableString<T>( function inspectIterable<T>(
value: IterableEntries<T>, value: IterableEntries<T>,
ctx: ConsoleContext, ctx: ConsoleContext,
level: number, level: number,
maxLevel: number, options: InspectIterableOptions<T>,
config: IterablePrintConfig<T> inspectOptions: Required<InspectOptions>
): string { ): string {
if (level >= maxLevel) { if (level >= inspectOptions.depth) {
return cyan(`[${config.typeName}]`); return cyan(`[${options.typeName}]`);
} }
ctx.add(value); ctx.add(value);
@ -109,42 +122,57 @@ function createIterableString<T>(
return iter.next(); return iter.next();
}; };
for (const el of iter) { for (const el of iter) {
if (entriesLength < MAX_ITERABLE_LENGTH) { if (entriesLength < inspectOptions.iterableLimit) {
entries.push( entries.push(
config.entryHandler(el, ctx, level + 1, maxLevel, next.bind(iter)) options.entryHandler(
el,
ctx,
level + 1,
inspectOptions,
next.bind(iter)
)
); );
} }
entriesLength++; entriesLength++;
} }
ctx.delete(value); ctx.delete(value);
if (entriesLength > MAX_ITERABLE_LENGTH) { if (options.sort) {
const nmore = entriesLength - MAX_ITERABLE_LENGTH; entries.sort();
}
if (entriesLength > inspectOptions.iterableLimit) {
const nmore = entriesLength - inspectOptions.iterableLimit;
entries.push(`... ${nmore} more items`); entries.push(`... ${nmore} more items`);
} }
const iPrefix = `${config.displayName ? config.displayName + " " : ""}`; const iPrefix = `${options.displayName ? options.displayName + " " : ""}`;
const initIndentation = `\n${DEFAULT_INDENT.repeat(level + 1)}`; const initIndentation = `\n${DEFAULT_INDENT.repeat(level + 1)}`;
const entryIndentation = `,\n${DEFAULT_INDENT.repeat(level + 1)}`; const entryIndentation = `,\n${DEFAULT_INDENT.repeat(level + 1)}`;
const closingIndentation = `\n${DEFAULT_INDENT.repeat(level)}`; const closingIndentation = `${
inspectOptions.trailingComma ? "," : ""
}\n${DEFAULT_INDENT.repeat(level)}`;
let iContent: string; let iContent: string;
if (config.group && entries.length > MIN_GROUP_LENGTH) { if (options.group && entries.length > MIN_GROUP_LENGTH) {
const groups = groupEntries(entries, level, value); const groups = groupEntries(entries, level, value);
iContent = `${initIndentation}${groups.join( iContent = `${initIndentation}${groups.join(
entryIndentation entryIndentation
)}${closingIndentation}`; )}${closingIndentation}`;
} else { } else {
iContent = entries.length === 0 ? "" : ` ${entries.join(", ")} `; iContent = entries.length === 0 ? "" : ` ${entries.join(", ")} `;
if (stripColor(iContent).length > LINE_BREAKING_LENGTH) { if (
stripColor(iContent).length > LINE_BREAKING_LENGTH ||
!inspectOptions.compact
) {
iContent = `${initIndentation}${entries.join( iContent = `${initIndentation}${entries.join(
entryIndentation entryIndentation
)}${closingIndentation}`; )}${closingIndentation}`;
} }
} }
return `${iPrefix}${config.delims[0]}${iContent}${config.delims[1]}`; return `${iPrefix}${options.delims[0]}${iContent}${options.delims[1]}`;
} }
// Ported from Node.js // Ported from Node.js
@ -152,12 +180,13 @@ function createIterableString<T>(
function groupEntries<T>( function groupEntries<T>(
entries: string[], entries: string[],
level: number, level: number,
value: Iterable<T> value: Iterable<T>,
iterableLimit = 100
): string[] { ): string[] {
let totalLength = 0; let totalLength = 0;
let maxLength = 0; let maxLength = 0;
let entriesLength = entries.length; let entriesLength = entries.length;
if (MAX_ITERABLE_LENGTH < entriesLength) { if (iterableLimit < entriesLength) {
// This makes sure the "... n more items" part is not taken into account. // This makes sure the "... n more items" part is not taken into account.
entriesLength--; entriesLength--;
} }
@ -254,7 +283,7 @@ function groupEntries<T>(
} }
tmp.push(str); tmp.push(str);
} }
if (MAX_ITERABLE_LENGTH < entries.length) { if (iterableLimit < entries.length) {
tmp.push(entries[entriesLength]); tmp.push(entries[entriesLength]);
} }
entries = tmp; entries = tmp;
@ -262,11 +291,11 @@ function groupEntries<T>(
return entries; return entries;
} }
function stringify( function inspectValue(
value: unknown, value: unknown,
ctx: ConsoleContext, ctx: ConsoleContext,
level: number, level: number,
maxLevel: number inspectOptions: Required<InspectOptions>
): string { ): string {
switch (typeof value) { switch (typeof value) {
case "string": case "string":
@ -283,7 +312,7 @@ function stringify(
case "bigint": // Bigints are yellow case "bigint": // Bigints are yellow
return yellow(`${value}n`); return yellow(`${value}n`);
case "function": // Function string is cyan case "function": // Function string is cyan
return cyan(createFunctionString(value as Function, ctx)); return cyan(inspectFunction(value as Function, ctx));
case "object": // null is bold case "object": // null is bold
if (value === null) { if (value === null) {
return bold("null"); return bold("null");
@ -294,7 +323,7 @@ function stringify(
return cyan("[Circular]"); return cyan("[Circular]");
} }
return createObjectString(value, ctx, level, maxLevel); return inspectObject(value, ctx, level, inspectOptions);
default: default:
// Not implemented is red // Not implemented is red
return red("[Not Implemented]"); return red("[Not Implemented]");
@ -320,11 +349,11 @@ function quoteString(string: string): string {
} }
// Print strings when they are inside of arrays or objects with quotes // Print strings when they are inside of arrays or objects with quotes
function stringifyWithQuotes( function inspectValueWithQuotes(
value: unknown, value: unknown,
ctx: ConsoleContext, ctx: ConsoleContext,
level: number, level: number,
maxLevel: number inspectOptions: Required<InspectOptions>
): string { ): string {
switch (typeof value) { switch (typeof value) {
case "string": case "string":
@ -334,21 +363,21 @@ function stringifyWithQuotes(
: value; : value;
return green(quoteString(trunc)); // Quoted strings are green return green(quoteString(trunc)); // Quoted strings are green
default: default:
return stringify(value, ctx, level, maxLevel); return inspectValue(value, ctx, level, inspectOptions);
} }
} }
function createArrayString( function inspectArray(
value: unknown[], value: unknown[],
ctx: ConsoleContext, ctx: ConsoleContext,
level: number, level: number,
maxLevel: number inspectOptions: Required<InspectOptions>
): string { ): string {
const printConfig: IterablePrintConfig<unknown> = { const options: InspectIterableOptions<unknown> = {
typeName: "Array", typeName: "Array",
displayName: "", displayName: "",
delims: ["[", "]"], delims: ["[", "]"],
entryHandler: (entry, ctx, level, maxLevel, next): string => { entryHandler: (entry, ctx, level, inspectOptions, next): string => {
const [index, val] = entry as [number, unknown]; const [index, val] = entry as [number, unknown];
let i = index; let i = index;
if (!value.hasOwnProperty(i)) { if (!value.hasOwnProperty(i)) {
@ -361,117 +390,127 @@ function createArrayString(
const ending = emptyItems > 1 ? "s" : ""; const ending = emptyItems > 1 ? "s" : "";
return dim(`<${emptyItems} empty item${ending}>`); return dim(`<${emptyItems} empty item${ending}>`);
} else { } else {
return stringifyWithQuotes(val, ctx, level, maxLevel); return inspectValueWithQuotes(val, ctx, level, inspectOptions);
} }
}, },
group: true, group: inspectOptions.compact,
sort: false,
}; };
return createIterableString(value, ctx, level, maxLevel, printConfig); return inspectIterable(value, ctx, level, options, inspectOptions);
} }
function createTypedArrayString( function inspectTypedArray(
typedArrayName: string, typedArrayName: string,
value: TypedArray, value: TypedArray,
ctx: ConsoleContext, ctx: ConsoleContext,
level: number, level: number,
maxLevel: number inspectOptions: Required<InspectOptions>
): string { ): string {
const valueLength = value.length; const valueLength = value.length;
const printConfig: IterablePrintConfig<unknown> = { const options: InspectIterableOptions<unknown> = {
typeName: typedArrayName, typeName: typedArrayName,
displayName: `${typedArrayName}(${valueLength})`, displayName: `${typedArrayName}(${valueLength})`,
delims: ["[", "]"], delims: ["[", "]"],
entryHandler: (entry, ctx, level, maxLevel): string => { entryHandler: (entry, ctx, level, inspectOptions): string => {
const val = entry[1]; const val = entry[1];
return stringifyWithQuotes(val, ctx, level + 1, maxLevel); return inspectValueWithQuotes(val, ctx, level + 1, inspectOptions);
}, },
group: true, group: inspectOptions.compact,
sort: false,
}; };
return createIterableString(value, ctx, level, maxLevel, printConfig); return inspectIterable(value, ctx, level, options, inspectOptions);
} }
function createSetString( function inspectSet(
value: Set<unknown>, value: Set<unknown>,
ctx: ConsoleContext, ctx: ConsoleContext,
level: number, level: number,
maxLevel: number inspectOptions: Required<InspectOptions>
): string { ): string {
const printConfig: IterablePrintConfig<unknown> = { const options: InspectIterableOptions<unknown> = {
typeName: "Set", typeName: "Set",
displayName: "Set", displayName: "Set",
delims: ["{", "}"], delims: ["{", "}"],
entryHandler: (entry, ctx, level, maxLevel): string => { entryHandler: (entry, ctx, level, inspectOptions): string => {
const val = entry[1]; const val = entry[1];
return stringifyWithQuotes(val, ctx, level + 1, maxLevel); return inspectValueWithQuotes(val, ctx, level + 1, inspectOptions);
}, },
group: false, group: false,
sort: inspectOptions.sorted,
}; };
return createIterableString(value, ctx, level, maxLevel, printConfig); return inspectIterable(value, ctx, level, options, inspectOptions);
} }
function createMapString( function inspectMap(
value: Map<unknown, unknown>, value: Map<unknown, unknown>,
ctx: ConsoleContext, ctx: ConsoleContext,
level: number, level: number,
maxLevel: number inspectOptions: Required<InspectOptions>
): string { ): string {
const printConfig: IterablePrintConfig<[unknown]> = { const options: InspectIterableOptions<[unknown]> = {
typeName: "Map", typeName: "Map",
displayName: "Map", displayName: "Map",
delims: ["{", "}"], delims: ["{", "}"],
entryHandler: (entry, ctx, level, maxLevel): string => { entryHandler: (entry, ctx, level, inspectOptions): string => {
const [key, val] = entry; const [key, val] = entry;
return `${stringifyWithQuotes( return `${inspectValueWithQuotes(
key, key,
ctx, ctx,
level + 1, level + 1,
maxLevel inspectOptions
)} => ${stringifyWithQuotes(val, ctx, level + 1, maxLevel)}`; )} => ${inspectValueWithQuotes(val, ctx, level + 1, inspectOptions)}`;
}, },
group: false, group: false,
sort: inspectOptions.sorted,
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any return inspectIterable(
return createIterableString(value as any, ctx, level, maxLevel, printConfig); // eslint-disable-next-line @typescript-eslint/no-explicit-any
value as any,
ctx,
level,
options,
inspectOptions
);
} }
function createWeakSetString(): string { function inspectWeakSet(): string {
return `WeakSet { ${cyan("[items unknown]")} }`; // as seen in Node, with cyan color return `WeakSet { ${cyan("[items unknown]")} }`; // as seen in Node, with cyan color
} }
function createWeakMapString(): string { function inspectWeakMap(): string {
return `WeakMap { ${cyan("[items unknown]")} }`; // as seen in Node, with cyan color return `WeakMap { ${cyan("[items unknown]")} }`; // as seen in Node, with cyan color
} }
function createDateString(value: Date): string { function inspectDate(value: Date): string {
// without quotes, ISO format, in magenta like before // without quotes, ISO format, in magenta like before
return magenta(isInvalidDate(value) ? "Invalid Date" : value.toISOString()); return magenta(isInvalidDate(value) ? "Invalid Date" : value.toISOString());
} }
function createRegExpString(value: RegExp): string { function inspectRegExp(value: RegExp): string {
return red(value.toString()); // RegExps are red return red(value.toString()); // RegExps are red
} }
/* eslint-disable @typescript-eslint/ban-types */ /* eslint-disable @typescript-eslint/ban-types */
function createStringWrapperString(value: String): string { function inspectStringObject(value: String): string {
return cyan(`[String: "${value.toString()}"]`); // wrappers are in cyan return cyan(`[String: "${value.toString()}"]`); // wrappers are in cyan
} }
function createBooleanWrapperString(value: Boolean): string { function inspectBooleanObject(value: Boolean): string {
return cyan(`[Boolean: ${value.toString()}]`); // wrappers are in cyan return cyan(`[Boolean: ${value.toString()}]`); // wrappers are in cyan
} }
function createNumberWrapperString(value: Number): string { function inspectNumberObject(value: Number): string {
return cyan(`[Number: ${value.toString()}]`); // wrappers are in cyan return cyan(`[Number: ${value.toString()}]`); // wrappers are in cyan
} }
/* eslint-enable @typescript-eslint/ban-types */ /* eslint-enable @typescript-eslint/ban-types */
function createPromiseString( function inspectPromise(
value: Promise<unknown>, value: Promise<unknown>,
ctx: ConsoleContext, ctx: ConsoleContext,
level: number, level: number,
maxLevel: number inspectOptions: Required<InspectOptions>
): string { ): string {
const [state, result] = Deno.core.getPromiseDetails(value); const [state, result] = Deno.core.getPromiseDetails(value);
@ -482,11 +521,11 @@ function createPromiseString(
const prefix = const prefix =
state === PromiseState.Fulfilled ? "" : `${red("<rejected>")} `; state === PromiseState.Fulfilled ? "" : `${red("<rejected>")} `;
const str = `${prefix}${stringifyWithQuotes( const str = `${prefix}${inspectValueWithQuotes(
result, result,
ctx, ctx,
level + 1, level + 1,
maxLevel inspectOptions
)}`; )}`;
if (str.length + PROMISE_STRING_BASE_LENGTH > LINE_BREAKING_LENGTH) { if (str.length + PROMISE_STRING_BASE_LENGTH > LINE_BREAKING_LENGTH) {
@ -498,13 +537,13 @@ function createPromiseString(
// TODO: Proxy // TODO: Proxy
function createRawObjectString( function inspectRawObject(
value: Record<string, unknown>, value: Record<string, unknown>,
ctx: ConsoleContext, ctx: ConsoleContext,
level: number, level: number,
maxLevel: number inspectOptions: Required<InspectOptions>
): string { ): string {
if (level >= maxLevel) { if (level >= inspectOptions.depth) {
return cyan("[Object]"); // wrappers are in cyan return cyan("[Object]"); // wrappers are in cyan
} }
ctx.add(value); ctx.add(value);
@ -525,20 +564,31 @@ function createRawObjectString(
const entries: string[] = []; const entries: string[] = [];
const stringKeys = Object.keys(value); const stringKeys = Object.keys(value);
const symbolKeys = Object.getOwnPropertySymbols(value); const symbolKeys = Object.getOwnPropertySymbols(value);
if (inspectOptions.sorted) {
stringKeys.sort();
symbolKeys.sort((s1, s2) =>
(s1.description ?? "").localeCompare(s2.description ?? "")
);
}
for (const key of stringKeys) { for (const key of stringKeys) {
entries.push( entries.push(
`${key}: ${stringifyWithQuotes(value[key], ctx, level + 1, maxLevel)}` `${key}: ${inspectValueWithQuotes(
value[key],
ctx,
level + 1,
inspectOptions
)}`
); );
} }
for (const key of symbolKeys) { for (const key of symbolKeys) {
entries.push( entries.push(
`${key.toString()}: ${stringifyWithQuotes( `${key.toString()}: ${inspectValueWithQuotes(
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
value[key as any], value[key as any],
ctx, ctx,
level + 1, level + 1,
maxLevel inspectOptions
)}` )}`
); );
} }
@ -550,12 +600,12 @@ function createRawObjectString(
if (entries.length === 0) { if (entries.length === 0) {
baseString = "{}"; baseString = "{}";
} else if (totalLength > LINE_BREAKING_LENGTH) { } else if (totalLength > LINE_BREAKING_LENGTH || !inspectOptions.compact) {
const entryIndent = DEFAULT_INDENT.repeat(level + 1); const entryIndent = DEFAULT_INDENT.repeat(level + 1);
const closingIndent = DEFAULT_INDENT.repeat(level); const closingIndent = DEFAULT_INDENT.repeat(level);
baseString = `{\n${entryIndent}${entries.join( baseString = `{\n${entryIndent}${entries.join(`,\n${entryIndent}`)}${
`,\n${entryIndent}` inspectOptions.trailingComma ? "," : ""
)}\n${closingIndent}}`; }\n${closingIndent}}`;
} else { } else {
baseString = `{ ${entries.join(", ")} }`; baseString = `{ ${entries.join(", ")} }`;
} }
@ -567,9 +617,11 @@ function createRawObjectString(
return baseString; return baseString;
} }
function createObjectString( function inspectObject(
value: {}, value: {},
...args: [ConsoleContext, number, number] consoleContext: ConsoleContext,
level: number,
inspectOptions: Required<InspectOptions>
): string { ): string {
if (customInspect in value && typeof value[customInspect] === "function") { if (customInspect in value && typeof value[customInspect] === "function") {
try { try {
@ -579,43 +631,46 @@ function createObjectString(
if (value instanceof Error) { if (value instanceof Error) {
return String(value.stack); return String(value.stack);
} else if (Array.isArray(value)) { } else if (Array.isArray(value)) {
return createArrayString(value, ...args); return inspectArray(value, consoleContext, level, inspectOptions);
} else if (value instanceof Number) { } else if (value instanceof Number) {
return createNumberWrapperString(value); return inspectNumberObject(value);
} else if (value instanceof Boolean) { } else if (value instanceof Boolean) {
return createBooleanWrapperString(value); return inspectBooleanObject(value);
} else if (value instanceof String) { } else if (value instanceof String) {
return createStringWrapperString(value); return inspectStringObject(value);
} else if (value instanceof Promise) { } else if (value instanceof Promise) {
return createPromiseString(value, ...args); return inspectPromise(value, consoleContext, level, inspectOptions);
} else if (value instanceof RegExp) { } else if (value instanceof RegExp) {
return createRegExpString(value); return inspectRegExp(value);
} else if (value instanceof Date) { } else if (value instanceof Date) {
return createDateString(value); return inspectDate(value);
} else if (value instanceof Set) { } else if (value instanceof Set) {
return createSetString(value, ...args); return inspectSet(value, consoleContext, level, inspectOptions);
} else if (value instanceof Map) { } else if (value instanceof Map) {
return createMapString(value, ...args); return inspectMap(value, consoleContext, level, inspectOptions);
} else if (value instanceof WeakSet) { } else if (value instanceof WeakSet) {
return createWeakSetString(); return inspectWeakSet();
} else if (value instanceof WeakMap) { } else if (value instanceof WeakMap) {
return createWeakMapString(); return inspectWeakMap();
} else if (isTypedArray(value)) { } else if (isTypedArray(value)) {
return createTypedArrayString( return inspectTypedArray(
Object.getPrototypeOf(value).constructor.name, Object.getPrototypeOf(value).constructor.name,
value, value,
...args consoleContext,
level,
inspectOptions
); );
} else { } else {
// Otherwise, default object formatting // Otherwise, default object formatting
return createRawObjectString(value, ...args); return inspectRawObject(value, consoleContext, level, inspectOptions);
} }
} }
export function stringifyArgs( export function inspectArgs(
args: unknown[], args: unknown[],
{ depth = DEFAULT_MAX_DEPTH, indentLevel = 0 }: InspectOptions = {} inspectOptions: InspectOptions = {}
): string { ): string {
const rInspectOptions = { ...DEFAULT_INSPECT_OPTIONS, ...inspectOptions };
const first = args[0]; const first = args[0];
let a = 0; let a = 0;
let str = ""; let str = "";
@ -658,7 +713,12 @@ export function stringifyArgs(
case CHAR_LOWERCASE_O: case CHAR_LOWERCASE_O:
case CHAR_UPPERCASE_O: case CHAR_UPPERCASE_O:
// format as an object // format as an object
tempStr = stringify(args[++a], new Set<unknown>(), 0, depth); tempStr = inspectValue(
args[++a],
new Set<unknown>(),
0,
rInspectOptions
);
break; break;
case CHAR_PERCENT: case CHAR_PERCENT:
str += first.slice(lastPos, i); str += first.slice(lastPos, i);
@ -701,14 +761,14 @@ export function stringifyArgs(
str += value; str += value;
} else { } else {
// use default maximum depth for null or undefined argument // use default maximum depth for null or undefined argument
str += stringify(value, new Set<unknown>(), 0, depth); str += inspectValue(value, new Set<unknown>(), 0, rInspectOptions);
} }
join = " "; join = " ";
a++; a++;
} }
if (indentLevel > 0) { if (rInspectOptions.indentLevel > 0) {
const groupIndent = DEFAULT_INDENT.repeat(indentLevel); const groupIndent = DEFAULT_INDENT.repeat(rInspectOptions.indentLevel);
if (str.indexOf("\n") !== -1) { if (str.indexOf("\n") !== -1) {
str = str.replace(/\n/g, `\n${groupIndent}`); str = str.replace(/\n/g, `\n${groupIndent}`);
} }
@ -745,7 +805,7 @@ export class Console {
log = (...args: unknown[]): void => { log = (...args: unknown[]): void => {
this.#printFunc( this.#printFunc(
stringifyArgs(args, { inspectArgs(args, {
indentLevel: this.indentLevel, indentLevel: this.indentLevel,
}) + "\n", }) + "\n",
false false
@ -756,14 +816,14 @@ export class Console {
info = this.log; info = this.log;
dir = (obj: unknown, options: InspectOptions = {}): void => { dir = (obj: unknown, options: InspectOptions = {}): void => {
this.#printFunc(stringifyArgs([obj], options) + "\n", false); this.#printFunc(inspectArgs([obj], options) + "\n", false);
}; };
dirxml = this.dir; dirxml = this.dir;
warn = (...args: unknown[]): void => { warn = (...args: unknown[]): void => {
this.#printFunc( this.#printFunc(
stringifyArgs(args, { inspectArgs(args, {
indentLevel: this.indentLevel, indentLevel: this.indentLevel,
}) + "\n", }) + "\n",
true true
@ -832,7 +892,10 @@ export class Console {
const values: string[] = []; const values: string[] = [];
const stringifyValue = (value: unknown): string => const stringifyValue = (value: unknown): string =>
stringifyWithQuotes(value, new Set<unknown>(), 0, 1); inspectValueWithQuotes(value, new Set<unknown>(), 0, {
...DEFAULT_INSPECT_OPTIONS,
depth: 1,
});
const toTable = (header: string[], body: string[][]): void => const toTable = (header: string[], body: string[][]): void =>
this.log(cliTable(header, body)); this.log(cliTable(header, body));
const createColumn = (value: unknown, shift?: number): string[] => [ const createColumn = (value: unknown, shift?: number): string[] => [
@ -966,7 +1029,7 @@ export class Console {
}; };
trace = (...args: unknown[]): void => { trace = (...args: unknown[]): void => {
const message = stringifyArgs(args, { indentLevel: 0 }); const message = inspectArgs(args, { indentLevel: 0 });
const err = { const err = {
name: "Trace", name: "Trace",
message, message,
@ -980,19 +1043,24 @@ export class Console {
} }
} }
export const customInspect = Symbol("Deno.symbols.customInspect"); export const customInspect = Symbol("Deno.customInspect");
export function inspect( export function inspect(
value: unknown, value: unknown,
{ depth = DEFAULT_MAX_DEPTH }: InspectOptions = {} inspectOptions: InspectOptions = {}
): string { ): string {
if (typeof value === "string") { if (typeof value === "string") {
return value; return value;
} else { } else {
return stringify(value, new Set<unknown>(), 0, depth); return inspectValue(value, new Set<unknown>(), 0, {
...DEFAULT_INSPECT_OPTIONS,
...inspectOptions,
// TODO(nayeemrmn): Indent level is not supported.
indentLevel: 0,
});
} }
} }
// Expose these fields to internalObject for tests. // Expose these fields to internalObject for tests.
exposeForTest("Console", Console); exposeForTest("Console", Console);
exposeForTest("stringifyArgs", stringifyArgs); exposeForTest("inspectArgs", inspectArgs);

View file

@ -11,22 +11,15 @@
import { assert, assertEquals, unitTest } from "./test_util.ts"; import { assert, assertEquals, unitTest } from "./test_util.ts";
import { stripColor } from "../../../std/fmt/colors.ts"; import { stripColor } from "../../../std/fmt/colors.ts";
// Some of these APIs aren't exposed in the types and so we have to cast to any
// in order to "trick" TypeScript.
const {
inspect,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} = Deno as any;
const customInspect = Deno.customInspect; const customInspect = Deno.customInspect;
const { const {
Console, Console,
stringifyArgs, inspectArgs,
// @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol // @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
} = Deno[Deno.internal]; } = Deno[Deno.internal];
function stringify(...args: unknown[]): string { function stringify(...args: unknown[]): string {
return stripColor(stringifyArgs(args).replace(/\n$/, "")); return stripColor(inspectArgs(args).replace(/\n$/, ""));
} }
// test cases from web-platform-tests // test cases from web-platform-tests
@ -238,7 +231,7 @@ unitTest(function consoleTestStringifyCircular(): void {
'TAG { str: 1, Symbol(sym): 2, Symbol(Symbol.toStringTag): "TAG" }' 'TAG { str: 1, Symbol(sym): 2, Symbol(Symbol.toStringTag): "TAG" }'
); );
// test inspect is working the same // test inspect is working the same
assertEquals(stripColor(inspect(nestedObj)), nestedObjExpected); assertEquals(stripColor(Deno.inspect(nestedObj)), nestedObjExpected);
}); });
/* eslint-enable @typescript-eslint/explicit-function-return-type */ /* eslint-enable @typescript-eslint/explicit-function-return-type */
@ -246,24 +239,21 @@ unitTest(function consoleTestStringifyWithDepth(): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const nestedObj: any = { a: { b: { c: { d: { e: { f: 42 } } } } } }; const nestedObj: any = { a: { b: { c: { d: { e: { f: 42 } } } } } };
assertEquals( assertEquals(
stripColor(stringifyArgs([nestedObj], { depth: 3 })), stripColor(inspectArgs([nestedObj], { depth: 3 })),
"{ a: { b: { c: [Object] } } }" "{ a: { b: { c: [Object] } } }"
); );
assertEquals( assertEquals(
stripColor(stringifyArgs([nestedObj], { depth: 4 })), stripColor(inspectArgs([nestedObj], { depth: 4 })),
"{ a: { b: { c: { d: [Object] } } } }" "{ a: { b: { c: { d: [Object] } } } }"
); );
assertEquals(stripColor(inspectArgs([nestedObj], { depth: 0 })), "[Object]");
assertEquals( assertEquals(
stripColor(stringifyArgs([nestedObj], { depth: 0 })), stripColor(inspectArgs([nestedObj])),
"[Object]"
);
assertEquals(
stripColor(stringifyArgs([nestedObj])),
"{ a: { b: { c: { d: [Object] } } } }" "{ a: { b: { c: { d: [Object] } } } }"
); );
// test inspect is working the same way // test inspect is working the same way
assertEquals( assertEquals(
stripColor(inspect(nestedObj, { depth: 4 })), stripColor(Deno.inspect(nestedObj, { depth: 4 })),
"{ a: { b: { c: { d: [Object] } } } }" "{ a: { b: { c: { d: [Object] } } } }"
); );
}); });
@ -653,7 +643,7 @@ unitTest(function consoleTestWithCustomInspectorError(): void {
assertEquals(stringify(new B({ a: "a" })), "a"); assertEquals(stringify(new B({ a: "a" })), "a");
assertEquals( assertEquals(
stringify(B.prototype), stringify(B.prototype),
"{ Symbol(Deno.symbols.customInspect): [Function: [Deno.symbols.customInspect]] }" "{ Symbol(Deno.customInspect): [Function: [Deno.customInspect]] }"
); );
}); });
@ -1175,3 +1165,111 @@ unitTest(function consoleTrace(): void {
assert(err.toString().includes("Trace: custom message")); assert(err.toString().includes("Trace: custom message"));
}); });
}); });
unitTest(function inspectSorted(): void {
assertEquals(
Deno.inspect({ b: 2, a: 1 }, { sorted: true }),
"{ a: 1, b: 2 }"
);
assertEquals(
Deno.inspect(new Set(["b", "a"]), { sorted: true }),
`Set { "a", "b" }`
);
assertEquals(
Deno.inspect(
new Map([
["b", 2],
["a", 1],
]),
{ sorted: true }
),
`Map { "a" => 1, "b" => 2 }`
);
});
unitTest(function inspectTrailingComma(): void {
assertEquals(
Deno.inspect(
[
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
],
{ trailingComma: true }
),
`[
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
]`
);
assertEquals(
Deno.inspect(
{
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: 1,
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: 2,
},
{ trailingComma: true }
),
`{
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: 1,
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: 2,
}`
);
assertEquals(
Deno.inspect(
new Set([
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
]),
{ trailingComma: true }
),
`Set {
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
}`
);
assertEquals(
Deno.inspect(
new Map([
["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 1],
["bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 2],
]),
{ trailingComma: true }
),
`Map {
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" => 1,
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" => 2,
}`
);
});
unitTest(function inspectCompact(): void {
assertEquals(
Deno.inspect({ a: 1, b: 2 }, { compact: false }),
`{
a: 1,
b: 2
}`
);
});
unitTest(function inspectIterableLimit(): void {
assertEquals(
Deno.inspect(["a", "b", "c"], { iterableLimit: 2 }),
`[ "a", "b", ... 1 more items ]`
);
assertEquals(
Deno.inspect(new Set(["a", "b", "c"]), { iterableLimit: 2 }),
`Set { "a", "b", ... 1 more items }`
);
assertEquals(
Deno.inspect(
new Map([
["a", 1],
["b", 2],
["c", 3],
]),
{ iterableLimit: 2 }
),
`Map { "a" => 1, "b" => 2, ... 1 more items }`
);
});

View file

@ -6,7 +6,7 @@ import {
assertStringContains, assertStringContains,
} from "./test_util.ts"; } from "./test_util.ts";
const { const {
stringifyArgs, inspectArgs,
// @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol // @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
} = Deno[Deno.internal]; } = Deno[Deno.internal];
@ -402,7 +402,7 @@ unitTest(function toStringShouldBeWebCompatibility(): void {
}); });
function stringify(...args: unknown[]): string { function stringify(...args: unknown[]): string {
return stringifyArgs(args).replace(/\n$/, ""); return inspectArgs(args).replace(/\n$/, "");
} }
unitTest(function customInspectReturnsCorrectHeadersFormat(): void { unitTest(function customInspectReturnsCorrectHeadersFormat(): void {

View file

@ -3,8 +3,8 @@ import { unitTest, assert } from "./test_util.ts";
unitTest(function internalsExists(): void { unitTest(function internalsExists(): void {
const { const {
stringifyArgs, inspectArgs,
// @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol // @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
} = Deno[Deno.internal]; } = Deno[Deno.internal];
assert(!!stringifyArgs); assert(!!inspectArgs);
}); });

View file

@ -21,7 +21,10 @@ Don't link to / import any module whose path:
- Is that of a test module or test data: `test.ts`, `foo_test.ts`, - Is that of a test module or test data: `test.ts`, `foo_test.ts`,
`testdata/bar.txt`. `testdata/bar.txt`.
No stability is guaranteed for these files. Don't import any symbol with an underscore prefix: `export function _baz() {}`.
These elements are not considered part of the public API, thus no stability is
guaranteed for them.
## Documentation ## Documentation
@ -29,7 +32,7 @@ To browse documentation for modules:
- Go to https://deno.land/std/. - Go to https://deno.land/std/.
- Navigate to any module of interest. - Navigate to any module of interest.
- Click the "DOCUMENTATION" link. - Click "View Documentation".
## Contributing ## Contributing

View file

@ -19,8 +19,16 @@ export class AssertionError extends Error {
} }
} }
function format(v: unknown): string { export function _format(v: unknown): string {
let string = globalThis.Deno ? Deno.inspect(v) : String(v); let string = globalThis.Deno
? Deno.inspect(v, {
depth: Infinity,
sorted: true,
trailingComma: true,
compact: false,
iterableLimit: Infinity,
})
: String(v);
if (typeof v == "string") { if (typeof v == "string") {
string = `"${string.replace(/(?=["\\])/g, "\\")}"`; string = `"${string.replace(/(?=["\\])/g, "\\")}"`;
} }
@ -167,8 +175,8 @@ export function assertEquals(
return; return;
} }
let message = ""; let message = "";
const actualString = format(actual); const actualString = _format(actual);
const expectedString = format(expected); const expectedString = _format(expected);
try { try {
const diffResult = diff( const diffResult = diff(
actualString.split("\n"), actualString.split("\n"),
@ -248,13 +256,13 @@ export function assertStrictEquals<T>(
if (msg) { if (msg) {
message = msg; message = msg;
} else { } else {
const actualString = format(actual); const actualString = _format(actual);
const expectedString = format(expected); const expectedString = _format(expected);
if (actualString === expectedString) { if (actualString === expectedString) {
const withOffset = actualString const withOffset = actualString
.split("\n") .split("\n")
.map((l) => ` ${l}`) .map((l) => ` ${l}`)
.join("\n"); .join("\n");
message = `Values have the same structure but are not reference-equal:\n\n${red( message = `Values have the same structure but are not reference-equal:\n\n${red(
withOffset withOffset
@ -335,9 +343,9 @@ export function assertArrayContains(
return; return;
} }
if (!msg) { if (!msg) {
msg = `actual: "${format(actual)}" expected to contain: "${format( msg = `actual: "${_format(actual)}" expected to contain: "${_format(
expected expected
)}"\nmissing: ${format(missing)}`; )}"\nmissing: ${_format(missing)}`;
} }
throw new AssertionError(msg); throw new AssertionError(msg);
} }

View file

@ -1,5 +1,6 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
import { import {
_format,
assert, assert,
assertNotEquals, assertNotEquals,
assertStringContains, assertStringContains,
@ -15,7 +16,7 @@ import {
unimplemented, unimplemented,
unreachable, unreachable,
} from "./asserts.ts"; } from "./asserts.ts";
import { red, green, gray, bold, yellow } from "../fmt/colors.ts"; import { red, green, gray, bold, yellow, stripColor } from "../fmt/colors.ts";
Deno.test("testingEqual", function (): void { Deno.test("testingEqual", function (): void {
assert(equal("world", "world")); assert(equal("world", "world"));
@ -176,7 +177,23 @@ Deno.test("testingArrayContains", function (): void {
assertThrows( assertThrows(
(): void => assertArrayContains(fixtureObject, [{ deno: "node" }]), (): void => assertArrayContains(fixtureObject, [{ deno: "node" }]),
AssertionError, AssertionError,
`actual: "[ { deno: "luv" }, { deno: "Js" } ]" expected to contain: "[ { deno: "node" } ]"\nmissing: [ { deno: "node" } ]` `actual: "[
{
deno: "luv",
},
{
deno: "Js",
},
]" expected to contain: "[
{
deno: "node",
},
]"
missing: [
{
deno: "node",
},
]`
); );
}); });
@ -342,13 +359,13 @@ Deno.test({
assertThrows( assertThrows(
(): void => assertEquals([1, "2", 3], ["1", "2", 3]), (): void => assertEquals([1, "2", 3], ["1", "2", 3]),
AssertionError, AssertionError,
[ `
"Values are not equal:", [
...createHeader(), - 1,
removed(`- [ ${yellow("1")}, ${green('"2"')}, ${yellow("3")} ]`), + "1",
added(`+ [ ${green('"1"')}, ${green('"2"')}, ${yellow("3")} ]`), "2",
"", 3,
].join("\n") ]`
); );
}, },
}); });
@ -359,17 +376,16 @@ Deno.test({
assertThrows( assertThrows(
(): void => assertEquals({ a: 1, b: "2", c: 3 }, { a: 1, b: 2, c: [3] }), (): void => assertEquals({ a: 1, b: "2", c: 3 }, { a: 1, b: 2, c: [3] }),
AssertionError, AssertionError,
[ `
"Values are not equal:", {
...createHeader(), a: 1,
removed( + b: 2,
`- { a: ${yellow("1")}, b: ${green('"2"')}, c: ${yellow("3")} }` + c: [
), + 3,
added( + ],
`+ { a: ${yellow("1")}, b: ${yellow("2")}, c: [ ${yellow("3")} ] }` - b: "2",
), - c: 3,
"", }`
].join("\n")
); );
}, },
}); });
@ -418,13 +434,14 @@ Deno.test({
assertThrows( assertThrows(
(): void => assertStrictEquals({ a: 1, b: 2 }, { a: 1, c: [3] }), (): void => assertStrictEquals({ a: 1, b: 2 }, { a: 1, c: [3] }),
AssertionError, AssertionError,
[ `
"Values are not strictly equal:", {
...createHeader(), a: 1,
removed(`- { a: ${yellow("1")}, b: ${yellow("2")} }`), + c: [
added(`+ { a: ${yellow("1")}, c: [ ${yellow("3")} ] }`), + 3,
"", + ],
].join("\n") - b: 2,
}`
); );
}, },
}); });
@ -435,10 +452,12 @@ Deno.test({
assertThrows( assertThrows(
(): void => assertStrictEquals({ a: 1, b: 2 }, { a: 1, b: 2 }), (): void => assertStrictEquals({ a: 1, b: 2 }, { a: 1, b: 2 }),
AssertionError, AssertionError,
[ `Values have the same structure but are not reference-equal:
"Values have the same structure but are not reference-equal:\n",
red(` { a: ${yellow("1")}, b: ${yellow("2")} }`), {
].join("\n") a: 1,
b: 2,
}`
); );
}, },
}); });
@ -535,3 +554,75 @@ Deno.test("Assert Throws Async Non-Error Fail", () => {
"A non-Error object was thrown or rejected." "A non-Error object was thrown or rejected."
); );
}); });
Deno.test("assertEquals diff for differently ordered objects", () => {
assertThrows(
() => {
assertEquals(
{
aaaaaaaaaaaaaaaaaaaaaaaa: 0,
bbbbbbbbbbbbbbbbbbbbbbbb: 0,
ccccccccccccccccccccccc: 0,
},
{
ccccccccccccccccccccccc: 1,
aaaaaaaaaaaaaaaaaaaaaaaa: 0,
bbbbbbbbbbbbbbbbbbbbbbbb: 0,
}
);
},
AssertionError,
`
{
aaaaaaaaaaaaaaaaaaaaaaaa: 0,
bbbbbbbbbbbbbbbbbbbbbbbb: 0,
- ccccccccccccccccccccccc: 0,
+ ccccccccccccccccccccccc: 1,
}`
);
});
// Check that the diff formatter overrides some default behaviours of
// `Deno.inspect()` which are problematic for diffing.
Deno.test("assert diff formatting", () => {
// Wraps objects into multiple lines even when they are small. Prints trailing
// commas.
assertEquals(
stripColor(_format({ a: 1, b: 2 })),
`{
a: 1,
b: 2,
}`
);
// Same for nested small objects.
assertEquals(
stripColor(_format([{ x: { a: 1, b: 2 }, y: ["a", "b"] }])),
`[
{
x: {
a: 1,
b: 2,
},
y: [
"a",
"b",
],
},
]`
);
// Grouping is disabled.
assertEquals(
stripColor(_format(["i", "i", "i", "i", "i", "i", "i"])),
`[
"i",
"i",
"i",
"i",
"i",
"i",
"i",
]`
);
});