From 61108935f16bd2aa60d51525e3578719425eef03 Mon Sep 17 00:00:00 2001 From: David DeSimone Date: Wed, 10 Feb 2021 03:52:54 -0800 Subject: [PATCH] fix(console): log function object properties / do not log non-enumerable props by default (#9363) --- cli/dts/lib.deno.ns.d.ts | 2 + cli/tests/041_dyn_import_eval.out | 2 +- cli/tests/042_dyn_import_evalcontext.ts.out | 2 +- cli/tests/070_location.ts.out | 2 +- cli/tests/071_location_unset.ts.out | 2 +- cli/tests/fix_js_imports.ts.out | 2 +- cli/tests/top_level_await_loop.out | 4 +- cli/tests/unit/console_test.ts | 47 +++++++++++++++++- runtime/js/02_console.js | 54 +++++++++++++++++++-- 9 files changed, 104 insertions(+), 13 deletions(-) diff --git a/cli/dts/lib.deno.ns.d.ts b/cli/dts/lib.deno.ns.d.ts index b607554e15..04bf8ad80f 100644 --- a/cli/dts/lib.deno.ns.d.ts +++ b/cli/dts/lib.deno.ns.d.ts @@ -2034,6 +2034,8 @@ declare namespace Deno { trailingComma?: boolean; /*** Evaluate the result of calling getters. Defaults to false. */ getters?: boolean; + /** Show an object's non-enumerable properties. Defaults to false. */ + showHidden?: boolean; } /** Converts the input into a string that has the same format as printed by diff --git a/cli/tests/041_dyn_import_eval.out b/cli/tests/041_dyn_import_eval.out index a4c44eff80..12a45b8da9 100644 --- a/cli/tests/041_dyn_import_eval.out +++ b/cli/tests/041_dyn_import_eval.out @@ -1 +1 @@ -Module { isMod4: true, [Symbol(Symbol.toStringTag)]: "Module" } +Module { isMod4: true } diff --git a/cli/tests/042_dyn_import_evalcontext.ts.out b/cli/tests/042_dyn_import_evalcontext.ts.out index a4c44eff80..12a45b8da9 100644 --- a/cli/tests/042_dyn_import_evalcontext.ts.out +++ b/cli/tests/042_dyn_import_evalcontext.ts.out @@ -1 +1 @@ -Module { isMod4: true, [Symbol(Symbol.toStringTag)]: "Module" } +Module { isMod4: true } diff --git a/cli/tests/070_location.ts.out b/cli/tests/070_location.ts.out index 3c08d53fc8..e05561e58b 100644 --- a/cli/tests/070_location.ts.out +++ b/cli/tests/070_location.ts.out @@ -1,5 +1,5 @@ [WILDCARD][Function: Location] -Location { [Symbol(Symbol.toStringTag)]: "Location" } +Location {} Location { hash: "#bat", host: "foo", diff --git a/cli/tests/071_location_unset.ts.out b/cli/tests/071_location_unset.ts.out index 2c030a7733..43308f3bd9 100644 --- a/cli/tests/071_location_unset.ts.out +++ b/cli/tests/071_location_unset.ts.out @@ -1,4 +1,4 @@ [WILDCARD][Function: Location] -Location { [Symbol(Symbol.toStringTag)]: "Location" } +Location {} error: Uncaught ReferenceError: Access to "location", run again with --location . [WILDCARD] diff --git a/cli/tests/fix_js_imports.ts.out b/cli/tests/fix_js_imports.ts.out index 4fb3b8f120..5e45122de8 100644 --- a/cli/tests/fix_js_imports.ts.out +++ b/cli/tests/fix_js_imports.ts.out @@ -1 +1 @@ -Module { [Symbol(Symbol.toStringTag)]: "Module" } +Module {} diff --git a/cli/tests/top_level_await_loop.out b/cli/tests/top_level_await_loop.out index d704e3afd3..70e621e45b 100644 --- a/cli/tests/top_level_await_loop.out +++ b/cli/tests/top_level_await_loop.out @@ -1,5 +1,5 @@ loading [WILDCARD]a.js -loaded Module { default: [Function: Foo], [Symbol(Symbol.toStringTag)]: "Module" } +loaded Module { default: [Function: Foo] } loading [WILDCARD]b.js -loaded Module { default: [Function: Bar], [Symbol(Symbol.toStringTag)]: "Module" } +loaded Module { default: [Function: Bar] } all loaded diff --git a/cli/tests/unit/console_test.ts b/cli/tests/unit/console_test.ts index fc23b1d706..286b693ffb 100644 --- a/cli/tests/unit/console_test.ts +++ b/cli/tests/unit/console_test.ts @@ -310,7 +310,7 @@ unitTest(function consoleTestStringifyCircular(): void { assertEquals(stringify(nestedObj), nestedObjExpected); assertEquals( stringify(JSON), - 'JSON { [Symbol(Symbol.toStringTag)]: "JSON" }', + "JSON {}", ); assertEquals( stringify(console), @@ -335,7 +335,6 @@ unitTest(function consoleTestStringifyCircular(): void { clear: [Function: clear], trace: [Function: trace], indentLevel: 0, - [Symbol(Symbol.toStringTag)]: "console", [Symbol(isConsoleInstance)]: true }`, ); @@ -362,6 +361,50 @@ unitTest(function consoleTestStringifyFunctionWithPrototypeRemoved(): void { assertEquals(stringify(agf), "[Function: agf]"); }); +unitTest(function consoleTestStringifyFunctionWithProperties(): void { + const f = () => "test"; + f.x = () => "foo"; + f.y = 3; + f.z = () => "baz"; + f.b = function bar() {}; + f.a = new Map(); + assertEquals( + stringify({ f }), + `{ + f: [Function: f] { x: [Function], y: 3, z: [Function], b: [Function: bar], a: Map {} } +}`, + ); + + const t = () => {}; + t.x = f; + f.s = f; + f.t = t; + assertEquals( + stringify({ f }), + `{ + f: [Function: f] { + x: [Function], + y: 3, + z: [Function], + b: [Function: bar], + a: Map {}, + s: [Circular], + t: [Function: t] { x: [Circular] } + } +}`, + ); + + assertEquals( + stringify(Array), + `[Function: Array]`, + ); + + assertEquals( + stripColor(Deno.inspect(Array, { showHidden: true })), + `[Function: Array] { [Symbol(Symbol.species)]: [Getter] }`, + ); +}); + unitTest(function consoleTestStringifyWithDepth(): void { // deno-lint-ignore no-explicit-any const nestedObj: any = { a: { b: { c: { d: { e: { f: 42 } } } } } }; diff --git a/runtime/js/02_console.js b/runtime/js/02_console.js index bd6565e0eb..5d174ece40 100644 --- a/runtime/js/02_console.js +++ b/runtime/js/02_console.js @@ -17,6 +17,17 @@ return Object.prototype.hasOwnProperty.call(obj, v); } + function propertyIsEnumerable(obj, prop) { + if ( + obj == null || + typeof obj.propertyIsEnumerable !== "function" + ) { + return false; + } + + return obj.propertyIsEnumerable(prop); + } + // Copyright Joyent, Inc. and other Node contributors. MIT license. // Forked from Node's lib/internal/cli_table.js @@ -159,6 +170,7 @@ showProxy: false, colors: false, getters: false, + showHidden: false, }; const DEFAULT_INDENT = " "; // Default indent string @@ -189,7 +201,8 @@ return inspectOptions.colors ? fn : (s) => s; } - function inspectFunction(value, _ctx) { + function inspectFunction(value, ctx, level, inspectOptions) { + const cyan = maybeColor(colors.cyan, inspectOptions); if (customInspect in value && typeof value[customInspect] === "function") { return String(value[customInspect]()); } @@ -200,11 +213,32 @@ // use generic 'Function' instead. cstrName = "Function"; } + + // Our function may have properties, so we want to format those + // as if our function was an object + // If we didn't find any properties, we will just append an + // empty suffix. + let suffix = ``; + if ( + Object.keys(value).length > 0 || + Object.getOwnPropertySymbols(value).length > 0 + ) { + const propString = inspectRawObject(value, ctx, level, inspectOptions); + // Filter out the empty string for the case we only have + // non-enumerable symbols. + if ( + propString.length > 0 && + propString !== "{}" + ) { + suffix = ` ${propString}`; + } + } + if (value.name && value.name !== "anonymous") { // from MDN spec - return `[${cstrName}: ${value.name}]`; + return cyan(`[${cstrName}: ${value.name}]`) + suffix; } - return `[${cstrName}]`; + return cyan(`[${cstrName}]`) + suffix; } function inspectIterable( @@ -429,7 +463,12 @@ case "bigint": // Bigints are yellow return yellow(`${value}n`); case "function": // Function string is cyan - return cyan(inspectFunction(value, ctx)); + if (ctx.has(value)) { + // Circular string is cyan + return cyan("[Circular]"); + } + + return inspectFunction(value, ctx, level, inspectOptions); case "object": // null is bold if (value === null) { return bold("null"); @@ -797,6 +836,13 @@ } for (const key of symbolKeys) { + if ( + !inspectOptions.showHidden && + !propertyIsEnumerable(value, key) + ) { + continue; + } + if (inspectOptions.getters) { let propertyValue; let error;