Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

util: add sorted option #22788

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,9 @@ stream.write('With ES6');
<!-- YAML
added: v0.3.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/REPLACEME
description: The `sorted` option is supported now.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/22756
description: The inspection output is now limited to about 128 MB. Data
Expand Down Expand Up @@ -426,6 +429,10 @@ changes:
objects the same as arrays. Note that no text will be reduced below 16
characters, no matter the `breakLength` size. For more information, see the
example below. **Default:** `true`.
* `sorted` {boolean|Function} If set to `true` or a function, all properties
of an object and Set and Map entries will be sorted in the returned string.
If set to `true` the [default sort][] is going to be used. If set to a
function, it is used as a [compare function][].
* Returns: {string} The representation of passed object

The `util.inspect()` method returns a string representation of `object` that is
Expand Down Expand Up @@ -535,6 +542,34 @@ console.log(inspect(weakSet, { showHidden: true }));
// WeakSet { { a: 1 }, { b: 2 } }
```

The `sorted` option makes sure the output is identical, no matter of the
properties insertion order:

```js
const { inspect } = require('util');
const assert = require('assert');

const o1 = {
b: [2, 3, 1],
a: '`a` comes before `b`',
c: new Set([2, 3, 1])
};
console.log(inspect(o1, { sorted: true }));
// { a: '`a` comes before `b`', b: [ 2, 3, 1 ], c: Set { 1, 2, 3 } }
console.log(inspect(o1, { sorted: (a, b) => a < b }));
// { c: Set { 3, 2, 1 }, b: [ 2, 3, 1 ], a: '`a` comes before `b`' }

const o2 = {
c: new Set([2, 1, 3]),
a: '`a` comes before `b`',
b: [2, 3, 1]
};
assert.strict.equal(
inspect(o1, { sorted: true }),
inspect(o2, { sorted: true })
);
```

Please note that `util.inspect()` is a synchronous method that is mainly
intended as a debugging tool. Its maximum output length is limited to
approximately 128 MB and input values that result in output bigger than that
Expand Down Expand Up @@ -2150,7 +2185,9 @@ Deprecated predecessor of `console.log`.
[WHATWG Encoding Standard]: https://encoding.spec.whatwg.org/
[Common System Errors]: errors.html#errors_common_system_errors
[async function]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
[compare function]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Parameters
[constructor]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor
[default sort]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
[global symbol registry]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/for
[list of deprecated APIS]: deprecations.html#deprecations_list_of_deprecated_apis
[semantically incompatible]: https://github.com/nodejs/node/issues/4179
Expand Down
3 changes: 2 additions & 1 deletion lib/internal/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ function inspectValue(val) {
// comparison.
breakLength: Infinity,
// Assert does not detect proxies currently.
showProxy: false
showProxy: false,
sorted: true
}
);
}
Expand Down
18 changes: 15 additions & 3 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ const inspectDefaultOptions = Object.seal({
showProxy: false,
maxArrayLength: 100,
breakLength: 60,
compact: true
compact: true,
sorted: false
});

const kObjectType = 0;
Expand Down Expand Up @@ -394,6 +395,8 @@ function debuglog(set) {
function inspect(value, opts) {
// Default options
const ctx = {
budget: {},
indentationLvl: 0,
seen: [],
stylize: stylizeNoColor,
showHidden: inspectDefaultOptions.showHidden,
Expand All @@ -405,9 +408,8 @@ function inspect(value, opts) {
// `maxEntries`.
maxArrayLength: inspectDefaultOptions.maxArrayLength,
breakLength: inspectDefaultOptions.breakLength,
indentationLvl: 0,
compact: inspectDefaultOptions.compact,
budget: {}
sorted: inspectDefaultOptions.sorted
};
if (arguments.length > 1) {
// Legacy...
Expand Down Expand Up @@ -894,6 +896,16 @@ function formatRaw(ctx, value, recurseTimes) {
}
ctx.seen.pop();

if (ctx.sorted) {
const comparator = ctx.sorted === true ? undefined : ctx.sorted;
if (extrasType === kObjectType) {
output = output.sort(comparator);
} else if (keys.length > 1) {
const sorted = output.slice(output.length - keys.length).sort(comparator);
output.splice(output.length - keys.length, keys.length, ...sorted);
}
}

const res = reduceToSingleString(ctx, output, base, braces);
const budget = ctx.budget[ctx.indentationLvl] || 0;
const newLength = budget + res.length;
Expand Down
3 changes: 2 additions & 1 deletion test/parallel/test-assert-deep.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ function re(literals, ...values) {
depth: 1000,
customInspect: false,
maxArrayLength: Infinity,
breakLength: Infinity
breakLength: Infinity,
sorted: true
});
// Need to escape special characters.
result += str;
Expand Down
39 changes: 26 additions & 13 deletions test/parallel/test-assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ testAssertionMessage(-Infinity, '-Infinity');
testAssertionMessage([1, 2, 3], '[\n+ 1,\n+ 2,\n+ 3\n+ ]');
testAssertionMessage(function f() {}, '[Function: f]');
testAssertionMessage(function() {}, '[Function]');
testAssertionMessage(circular, '{\n+ y: 1,\n+ x: [Circular]\n+ }');
testAssertionMessage(circular, '{\n+ x: [Circular],\n+ y: 1\n+ }');
testAssertionMessage({ a: undefined, b: null },
'{\n+ a: undefined,\n+ b: null\n+ }');
testAssertionMessage({ a: NaN, b: Infinity, c: -Infinity },
Expand Down Expand Up @@ -602,8 +602,8 @@ assert.throws(
'\n' +
'+ {}\n' +
'- {\n' +
"- loop: 'forever',\n" +
'- [Symbol(nodejs.util.inspect.custom)]: [Function]\n' +
'- [Symbol(nodejs.util.inspect.custom)]: [Function],\n' +
"- loop: 'forever'\n" +
'- }'
});

Expand Down Expand Up @@ -885,9 +885,12 @@ common.expectsError(
code: 'ERR_ASSERTION',
name: 'AssertionError [ERR_ASSERTION]',
message: `${start}\n${actExp}\n\n` +
" Comparison {\n name: 'TypeError',\n" +
" message: 'Wrong value',\n+ code: 404\n" +
'- code: 404,\n- foo: undefined\n }'
' Comparison {\n' +
' code: 404,\n' +
'- foo: undefined,\n' +
" message: 'Wrong value',\n" +
" name: 'TypeError'\n" +
' }'
}
);

Expand All @@ -899,9 +902,13 @@ common.expectsError(
code: 'ERR_ASSERTION',
name: 'AssertionError [ERR_ASSERTION]',
message: `${start}\n${actExp}\n\n` +
" Comparison {\n name: 'TypeError',\n" +
" message: 'Wrong value',\n+ code: 404\n" +
"- code: '404',\n- foo: undefined\n }"
' Comparison {\n' +
'+ code: 404,\n' +
"- code: '404',\n" +
'- foo: undefined,\n' +
" message: 'Wrong value',\n" +
" name: 'TypeError'\n" +
' }'
}
);

Expand Down Expand Up @@ -931,8 +938,11 @@ common.expectsError(
name: 'AssertionError [ERR_ASSERTION]',
code: 'ERR_ASSERTION',
message: `${start}\n${actExp}\n\n` +
" Comparison {\n+ name: 'TypeError',\n- name: 'Error'," +
"\n message: 'e'\n }"
' Comparison {\n' +
" message: 'e',\n" +
"+ name: 'TypeError'\n" +
"- name: 'Error'\n" +
' }'
}
);
assert.throws(
Expand All @@ -942,8 +952,11 @@ common.expectsError(
code: 'ERR_ASSERTION',
generatedMessage: true,
message: `${start}\n${actExp}\n\n` +
" Comparison {\n name: 'Error',\n+ message: 'foo'" +
"\n- message: ''\n }"
' Comparison {\n' +
"+ message: 'foo',\n" +
"- message: '',\n" +
" name: 'Error'\n" +
' }'
}
);

Expand Down
2 changes: 1 addition & 1 deletion test/parallel/test-internal-errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ common.expectsError(() => {
}, {
code: 'ERR_ASSERTION',
type: assert.AssertionError,
message: /\+ message: 'Error for testing purposes: a'\n- message: \/\^Error/
message: /\+ message: 'Error for testing purposes: a',\n- message: \/\^Error/
});

// Test ERR_INVALID_FD_TYPE
Expand Down
30 changes: 30 additions & 0 deletions test/parallel/test-util-inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -1675,3 +1675,33 @@ assert.strictEqual(inspect(new BigUint64Array([0n])), 'BigUint64Array [ 0n ]');
);
rejection.catch(() => {});
}

assert.strictEqual(
inspect([1, 3, 2], { sorted: true }),
inspect([1, 3, 2])
);
assert.strictEqual(
inspect({ c: 3, a: 1, b: 2 }, { sorted: true }),
'{ a: 1, b: 2, c: 3 }'
);
assert.strictEqual(
inspect(
{ a200: 4, a100: 1, a102: 3, a101: 2 },
{ sorted(a, b) { return a < b; } }
),
'{ a200: 4, a102: 3, a101: 2, a100: 1 }'
);

// Non-indices array properties are sorted as well.
{
const arr = [3, 2, 1];
arr.b = 2;
arr.c = 3;
arr.a = 1;
arr[Symbol('b')] = true;
arr[Symbol('a')] = false;
assert.strictEqual(
inspect(arr, { sorted: true }),
'[ 3, 2, 1, [Symbol(a)]: false, [Symbol(b)]: true, a: 1, b: 2, c: 3 ]'
);
}