From 9d82b7659c23669b948acf3e2ba0e4e6f631a4b2 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Sat, 9 Nov 2019 16:14:24 +0300 Subject: [PATCH 01/15] util: fix .format() not always calling toString when it should be This makes sure that `util.format('%s', object)` will always call a user defined `toString` function. It was formerly not the case when the object had the function declared on the super class. At the same time this also makes sure that getters won't be triggered accessing the `constructor` property. PR-URL: https://github.com/nodejs/node/pull/30343 Fixes: https://github.com/nodejs/node/issues/30333 Reviewed-By: James M Snell Reviewed-By: Denys Otrishko Reviewed-By: Jeremiah Senkpiel --- lib/internal/util/inspect.js | 56 +++++++++++++++++------------ test/parallel/test-util-format.js | 60 +++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 22 deletions(-) diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index abf440be779dac..da6f9426ec7b25 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -1645,6 +1645,30 @@ function format(...args) { return formatWithOptions(undefined, ...args); } +function hasBuiltInToString(value) { + // Count objects that have no `toString` function as built-in. + if (typeof value.toString !== 'function') { + return true; + } + + // The object has a own `toString` property. Thus it's not not a built-in one. + if (ObjectPrototypeHasOwnProperty(value, 'toString')) { + return false; + } + + // Find the object that has the `toString` property as own property in the + // prototype chain. + let pointer = value; + do { + pointer = ObjectGetPrototypeOf(pointer); + } while (!ObjectPrototypeHasOwnProperty(pointer, 'toString')); + + // Check closer if the object is a built-in. + const descriptor = ObjectGetOwnPropertyDescriptor(pointer, 'constructor'); + return descriptor !== undefined && + typeof descriptor.value === 'function' && + builtInObjects.has(descriptor.value.name); +} const firstErrorLine = (error) => error.message.split('\n')[0]; let CIRCULAR_ERROR_MESSAGE; @@ -1692,29 +1716,17 @@ function formatWithOptions(inspectOptions, ...args) { tempStr = formatNumber(stylizeNoColor, tempArg); } else if (typeof tempArg === 'bigint') { tempStr = `${tempArg}n`; + } else if (typeof tempArg !== 'object' || + tempArg === null || + !hasBuiltInToString(tempArg)) { + tempStr = String(tempArg); } else { - let constr; - if (typeof tempArg !== 'object' || - tempArg === null || - (typeof tempArg.toString === 'function' && - // A direct own property. - (ObjectPrototypeHasOwnProperty(tempArg, 'toString') || - // A direct own property on the constructor prototype in - // case the constructor is not an built-in object. - ((constr = tempArg.constructor) && - !builtInObjects.has(constr.name) && - constr.prototype && - ObjectPrototypeHasOwnProperty(constr.prototype, - 'toString'))))) { - tempStr = String(tempArg); - } else { - tempStr = inspect(tempArg, { - ...inspectOptions, - compact: 3, - colors: false, - depth: 0 - }); - } + tempStr = inspect(tempArg, { + ...inspectOptions, + compact: 3, + colors: false, + depth: 0 + }); } break; case 106: // 'j' diff --git a/test/parallel/test-util-format.js b/test/parallel/test-util-format.js index cebc666728b246..9b30ca72a07816 100644 --- a/test/parallel/test-util-format.js +++ b/test/parallel/test-util-format.js @@ -160,6 +160,66 @@ assert.strictEqual(util.format('%s', () => 5), '() => 5'); util.format('%s', new Foobar(5)), 'Foobar [ <5 empty items>, aaa: true ]' ); + + // Subclassing: + class B extends Foo {} + + function C() {} + C.prototype.toString = function() { + return 'Custom'; + }; + + function D() { + C.call(this); + } + D.prototype = Object.create(C.prototype); + + assert.strictEqual( + util.format('%s', new B()), + 'Bar' + ); + assert.strictEqual( + util.format('%s', new C()), + 'Custom' + ); + assert.strictEqual( + util.format('%s', new D()), + 'Custom' + ); + + D.prototype.constructor = D; + assert.strictEqual( + util.format('%s', new D()), + 'Custom' + ); + + D.prototype.constructor = null; + assert.strictEqual( + util.format('%s', new D()), + 'Custom' + ); + + D.prototype.constructor = { name: 'Foobar' }; + assert.strictEqual( + util.format('%s', new D()), + 'Custom' + ); + + Object.defineProperty(D.prototype, 'constructor', { + get() { + throw new Error(); + }, + configurable: true + }); + assert.strictEqual( + util.format('%s', new D()), + 'Custom' + ); + + assert.strictEqual( + util.format('%s', Object.create(null)), + '[Object: null prototype] {}' + ); } // JSON format specifier From 2859667b1bcca2238085c6a3ab9ebb4ae9e70224 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Sat, 30 Nov 2019 10:56:11 +0100 Subject: [PATCH 02/15] benchmark: add more util inspect and format benchmarks This adds a couple of benchmarks to check different options and code paths. PR-URL: https://github.com/nodejs/node/pull/30767 Reviewed-By: James M Snell Reviewed-By: Denys Otrishko Reviewed-By: Rich Trott --- benchmark/util/format.js | 2 ++ benchmark/util/inspect-proxy.js | 18 +++++++++++++----- test/benchmark/test-benchmark-util.js | 4 +++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/benchmark/util/format.js b/benchmark/util/format.js index 44e950a6ca6522..976e0f4e655486 100644 --- a/benchmark/util/format.js +++ b/benchmark/util/format.js @@ -13,6 +13,8 @@ const inputs = { 'no-replace-2': ['foobar', 'yeah', 'mensch', 5], 'only-objects': [{ msg: 'This is an error' }, { msg: 'This is an error' }], 'many-%': ['replace%%%%s%%%%many%s%s%s', 'percent'], + 'object-to-string': ['foo %s bar', { toString() { return 'bla'; } }], + 'object-%s': ['foo %s bar', { a: true, b: false }], }; const bench = common.createBenchmark(main, { diff --git a/benchmark/util/inspect-proxy.js b/benchmark/util/inspect-proxy.js index dde86941ff42a3..fd89d568aba35f 100644 --- a/benchmark/util/inspect-proxy.js +++ b/benchmark/util/inspect-proxy.js @@ -3,13 +3,21 @@ const util = require('util'); const common = require('../common.js'); -const bench = common.createBenchmark(main, { n: [2e4] }); +const bench = common.createBenchmark(main, { + n: [2e4], + showProxy: [0, 1], + isProxy: [0, 1] +}); -function main({ n }) { - const proxyA = new Proxy({}, { get: () => {} }); - const proxyB = new Proxy(() => {}, {}); +function main({ n, showProxy, isProxy }) { + let proxyA = {}; + let proxyB = () => {}; + if (isProxy) { + proxyA = new Proxy(proxyA, { get: () => {} }); + proxyB = new Proxy(proxyB, {}); + } bench.start(); for (let i = 0; i < n; i += 1) - util.inspect({ a: proxyA, b: proxyB }, { showProxy: true }); + util.inspect({ a: proxyA, b: proxyB }, { showProxy }); bench.end(n); } diff --git a/test/benchmark/test-benchmark-util.js b/test/benchmark/test-benchmark-util.js index 97b02bbdeed5cd..b66d4fdb9b4cf6 100644 --- a/test/benchmark/test-benchmark-util.js +++ b/test/benchmark/test-benchmark-util.js @@ -14,5 +14,7 @@ runBenchmark('util', 'size=1', 'type=', 'len=1', - 'version=native'], + 'version=native', + 'isProxy=1', + 'showProxy=1'], { NODEJS_BENCHMARK_ZERO_ALLOWED: 1 }); From e110449d4e0e725d2c2eaad78a621ad1a1783446 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Sat, 30 Nov 2019 11:04:34 +0100 Subject: [PATCH 03/15] util: improve performance inspecting proxies This makes sure we do not retrieve the handler in case it's not required. This improves the performance a tiny bit for these cases. PR-URL: https://github.com/nodejs/node/pull/30767 Reviewed-By: James M Snell Reviewed-By: Denys Otrishko Reviewed-By: Rich Trott --- benchmark/util/inspect-proxy.js | 2 +- lib/internal/util/inspect.js | 4 ++-- src/node_util.cc | 20 ++++++++++++++------ test/parallel/test-util-inspect-proxy.js | 7 +++++-- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/benchmark/util/inspect-proxy.js b/benchmark/util/inspect-proxy.js index fd89d568aba35f..02379cdc770f3b 100644 --- a/benchmark/util/inspect-proxy.js +++ b/benchmark/util/inspect-proxy.js @@ -4,7 +4,7 @@ const util = require('util'); const common = require('../common.js'); const bench = common.createBenchmark(main, { - n: [2e4], + n: [1e5], showProxy: [0, 1], isProxy: [0, 1] }); diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index da6f9426ec7b25..09f755ac00649f 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -640,12 +640,12 @@ function formatValue(ctx, value, recurseTimes, typedArray) { const context = value; // Always check for proxies to prevent side effects and to prevent triggering // any proxy handlers. - const proxy = getProxyDetails(value); + const proxy = getProxyDetails(value, !!ctx.showProxy); if (proxy !== undefined) { if (ctx.showProxy) { return formatProxy(ctx, proxy, recurseTimes); } - value = proxy[0]; + value = proxy; } // Provide a hook for user-specified inspect functions. diff --git a/src/node_util.cc b/src/node_util.cc index 4d0b109371fb60..b9553eaaa6763d 100644 --- a/src/node_util.cc +++ b/src/node_util.cc @@ -91,15 +91,23 @@ static void GetProxyDetails(const FunctionCallbackInfo& args) { if (!args[0]->IsProxy()) return; + CHECK(args[1]->IsBoolean()); + Local proxy = args[0].As(); - Local ret[] = { - proxy->GetTarget(), - proxy->GetHandler() - }; + if (args[1]->IsTrue()) { + Local ret[] = { + proxy->GetTarget(), + proxy->GetHandler() + }; - args.GetReturnValue().Set( - Array::New(args.GetIsolate(), ret, arraysize(ret))); + args.GetReturnValue().Set( + Array::New(args.GetIsolate(), ret, arraysize(ret))); + } else { + Local ret = proxy->GetTarget(); + + args.GetReturnValue().Set(ret); + } } static void PreviewEntries(const FunctionCallbackInfo& args) { diff --git a/test/parallel/test-util-inspect-proxy.js b/test/parallel/test-util-inspect-proxy.js index c20af7450a426e..5e85b92f713175 100644 --- a/test/parallel/test-util-inspect-proxy.js +++ b/test/parallel/test-util-inspect-proxy.js @@ -43,10 +43,13 @@ util.inspect(proxyObj, opts); // getProxyDetails is an internal method, not intended for public use. // This is here to test that the internals are working correctly. -const details = processUtil.getProxyDetails(proxyObj); +let details = processUtil.getProxyDetails(proxyObj, true); assert.strictEqual(target, details[0]); assert.strictEqual(handler, details[1]); +details = processUtil.getProxyDetails(proxyObj, false); +assert.strictEqual(target, details); + assert.strictEqual( util.inspect(proxyObj, opts), 'Proxy [\n' + @@ -105,7 +108,7 @@ const expected6 = 'Proxy [\n' + ' ]\n' + ']'; assert.strictEqual( - util.inspect(proxy1, { showProxy: true, depth: null }), + util.inspect(proxy1, { showProxy: 1, depth: null }), expected1); assert.strictEqual(util.inspect(proxy2, opts), expected2); assert.strictEqual(util.inspect(proxy3, opts), expected3); From 62ed6a20373b834956f6703015167cb75f809a3c Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Sun, 1 Dec 2019 01:08:37 +0100 Subject: [PATCH 04/15] util: never trigger any proxy traps using `format()` PR-URL: https://github.com/nodejs/node/pull/30767 Reviewed-By: James M Snell Reviewed-By: Denys Otrishko Reviewed-By: Rich Trott --- lib/internal/util/inspect.js | 7 +++++++ test/parallel/test-util-inspect-proxy.js | 3 +++ 2 files changed, 10 insertions(+) diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index 09f755ac00649f..38665d1bc2a515 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -1646,6 +1646,13 @@ function format(...args) { } function hasBuiltInToString(value) { + // Prevent triggering proxy traps. + const getFullProxy = false; + const proxyTarget = getProxyDetails(value, getFullProxy); + if (proxyTarget !== undefined) { + value = proxyTarget; + } + // Count objects that have no `toString` function as built-in. if (typeof value.toString !== 'function') { return true; diff --git a/test/parallel/test-util-inspect-proxy.js b/test/parallel/test-util-inspect-proxy.js index 5e85b92f713175..5789a6ffbcf6c5 100644 --- a/test/parallel/test-util-inspect-proxy.js +++ b/test/parallel/test-util-inspect-proxy.js @@ -41,6 +41,9 @@ proxyObj = new Proxy(target, handler); // Inspecting the proxy should not actually walk it's properties util.inspect(proxyObj, opts); +// Make sure inspecting object does not trigger any proxy traps. +util.format('%s', proxyObj); + // getProxyDetails is an internal method, not intended for public use. // This is here to test that the internals are working correctly. let details = processUtil.getProxyDetails(proxyObj, true); From 7d19abacec2677a4939efa58f8194a4f823cc7e2 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Sat, 30 Nov 2019 13:33:18 +0100 Subject: [PATCH 05/15] util: fix built-in detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This makes sure that the regular expression matches all built-in objects properly. So far a couple where missed. PR-URL: https://github.com/nodejs/node/pull/30768 Fixes: https://github.com/nodejs/node/issues/30183 Reviewed-By: James M Snell Reviewed-By: Michaël Zasso --- lib/internal/util/inspect.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index 38665d1bc2a515..dd8a863a81083c 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -118,7 +118,7 @@ const { NativeModule } = require('internal/bootstrap/loaders'); let hexSlice; const builtInObjects = new Set( - ObjectGetOwnPropertyNames(global).filter((e) => /^([A-Z][a-z]+)+$/.test(e)) + ObjectGetOwnPropertyNames(global).filter((e) => /^[A-Z][a-zA-Z0-9]+$/.test(e)) ); // These options must stay in sync with `getUserOptions`. So if any option will From a9d41b71fbc8db21c1ad90cf377503f30bc6ec0b Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Sat, 30 Nov 2019 11:07:12 +0100 Subject: [PATCH 06/15] util: inspect (user defined) prototype properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is only active if the `showHidden` option is truthy. The implementation is a trade-off between accuracy and performance. This will miss properties such as properties added to built-in data types. The goal is mainly to visualize prototype getters and setters such as: class Foo { ownProperty = true get bar() { return 'Hello world!' } } const a = new Foo() The `bar` property is a non-enumerable property on the prototype while `ownProperty` will be set directly on the created instance. The output is similar to the one of Chromium when inspecting objects closer. The output from Firefox is difficult to compare, since it's always a structured interactive output and was therefore not taken into account. PR-URL: https://github.com/nodejs/node/pull/30768 Fixes: https://github.com/nodejs/node/issues/30183 Reviewed-By: James M Snell Reviewed-By: Michaël Zasso --- doc/api/util.md | 7 +- lib/internal/util/inspect.js | 116 +++++++++++++++--- test/parallel/test-util-inspect.js | 80 +++++++++++- ...test-whatwg-encoding-custom-textdecoder.js | 17 ++- 4 files changed, 198 insertions(+), 22 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index b26d2a9558c987..e2024764676653 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -398,6 +398,10 @@ stream.write('With ES6'); @@ -534,8 +535,8 @@ assert.doesNotThrow( ); ``` -If an `AssertionError` is thrown and a value is provided for the `message` -parameter, the value of `message` will be appended to the `AssertionError` +If an [`AssertionError`][] is thrown and a value is provided for the `message` +parameter, the value of `message` will be appended to the [`AssertionError`][] message: @@ -584,7 +585,7 @@ assert.equal({ a: { b: 1 } }, { a: { b: 1 } }); // AssertionError: { a: { b: 1 } } == { a: { b: 1 } } ``` -If the values are not equal, an `AssertionError` is thrown with a `message` +If the values are not equal, an [`AssertionError`][] is thrown with a `message` property set equal to the value of the `message` parameter. If the `message` parameter is undefined, a default error message is assigned. If the `message` parameter is an instance of an [`Error`][] then it will be thrown instead of the @@ -597,9 +598,9 @@ added: v0.1.21 * `message` {string|Error} **Default:** `'Failed'` -Throws an `AssertionError` with the provided error message or a default error -message. If the `message` parameter is an instance of an [`Error`][] then it -will be thrown instead of the `AssertionError`. +Throws an [`AssertionError`][] with the provided error message or a default +error message. If the `message` parameter is an instance of an [`Error`][] then +it will be thrown instead of the [`AssertionError`][]. ```js const assert = require('assert').strict; @@ -687,7 +688,7 @@ changes: - version: v10.0.0 pr-url: https://github.com/nodejs/node/pull/18247 description: Instead of throwing the original error it is now wrapped into - an `AssertionError` that contains the full stack trace. + an [`AssertionError`][] that contains the full stack trace. - version: v10.0.0 pr-url: https://github.com/nodejs/node/pull/18247 description: Value may now only be `undefined` or `null`. Before all falsy @@ -795,11 +796,11 @@ assert.notDeepEqual(obj1, obj4); // OK ``` -If the values are deeply equal, an `AssertionError` is thrown with a `message` -property set equal to the value of the `message` parameter. If the `message` -parameter is undefined, a default error message is assigned. If the `message` -parameter is an instance of an [`Error`][] then it will be thrown instead of the -`AssertionError`. +If the values are deeply equal, an [`AssertionError`][] is thrown with a +`message` property set equal to the value of the `message` parameter. If the +`message` parameter is undefined, a default error message is assigned. If the +`message` parameter is an instance of an [`Error`][] then it will be thrown +instead of the `AssertionError`. ## `assert.notDeepStrictEqual(actual, expected[, message])` + +* `string` {string} +* `regexp` {RegExp} +* `message` {string|Error} + +> Stability: 1 - Experimental + +Expects the `string` input not to match the regular expression. + +This feature is currently experimental and the name might change or it might be +completely removed again. + +```js +const assert = require('assert').strict; + +assert.doesNotMatch('I will fail', /fail/); +// AssertionError [ERR_ASSERTION]: The input was expected to not match the ... + +assert.doesNotMatch(123, /pass/); +// AssertionError [ERR_ASSERTION]: The "string" argument must be of type string. + +assert.doesNotMatch('I will pass', /different/); +// OK +``` + +If the values do match, or if the `string` argument is of another type than +`string`, an [`AssertionError`][] is thrown with a `message` property set equal +to the value of the `message` parameter. If the `message` parameter is +undefined, a default error message is assigned. If the `message` parameter is an +instance of an [`Error`][] then it will be thrown instead of the +[`AssertionError`][]. + ## `assert.doesNotReject(asyncFn[, error][, message])` + +* `string` {string} +* `regexp` {RegExp} +* `message` {string|Error} + +> Stability: 1 - Experimental + +Expects the `string` input to match the regular expression. + +This feature is currently experimental and the name might change or it might be +completely removed again. + +```js +const assert = require('assert').strict; + +assert.match('I will fail', /pass/); +// AssertionError [ERR_ASSERTION]: The input did not match the regular ... + +assert.match(123, /pass/); +// AssertionError [ERR_ASSERTION]: The "string" argument must be of type string. + +assert.match('I will pass', /pass/); +// OK +``` + +If the values do not match, or if the `string` argument is of another type than +`string`, an [`AssertionError`][] is thrown with a `message` property set equal +to the value of the `message` parameter. If the `message` parameter is +undefined, a default error message is assigned. If the `message` parameter is an +instance of an [`Error`][] then it will be thrown instead of the +[`AssertionError`][]. + ## `assert.notDeepEqual(actual, expected[, message])`