diff --git a/cli/js/lib.deno.ns.d.ts b/cli/js/lib.deno.ns.d.ts index 41243d0caf..32b9bb39b4 100644 --- a/cli/js/lib.deno.ns.d.ts +++ b/cli/js/lib.deno.ns.d.ts @@ -2034,8 +2034,18 @@ declare namespace Deno { | PluginPermissionDescriptor | HrtimePermissionDescriptor; - interface InspectOptions { + export interface InspectOptions { + /** Traversal depth for nested objects. Defaults to 4. */ 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 diff --git a/cli/js/repl.ts b/cli/js/repl.ts index daa112e1ee..8a37d991f6 100644 --- a/cli/js/repl.ts +++ b/cli/js/repl.ts @@ -3,16 +3,16 @@ import { exit } from "./ops/os.ts"; import { core } from "./core.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 { close } from "./ops/resources.ts"; function replLog(...args: unknown[]): void { - core.print(stringifyArgs(args) + "\n"); + core.print(inspectArgs(args) + "\n"); } 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 diff --git a/cli/js/testing.ts b/cli/js/testing.ts index 0648683a47..d38c5427d2 100644 --- a/cli/js/testing.ts +++ b/cli/js/testing.ts @@ -2,7 +2,7 @@ import { gray, green, italic, red, yellow } from "./colors.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 { exposeForTest } from "./internals.ts"; import { TextEncoder } from "./web/text_encoding.ts"; @@ -205,7 +205,7 @@ function reportToConsole(message: TestMessage): void { for (const { name, error } of failures) { log(name); - log(stringifyArgs([error!])); + log(inspectArgs([error!])); log(""); } diff --git a/cli/js/web/console.ts b/cli/js/web/console.ts index 1f96bfe9d0..5ddf501140 100644 --- a/cli/js/web/console.ts +++ b/cli/js/web/console.ts @@ -16,16 +16,28 @@ import { } from "../colors.ts"; type ConsoleContext = Set; -type InspectOptions = Partial<{ - depth: number; - indentLevel: number; -}>; + +export interface InspectOptions { + depth?: number; + indentLevel?: number; + sorted?: boolean; + trailingComma?: boolean; + compact?: boolean; + iterableLimit?: number; +} + +const DEFAULT_INSPECT_OPTIONS: Required = { + depth: 4, + indentLevel: 0, + sorted: false, + trailingComma: false, + compact: true, + iterableLimit: 100, +}; const DEFAULT_INDENT = " "; // Default indent string -const DEFAULT_MAX_DEPTH = 4; // Default depth of logging nested objects const LINE_BREAKING_LENGTH = 80; -const MAX_ITERABLE_LENGTH = 100; const MIN_GROUP_LENGTH = 6; const STR_ABBREVIATE_SIZE = 100; // Char codes @@ -63,7 +75,7 @@ function getClassInstanceName(instance: unknown): string { return ""; } -function createFunctionString(value: Function, _ctx: ConsoleContext): string { +function inspectFunction(value: Function, _ctx: ConsoleContext): string { // Might be Function/AsyncFunction/GeneratorFunction const cstrName = Object.getPrototypeOf(value).constructor.name; if (value.name && value.name !== "anonymous") { @@ -73,7 +85,7 @@ function createFunctionString(value: Function, _ctx: ConsoleContext): string { return `[${cstrName}]`; } -interface IterablePrintConfig { +interface InspectIterableOptions { typeName: string; displayName: string; delims: [string, string]; @@ -81,23 +93,24 @@ interface IterablePrintConfig { entry: [unknown, T], ctx: ConsoleContext, level: number, - maxLevel: number, + inspectOptions: Required, next: () => IteratorResult<[unknown, T], unknown> ) => string; group: boolean; + sort: boolean; } type IterableEntries = Iterable & { entries(): IterableIterator<[unknown, T]>; }; -function createIterableString( +function inspectIterable( value: IterableEntries, ctx: ConsoleContext, level: number, - maxLevel: number, - config: IterablePrintConfig + options: InspectIterableOptions, + inspectOptions: Required ): string { - if (level >= maxLevel) { - return cyan(`[${config.typeName}]`); + if (level >= inspectOptions.depth) { + return cyan(`[${options.typeName}]`); } ctx.add(value); @@ -109,42 +122,57 @@ function createIterableString( return iter.next(); }; for (const el of iter) { - if (entriesLength < MAX_ITERABLE_LENGTH) { + if (entriesLength < inspectOptions.iterableLimit) { entries.push( - config.entryHandler(el, ctx, level + 1, maxLevel, next.bind(iter)) + options.entryHandler( + el, + ctx, + level + 1, + inspectOptions, + next.bind(iter) + ) ); } entriesLength++; } ctx.delete(value); - if (entriesLength > MAX_ITERABLE_LENGTH) { - const nmore = entriesLength - MAX_ITERABLE_LENGTH; + if (options.sort) { + entries.sort(); + } + + if (entriesLength > inspectOptions.iterableLimit) { + const nmore = entriesLength - inspectOptions.iterableLimit; 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 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; - if (config.group && entries.length > MIN_GROUP_LENGTH) { + if (options.group && entries.length > MIN_GROUP_LENGTH) { const groups = groupEntries(entries, level, value); iContent = `${initIndentation}${groups.join( entryIndentation )}${closingIndentation}`; } else { 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( entryIndentation )}${closingIndentation}`; } } - return `${iPrefix}${config.delims[0]}${iContent}${config.delims[1]}`; + return `${iPrefix}${options.delims[0]}${iContent}${options.delims[1]}`; } // Ported from Node.js @@ -152,12 +180,13 @@ function createIterableString( function groupEntries( entries: string[], level: number, - value: Iterable + value: Iterable, + iterableLimit = 100 ): string[] { let totalLength = 0; let maxLength = 0; 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. entriesLength--; } @@ -254,7 +283,7 @@ function groupEntries( } tmp.push(str); } - if (MAX_ITERABLE_LENGTH < entries.length) { + if (iterableLimit < entries.length) { tmp.push(entries[entriesLength]); } entries = tmp; @@ -262,11 +291,11 @@ function groupEntries( return entries; } -function stringify( +function inspectValue( value: unknown, ctx: ConsoleContext, level: number, - maxLevel: number + inspectOptions: Required ): string { switch (typeof value) { case "string": @@ -283,7 +312,7 @@ function stringify( case "bigint": // Bigints are yellow return yellow(`${value}n`); 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 if (value === null) { return bold("null"); @@ -294,7 +323,7 @@ function stringify( return cyan("[Circular]"); } - return createObjectString(value, ctx, level, maxLevel); + return inspectObject(value, ctx, level, inspectOptions); default: // Not implemented is red 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 -function stringifyWithQuotes( +function inspectValueWithQuotes( value: unknown, ctx: ConsoleContext, level: number, - maxLevel: number + inspectOptions: Required ): string { switch (typeof value) { case "string": @@ -334,21 +363,21 @@ function stringifyWithQuotes( : value; return green(quoteString(trunc)); // Quoted strings are green default: - return stringify(value, ctx, level, maxLevel); + return inspectValue(value, ctx, level, inspectOptions); } } -function createArrayString( +function inspectArray( value: unknown[], ctx: ConsoleContext, level: number, - maxLevel: number + inspectOptions: Required ): string { - const printConfig: IterablePrintConfig = { + const options: InspectIterableOptions = { typeName: "Array", displayName: "", delims: ["[", "]"], - entryHandler: (entry, ctx, level, maxLevel, next): string => { + entryHandler: (entry, ctx, level, inspectOptions, next): string => { const [index, val] = entry as [number, unknown]; let i = index; if (!value.hasOwnProperty(i)) { @@ -361,117 +390,127 @@ function createArrayString( const ending = emptyItems > 1 ? "s" : ""; return dim(`<${emptyItems} empty item${ending}>`); } 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, value: TypedArray, ctx: ConsoleContext, level: number, - maxLevel: number + inspectOptions: Required ): string { const valueLength = value.length; - const printConfig: IterablePrintConfig = { + const options: InspectIterableOptions = { typeName: typedArrayName, displayName: `${typedArrayName}(${valueLength})`, delims: ["[", "]"], - entryHandler: (entry, ctx, level, maxLevel): string => { + entryHandler: (entry, ctx, level, inspectOptions): string => { 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, ctx: ConsoleContext, level: number, - maxLevel: number + inspectOptions: Required ): string { - const printConfig: IterablePrintConfig = { + const options: InspectIterableOptions = { typeName: "Set", displayName: "Set", delims: ["{", "}"], - entryHandler: (entry, ctx, level, maxLevel): string => { + entryHandler: (entry, ctx, level, inspectOptions): string => { const val = entry[1]; - return stringifyWithQuotes(val, ctx, level + 1, maxLevel); + return inspectValueWithQuotes(val, ctx, level + 1, inspectOptions); }, 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, ctx: ConsoleContext, level: number, - maxLevel: number + inspectOptions: Required ): string { - const printConfig: IterablePrintConfig<[unknown]> = { + const options: InspectIterableOptions<[unknown]> = { typeName: "Map", displayName: "Map", delims: ["{", "}"], - entryHandler: (entry, ctx, level, maxLevel): string => { + entryHandler: (entry, ctx, level, inspectOptions): string => { const [key, val] = entry; - return `${stringifyWithQuotes( + return `${inspectValueWithQuotes( key, ctx, level + 1, - maxLevel - )} => ${stringifyWithQuotes(val, ctx, level + 1, maxLevel)}`; + inspectOptions + )} => ${inspectValueWithQuotes(val, ctx, level + 1, inspectOptions)}`; }, group: false, + sort: inspectOptions.sorted, }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return createIterableString(value as any, ctx, level, maxLevel, printConfig); + return inspectIterable( + // 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 } -function createWeakMapString(): string { +function inspectWeakMap(): string { 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 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 } /* 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 } -function createBooleanWrapperString(value: Boolean): string { +function inspectBooleanObject(value: Boolean): string { 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 } /* eslint-enable @typescript-eslint/ban-types */ -function createPromiseString( +function inspectPromise( value: Promise, ctx: ConsoleContext, level: number, - maxLevel: number + inspectOptions: Required ): string { const [state, result] = Deno.core.getPromiseDetails(value); @@ -482,11 +521,11 @@ function createPromiseString( const prefix = state === PromiseState.Fulfilled ? "" : `${red("")} `; - const str = `${prefix}${stringifyWithQuotes( + const str = `${prefix}${inspectValueWithQuotes( result, ctx, level + 1, - maxLevel + inspectOptions )}`; if (str.length + PROMISE_STRING_BASE_LENGTH > LINE_BREAKING_LENGTH) { @@ -498,13 +537,13 @@ function createPromiseString( // TODO: Proxy -function createRawObjectString( +function inspectRawObject( value: Record, ctx: ConsoleContext, level: number, - maxLevel: number + inspectOptions: Required ): string { - if (level >= maxLevel) { + if (level >= inspectOptions.depth) { return cyan("[Object]"); // wrappers are in cyan } ctx.add(value); @@ -525,20 +564,31 @@ function createRawObjectString( const entries: string[] = []; const stringKeys = Object.keys(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) { entries.push( - `${key}: ${stringifyWithQuotes(value[key], ctx, level + 1, maxLevel)}` + `${key}: ${inspectValueWithQuotes( + value[key], + ctx, + level + 1, + inspectOptions + )}` ); } for (const key of symbolKeys) { entries.push( - `${key.toString()}: ${stringifyWithQuotes( + `${key.toString()}: ${inspectValueWithQuotes( // eslint-disable-next-line @typescript-eslint/no-explicit-any value[key as any], ctx, level + 1, - maxLevel + inspectOptions )}` ); } @@ -550,12 +600,12 @@ function createRawObjectString( if (entries.length === 0) { baseString = "{}"; - } else if (totalLength > LINE_BREAKING_LENGTH) { + } else if (totalLength > LINE_BREAKING_LENGTH || !inspectOptions.compact) { const entryIndent = DEFAULT_INDENT.repeat(level + 1); const closingIndent = DEFAULT_INDENT.repeat(level); - baseString = `{\n${entryIndent}${entries.join( - `,\n${entryIndent}` - )}\n${closingIndent}}`; + baseString = `{\n${entryIndent}${entries.join(`,\n${entryIndent}`)}${ + inspectOptions.trailingComma ? "," : "" + }\n${closingIndent}}`; } else { baseString = `{ ${entries.join(", ")} }`; } @@ -567,9 +617,11 @@ function createRawObjectString( return baseString; } -function createObjectString( +function inspectObject( value: {}, - ...args: [ConsoleContext, number, number] + consoleContext: ConsoleContext, + level: number, + inspectOptions: Required ): string { if (customInspect in value && typeof value[customInspect] === "function") { try { @@ -579,43 +631,46 @@ function createObjectString( if (value instanceof Error) { return String(value.stack); } else if (Array.isArray(value)) { - return createArrayString(value, ...args); + return inspectArray(value, consoleContext, level, inspectOptions); } else if (value instanceof Number) { - return createNumberWrapperString(value); + return inspectNumberObject(value); } else if (value instanceof Boolean) { - return createBooleanWrapperString(value); + return inspectBooleanObject(value); } else if (value instanceof String) { - return createStringWrapperString(value); + return inspectStringObject(value); } else if (value instanceof Promise) { - return createPromiseString(value, ...args); + return inspectPromise(value, consoleContext, level, inspectOptions); } else if (value instanceof RegExp) { - return createRegExpString(value); + return inspectRegExp(value); } else if (value instanceof Date) { - return createDateString(value); + return inspectDate(value); } else if (value instanceof Set) { - return createSetString(value, ...args); + return inspectSet(value, consoleContext, level, inspectOptions); } else if (value instanceof Map) { - return createMapString(value, ...args); + return inspectMap(value, consoleContext, level, inspectOptions); } else if (value instanceof WeakSet) { - return createWeakSetString(); + return inspectWeakSet(); } else if (value instanceof WeakMap) { - return createWeakMapString(); + return inspectWeakMap(); } else if (isTypedArray(value)) { - return createTypedArrayString( + return inspectTypedArray( Object.getPrototypeOf(value).constructor.name, value, - ...args + consoleContext, + level, + inspectOptions ); } else { // Otherwise, default object formatting - return createRawObjectString(value, ...args); + return inspectRawObject(value, consoleContext, level, inspectOptions); } } -export function stringifyArgs( +export function inspectArgs( args: unknown[], - { depth = DEFAULT_MAX_DEPTH, indentLevel = 0 }: InspectOptions = {} + inspectOptions: InspectOptions = {} ): string { + const rInspectOptions = { ...DEFAULT_INSPECT_OPTIONS, ...inspectOptions }; const first = args[0]; let a = 0; let str = ""; @@ -658,7 +713,12 @@ export function stringifyArgs( case CHAR_LOWERCASE_O: case CHAR_UPPERCASE_O: // format as an object - tempStr = stringify(args[++a], new Set(), 0, depth); + tempStr = inspectValue( + args[++a], + new Set(), + 0, + rInspectOptions + ); break; case CHAR_PERCENT: str += first.slice(lastPos, i); @@ -701,14 +761,14 @@ export function stringifyArgs( str += value; } else { // use default maximum depth for null or undefined argument - str += stringify(value, new Set(), 0, depth); + str += inspectValue(value, new Set(), 0, rInspectOptions); } join = " "; a++; } - if (indentLevel > 0) { - const groupIndent = DEFAULT_INDENT.repeat(indentLevel); + if (rInspectOptions.indentLevel > 0) { + const groupIndent = DEFAULT_INDENT.repeat(rInspectOptions.indentLevel); if (str.indexOf("\n") !== -1) { str = str.replace(/\n/g, `\n${groupIndent}`); } @@ -745,7 +805,7 @@ export class Console { log = (...args: unknown[]): void => { this.#printFunc( - stringifyArgs(args, { + inspectArgs(args, { indentLevel: this.indentLevel, }) + "\n", false @@ -756,14 +816,14 @@ export class Console { info = this.log; dir = (obj: unknown, options: InspectOptions = {}): void => { - this.#printFunc(stringifyArgs([obj], options) + "\n", false); + this.#printFunc(inspectArgs([obj], options) + "\n", false); }; dirxml = this.dir; warn = (...args: unknown[]): void => { this.#printFunc( - stringifyArgs(args, { + inspectArgs(args, { indentLevel: this.indentLevel, }) + "\n", true @@ -832,7 +892,10 @@ export class Console { const values: string[] = []; const stringifyValue = (value: unknown): string => - stringifyWithQuotes(value, new Set(), 0, 1); + inspectValueWithQuotes(value, new Set(), 0, { + ...DEFAULT_INSPECT_OPTIONS, + depth: 1, + }); const toTable = (header: string[], body: string[][]): void => this.log(cliTable(header, body)); const createColumn = (value: unknown, shift?: number): string[] => [ @@ -966,7 +1029,7 @@ export class Console { }; trace = (...args: unknown[]): void => { - const message = stringifyArgs(args, { indentLevel: 0 }); + const message = inspectArgs(args, { indentLevel: 0 }); const err = { name: "Trace", message, @@ -980,19 +1043,24 @@ export class Console { } } -export const customInspect = Symbol("Deno.symbols.customInspect"); +export const customInspect = Symbol("Deno.customInspect"); export function inspect( value: unknown, - { depth = DEFAULT_MAX_DEPTH }: InspectOptions = {} + inspectOptions: InspectOptions = {} ): string { if (typeof value === "string") { return value; } else { - return stringify(value, new Set(), 0, depth); + return inspectValue(value, new Set(), 0, { + ...DEFAULT_INSPECT_OPTIONS, + ...inspectOptions, + // TODO(nayeemrmn): Indent level is not supported. + indentLevel: 0, + }); } } // Expose these fields to internalObject for tests. exposeForTest("Console", Console); -exposeForTest("stringifyArgs", stringifyArgs); +exposeForTest("inspectArgs", inspectArgs); diff --git a/cli/tests/unit/console_test.ts b/cli/tests/unit/console_test.ts index d7281fbb28..7a03cd6b62 100644 --- a/cli/tests/unit/console_test.ts +++ b/cli/tests/unit/console_test.ts @@ -11,22 +11,15 @@ import { assert, assertEquals, unitTest } from "./test_util.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 { Console, - stringifyArgs, + inspectArgs, // @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol } = Deno[Deno.internal]; function stringify(...args: unknown[]): string { - return stripColor(stringifyArgs(args).replace(/\n$/, "")); + return stripColor(inspectArgs(args).replace(/\n$/, "")); } // test cases from web-platform-tests @@ -238,7 +231,7 @@ unitTest(function consoleTestStringifyCircular(): void { 'TAG { str: 1, Symbol(sym): 2, Symbol(Symbol.toStringTag): "TAG" }' ); // 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 */ @@ -246,24 +239,21 @@ unitTest(function consoleTestStringifyWithDepth(): void { // eslint-disable-next-line @typescript-eslint/no-explicit-any const nestedObj: any = { a: { b: { c: { d: { e: { f: 42 } } } } } }; assertEquals( - stripColor(stringifyArgs([nestedObj], { depth: 3 })), + stripColor(inspectArgs([nestedObj], { depth: 3 })), "{ a: { b: { c: [Object] } } }" ); assertEquals( - stripColor(stringifyArgs([nestedObj], { depth: 4 })), + stripColor(inspectArgs([nestedObj], { depth: 4 })), "{ a: { b: { c: { d: [Object] } } } }" ); + assertEquals(stripColor(inspectArgs([nestedObj], { depth: 0 })), "[Object]"); assertEquals( - stripColor(stringifyArgs([nestedObj], { depth: 0 })), - "[Object]" - ); - assertEquals( - stripColor(stringifyArgs([nestedObj])), + stripColor(inspectArgs([nestedObj])), "{ a: { b: { c: { d: [Object] } } } }" ); // test inspect is working the same way assertEquals( - stripColor(inspect(nestedObj, { depth: 4 })), + stripColor(Deno.inspect(nestedObj, { depth: 4 })), "{ a: { b: { c: { d: [Object] } } } }" ); }); @@ -653,7 +643,7 @@ unitTest(function consoleTestWithCustomInspectorError(): void { assertEquals(stringify(new B({ a: "a" })), "a"); assertEquals( 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")); }); }); + +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 }` + ); +}); diff --git a/cli/tests/unit/headers_test.ts b/cli/tests/unit/headers_test.ts index 8fbf1d4e40..2156fb56b5 100644 --- a/cli/tests/unit/headers_test.ts +++ b/cli/tests/unit/headers_test.ts @@ -6,7 +6,7 @@ import { assertStringContains, } from "./test_util.ts"; const { - stringifyArgs, + inspectArgs, // @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol } = Deno[Deno.internal]; @@ -402,7 +402,7 @@ unitTest(function toStringShouldBeWebCompatibility(): void { }); function stringify(...args: unknown[]): string { - return stringifyArgs(args).replace(/\n$/, ""); + return inspectArgs(args).replace(/\n$/, ""); } unitTest(function customInspectReturnsCorrectHeadersFormat(): void { diff --git a/cli/tests/unit/internals_test.ts b/cli/tests/unit/internals_test.ts index 3f4bdae79e..e59783e544 100644 --- a/cli/tests/unit/internals_test.ts +++ b/cli/tests/unit/internals_test.ts @@ -3,8 +3,8 @@ import { unitTest, assert } from "./test_util.ts"; unitTest(function internalsExists(): void { const { - stringifyArgs, + inspectArgs, // @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol } = Deno[Deno.internal]; - assert(!!stringifyArgs); + assert(!!inspectArgs); }); diff --git a/std/README.md b/std/README.md index bf1d6940bf..12380412f9 100644 --- a/std/README.md +++ b/std/README.md @@ -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`, `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 @@ -29,7 +32,7 @@ To browse documentation for modules: - Go to https://deno.land/std/. - Navigate to any module of interest. -- Click the "DOCUMENTATION" link. +- Click "View Documentation". ## Contributing diff --git a/std/testing/asserts.ts b/std/testing/asserts.ts index 10e2b9c971..b1164090d4 100644 --- a/std/testing/asserts.ts +++ b/std/testing/asserts.ts @@ -19,8 +19,16 @@ export class AssertionError extends Error { } } -function format(v: unknown): string { - let string = globalThis.Deno ? Deno.inspect(v) : String(v); +export function _format(v: unknown): string { + let string = globalThis.Deno + ? Deno.inspect(v, { + depth: Infinity, + sorted: true, + trailingComma: true, + compact: false, + iterableLimit: Infinity, + }) + : String(v); if (typeof v == "string") { string = `"${string.replace(/(?=["\\])/g, "\\")}"`; } @@ -167,8 +175,8 @@ export function assertEquals( return; } let message = ""; - const actualString = format(actual); - const expectedString = format(expected); + const actualString = _format(actual); + const expectedString = _format(expected); try { const diffResult = diff( actualString.split("\n"), @@ -248,13 +256,13 @@ export function assertStrictEquals( if (msg) { message = msg; } else { - const actualString = format(actual); - const expectedString = format(expected); + const actualString = _format(actual); + const expectedString = _format(expected); if (actualString === expectedString) { const withOffset = actualString .split("\n") - .map((l) => ` ${l}`) + .map((l) => ` ${l}`) .join("\n"); message = `Values have the same structure but are not reference-equal:\n\n${red( withOffset @@ -335,9 +343,9 @@ export function assertArrayContains( return; } if (!msg) { - msg = `actual: "${format(actual)}" expected to contain: "${format( + msg = `actual: "${_format(actual)}" expected to contain: "${_format( expected - )}"\nmissing: ${format(missing)}`; + )}"\nmissing: ${_format(missing)}`; } throw new AssertionError(msg); } diff --git a/std/testing/asserts_test.ts b/std/testing/asserts_test.ts index 011e98590b..c7fa5a737f 100644 --- a/std/testing/asserts_test.ts +++ b/std/testing/asserts_test.ts @@ -1,5 +1,6 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. import { + _format, assert, assertNotEquals, assertStringContains, @@ -15,7 +16,7 @@ import { unimplemented, unreachable, } 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 { assert(equal("world", "world")); @@ -176,7 +177,23 @@ Deno.test("testingArrayContains", function (): void { assertThrows( (): void => assertArrayContains(fixtureObject, [{ deno: "node" }]), 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( (): void => assertEquals([1, "2", 3], ["1", "2", 3]), AssertionError, - [ - "Values are not equal:", - ...createHeader(), - removed(`- [ ${yellow("1")}, ${green('"2"')}, ${yellow("3")} ]`), - added(`+ [ ${green('"1"')}, ${green('"2"')}, ${yellow("3")} ]`), - "", - ].join("\n") + ` + [ +- 1, ++ "1", + "2", + 3, + ]` ); }, }); @@ -359,17 +376,16 @@ Deno.test({ assertThrows( (): void => assertEquals({ a: 1, b: "2", c: 3 }, { a: 1, b: 2, c: [3] }), AssertionError, - [ - "Values are not equal:", - ...createHeader(), - removed( - `- { a: ${yellow("1")}, b: ${green('"2"')}, c: ${yellow("3")} }` - ), - added( - `+ { a: ${yellow("1")}, b: ${yellow("2")}, c: [ ${yellow("3")} ] }` - ), - "", - ].join("\n") + ` + { + a: 1, ++ b: 2, ++ c: [ ++ 3, ++ ], +- b: "2", +- c: 3, + }` ); }, }); @@ -418,13 +434,14 @@ Deno.test({ assertThrows( (): void => assertStrictEquals({ a: 1, b: 2 }, { a: 1, c: [3] }), AssertionError, - [ - "Values are not strictly equal:", - ...createHeader(), - removed(`- { a: ${yellow("1")}, b: ${yellow("2")} }`), - added(`+ { a: ${yellow("1")}, c: [ ${yellow("3")} ] }`), - "", - ].join("\n") + ` + { + a: 1, ++ c: [ ++ 3, ++ ], +- b: 2, + }` ); }, }); @@ -435,10 +452,12 @@ Deno.test({ assertThrows( (): void => assertStrictEquals({ a: 1, b: 2 }, { a: 1, b: 2 }), AssertionError, - [ - "Values have the same structure but are not reference-equal:\n", - red(` { a: ${yellow("1")}, b: ${yellow("2")} }`), - ].join("\n") + `Values have the same structure but are not reference-equal: + + { + a: 1, + b: 2, + }` ); }, }); @@ -535,3 +554,75 @@ Deno.test("Assert Throws Async Non-Error Fail", () => { "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", +]` + ); +});