diff --git a/lib/util.js b/lib/util.js index afd3cae724a6a7..8fd1e204cd470d 100644 --- a/lib/util.js +++ b/lib/util.js @@ -90,6 +90,9 @@ let internalDeepEqual; /* eslint-disable no-control-regex */ const strEscapeSequencesRegExp = /[\x00-\x1f\x27\x5c]/; const strEscapeSequencesReplacer = /[\x00-\x1f\x27\x5c]/g; +const strEscapeSequencesRegExpSingle = /[\x00-\x1f\x5c]/; +const strEscapeSequencesReplacerSingle = /[\x00-\x1f\x5c]/g; + /* eslint-enable no-control-regex */ const keyStrRegExp = /^[a-zA-Z_][a-zA-Z_0-9]*$/; @@ -116,21 +119,56 @@ const meta = [ '', '', '', '', '', '', '', '\\\\' ]; +function addQuotes(str, quotes) { + if (quotes === -1) { + return `"${str}"`; + } + if (quotes === -2) { + return `\`${str}\``; + } + return `'${str}'`; +} + const escapeFn = (str) => meta[str.charCodeAt(0)]; // Escape control characters, single quotes and the backslash. // This is similar to JSON stringify escaping. function strEscape(str) { + let escapeTest = strEscapeSequencesRegExp; + let escapeReplace = strEscapeSequencesReplacer; + let singleQuote = 39; + + // Check for double quotes. If not present, do not escape single quotes and + // instead wrap the text in double quotes. If double quotes exist, check for + // backticks. If they do not exist, use those as fallback instead of the + // double quotes. + if (str.indexOf("'") !== -1) { + // This invalidates the charCode and therefore can not be matched for + // anymore. + if (str.indexOf('"') === -1) { + singleQuote = -1; + } else if (str.indexOf('`') === -1 && str.indexOf('${') === -1) { + singleQuote = -2; + } + if (singleQuote !== 39) { + escapeTest = strEscapeSequencesRegExpSingle; + escapeReplace = strEscapeSequencesReplacerSingle; + } + } + // Some magic numbers that worked out fine while benchmarking with v8 6.0 - if (str.length < 5000 && !strEscapeSequencesRegExp.test(str)) - return `'${str}'`; - if (str.length > 100) - return `'${str.replace(strEscapeSequencesReplacer, escapeFn)}'`; + if (str.length < 5000 && !escapeTest.test(str)) + return addQuotes(str, singleQuote); + if (str.length > 100) { + str = str.replace(escapeReplace, escapeFn); + return addQuotes(str, singleQuote); + } + let result = ''; let last = 0; for (var i = 0; i < str.length; i++) { const point = str.charCodeAt(i); - if (point === 39 || point === 92 || point < 32) { + if (point === singleQuote || point === 92 || point < 32) { if (last === i) { result += meta[point]; } else { @@ -144,7 +182,7 @@ function strEscape(str) { } else if (last !== i) { result += str.slice(last); } - return `'${result}'`; + return addQuotes(result, singleQuote); } function tryStringify(arg) { diff --git a/test/parallel/test-repl-colors.js b/test/parallel/test-repl-colors.js index dfcc020d899ee6..f484f57945a16b 100644 --- a/test/parallel/test-repl-colors.js +++ b/test/parallel/test-repl-colors.js @@ -24,7 +24,7 @@ process.on('exit', function() { // https://github.com/nodejs/node/pull/16485#issuecomment-350428638 // The color setting of the REPL should not have leaked over into // the color setting of `util.inspect.defaultOptions`. - strictEqual(output.includes(`'\\'string\\''`), true); + strictEqual(output.includes(`"'string'"`), true); strictEqual(output.includes(`'\u001b[32m\\'string\\'\u001b[39m'`), false); strictEqual(inspect.defaultOptions.colors, false); strictEqual(repl.writer.options.colors, true); diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index 1f05f4987eeb41..b5b622b49e60e4 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -47,7 +47,7 @@ assert.strictEqual(util.inspect(new Date('')), (new Date('')).toString()); assert.strictEqual(util.inspect('\n\u0001'), "'\\n\\u0001'"); assert.strictEqual( util.inspect(`${Array(75).fill(1)}'\n\u001d\n\u0003`), - `'${Array(75).fill(1)}\\'\\n\\u001d\\n\\u0003'` + `"${Array(75).fill(1)}'\\n\\u001d\\n\\u0003"` ); assert.strictEqual(util.inspect([]), '[]'); assert.strictEqual(util.inspect(Object.create([])), 'Array {}'); @@ -93,7 +93,7 @@ assert.strictEqual( Object.assign(new String('hello'), { [Symbol('foo')]: 123 }), { showHidden: true } ), - '{ [String: \'hello\'] [length]: 5, [Symbol(foo)]: 123 }' + "{ [String: 'hello'] [length]: 5, [Symbol(foo)]: 123 }" ); assert.strictEqual(util.inspect((new JSStream())._externalStream), @@ -265,7 +265,7 @@ assert.strictEqual( name: { value: 'Tim', enumerable: true }, hidden: { value: 'secret' } })), - '{ name: \'Tim\' }' + "{ name: 'Tim' }" ); // Dynamic properties. @@ -308,7 +308,7 @@ assert.strictEqual( } ); assert.strictEqual(util.inspect(value), - '[ 1, 2, 3, growingLength: [Getter], \'-1\': -1 ]'); + "[ 1, 2, 3, growingLength: [Getter], '-1': -1 ]"); } // Array with inherited number properties. @@ -422,24 +422,24 @@ assert.strictEqual(util.inspect(-5e-324), '-5e-324'); // Test for sparse array. { const a = ['foo', 'bar', 'baz']; - assert.strictEqual(util.inspect(a), '[ \'foo\', \'bar\', \'baz\' ]'); + assert.strictEqual(util.inspect(a), "[ 'foo', 'bar', 'baz' ]"); delete a[1]; - assert.strictEqual(util.inspect(a), '[ \'foo\', <1 empty item>, \'baz\' ]'); + assert.strictEqual(util.inspect(a), "[ 'foo', <1 empty item>, 'baz' ]"); assert.strictEqual( util.inspect(a, true), - '[ \'foo\', <1 empty item>, \'baz\', [length]: 3 ]' + "[ 'foo', <1 empty item>, 'baz', [length]: 3 ]" ); assert.strictEqual(util.inspect(new Array(5)), '[ <5 empty items> ]'); a[3] = 'bar'; a[100] = 'qux'; assert.strictEqual( util.inspect(a, { breakLength: Infinity }), - '[ \'foo\', <1 empty item>, \'baz\', \'bar\', <96 empty items>, \'qux\' ]' + "[ 'foo', <1 empty item>, 'baz', 'bar', <96 empty items>, 'qux' ]" ); delete a[3]; assert.strictEqual( util.inspect(a, { maxArrayLength: 4 }), - '[ \'foo\', <1 empty item>, \'baz\', <97 empty items>, ... 1 more item ]' + "[ 'foo', <1 empty item>, 'baz', <97 empty items>, ... 1 more item ]" ); } @@ -630,13 +630,13 @@ assert.strictEqual(util.inspect(Object.create(Date.prototype)), 'Date {}'); assert.strictEqual( util.inspect(w), - '{ \'\\\\\': 1, \'\\\\\\\\\': 2, \'\\\\\\\\\\\\\': 3, ' + - '\'\\\\\\\\\\\\\\\\\': 4, \'\\n\': 5, \'\\r\': 6 }' + "{ '\\\\': 1, '\\\\\\\\': 2, '\\\\\\\\\\\\': 3, " + + "'\\\\\\\\\\\\\\\\': 4, '\\n': 5, '\\r': 6 }" ); assert.strictEqual( util.inspect(y), - '[ \'a\', \'b\', \'c\', \'\\\\\\\\\': \'d\', ' + - '\'\\n\': \'e\', \'\\r\': \'f\' ]' + "[ 'a', 'b', 'c', '\\\\\\\\': 'd', " + + "'\\n': 'e', '\\r': 'f' ]" ); } @@ -733,7 +733,7 @@ util.inspect({ hasOwnProperty: null }); // A custom [util.inspect.custom]() should be able to return other Objects. subject[util.inspect.custom] = () => ({ foo: 'bar' }); - assert.strictEqual(util.inspect(subject), '{ foo: \'bar\' }'); + assert.strictEqual(util.inspect(subject), "{ foo: 'bar' }"); subject[util.inspect.custom] = (depth, opts) => { assert.strictEqual(opts.customInspectOptions, true); @@ -776,7 +776,7 @@ util.inspect({ hasOwnProperty: null }); } // Test boxed primitives output the correct values. -assert.strictEqual(util.inspect(new String('test')), '[String: \'test\']'); +assert.strictEqual(util.inspect(new String('test')), "[String: 'test']"); assert.strictEqual( util.inspect(Object(Symbol('test'))), '[Symbol: Symbol(test)]' @@ -792,15 +792,15 @@ assert.strictEqual(util.inspect(new Number(13.37)), '[Number: 13.37]'); { const str = new String('baz'); str.foo = 'bar'; - assert.strictEqual(util.inspect(str), '{ [String: \'baz\'] foo: \'bar\' }'); + assert.strictEqual(util.inspect(str), "{ [String: 'baz'] foo: 'bar' }"); const bool = new Boolean(true); bool.foo = 'bar'; - assert.strictEqual(util.inspect(bool), '{ [Boolean: true] foo: \'bar\' }'); + assert.strictEqual(util.inspect(bool), "{ [Boolean: true] foo: 'bar' }"); const num = new Number(13.37); num.foo = 'bar'; - assert.strictEqual(util.inspect(num), '{ [Number: 13.37] foo: \'bar\' }'); + assert.strictEqual(util.inspect(num), "{ [Number: 13.37] foo: 'bar' }"); } // Test es6 Symbol. @@ -829,7 +829,7 @@ if (typeof Symbol !== 'undefined') { assert.strictEqual(util.inspect(subject), '{ [Symbol(symbol)]: 42 }'); assert.strictEqual( util.inspect(subject, options), - '{ [Symbol(symbol)]: 42, [Symbol()]: \'non-enum\' }' + "{ [Symbol(symbol)]: 42, [Symbol()]: 'non-enum' }" ); subject = [1, 2, 3]; @@ -847,7 +847,7 @@ if (typeof Symbol !== 'undefined') { set.bar = 42; assert.strictEqual( util.inspect(set, true), - 'Set { \'foo\', [size]: 1, bar: 42 }' + "Set { 'foo', [size]: 1, bar: 42 }" ); } @@ -862,11 +862,11 @@ if (typeof Symbol !== 'undefined') { { assert.strictEqual(util.inspect(new Map()), 'Map {}'); assert.strictEqual(util.inspect(new Map([[1, 'a'], [2, 'b'], [3, 'c']])), - 'Map { 1 => \'a\', 2 => \'b\', 3 => \'c\' }'); + "Map { 1 => 'a', 2 => 'b', 3 => 'c' }"); const map = new Map([['foo', null]]); map.bar = 42; assert.strictEqual(util.inspect(map, true), - 'Map { \'foo\' => null, [size]: 1, bar: 42 }'); + "Map { 'foo' => null, [size]: 1, bar: 42 }"); } // Test circular Map. @@ -897,7 +897,7 @@ if (typeof Symbol !== 'undefined') { const promiseWithProperty = Promise.resolve('foo'); promiseWithProperty.bar = 42; assert.strictEqual(util.inspect(promiseWithProperty), - 'Promise { \'foo\', bar: 42 }'); + "Promise { 'foo', bar: 42 }"); } // Make sure it doesn't choke on polyfills. Unlike Set/Map, there is no standard @@ -913,14 +913,14 @@ if (typeof Symbol !== 'undefined') { // Test Map iterators. { const map = new Map([['foo', 'bar']]); - assert.strictEqual(util.inspect(map.keys()), '[Map Iterator] { \'foo\' }'); - assert.strictEqual(util.inspect(map.values()), '[Map Iterator] { \'bar\' }'); + assert.strictEqual(util.inspect(map.keys()), "[Map Iterator] { 'foo' }"); + assert.strictEqual(util.inspect(map.values()), "[Map Iterator] { 'bar' }"); assert.strictEqual(util.inspect(map.entries()), - '[Map Iterator] { [ \'foo\', \'bar\' ] }'); + "[Map Iterator] { [ 'foo', 'bar' ] }"); // Make sure the iterator doesn't get consumed. const keys = map.keys(); - assert.strictEqual(util.inspect(keys), '[Map Iterator] { \'foo\' }'); - assert.strictEqual(util.inspect(keys), '[Map Iterator] { \'foo\' }'); + assert.strictEqual(util.inspect(keys), "[Map Iterator] { 'foo' }"); + assert.strictEqual(util.inspect(keys), "[Map Iterator] { 'foo' }"); keys.extra = true; assert.strictEqual( util.inspect(keys, { maxArrayLength: 0 }), @@ -995,7 +995,7 @@ if (typeof Symbol !== 'undefined') { assert.strictEqual(util.inspect(new SetSubclass([1, 2, 3])), 'SetSubclass [Set] { 1, 2, 3 }'); assert.strictEqual(util.inspect(new MapSubclass([['foo', 42]])), - 'MapSubclass [Map] { \'foo\' => 42 }'); + "MapSubclass [Map] { 'foo' => 42 }"); assert.strictEqual(util.inspect(new PromiseSubclass(() => {})), 'PromiseSubclass [Promise] { }'); assert.strictEqual( @@ -1048,7 +1048,7 @@ if (typeof Symbol !== 'undefined') { { const x = []; x[''] = 1; - assert.strictEqual(util.inspect(x), '[ \'\': 1 ]'); + assert.strictEqual(util.inspect(x), "[ '': 1 ]"); } // The following maxArrayLength tests were introduced after v6.0.0 was released. @@ -1094,10 +1094,10 @@ if (typeof Symbol !== 'undefined') { const breakpoint = oneLine.length - 5; const twoLines = util.inspect(obj, { breakLength: breakpoint }); - assert.strictEqual(oneLine, '{ foo: \'abc\', bar: \'xyz\' }'); + assert.strictEqual(oneLine, "{ foo: 'abc', bar: 'xyz' }"); assert.strictEqual(oneLine, util.inspect(obj, { breakLength: breakpoint + 1 })); - assert.strictEqual(twoLines, '{ foo: \'abc\',\n bar: \'xyz\' }'); + assert.strictEqual(twoLines, "{ foo: 'abc',\n bar: 'xyz' }"); } // util.inspect.defaultOptions tests. @@ -1168,7 +1168,7 @@ util.inspect(process); { // @@toStringTag assert.strictEqual(util.inspect({ [Symbol.toStringTag]: 'a' }), - 'Object [a] { [Symbol(Symbol.toStringTag)]: \'a\' }'); + "Object [a] { [Symbol(Symbol.toStringTag)]: 'a' }"); class Foo { constructor() { @@ -1184,17 +1184,17 @@ util.inspect(process); Object.create(null, { [Symbol.toStringTag]: { value: 'foo' } })), '[foo] {}'); - assert.strictEqual(util.inspect(new Foo()), 'Foo [bar] { foo: \'bar\' }'); + assert.strictEqual(util.inspect(new Foo()), "Foo [bar] { foo: 'bar' }"); assert.strictEqual( util.inspect(new (class extends Foo {})()), - 'Foo [bar] { foo: \'bar\' }'); + "Foo [bar] { foo: 'bar' }"); assert.strictEqual( util.inspect(Object.create(Object.create(Foo.prototype), { foo: { value: 'bar', enumerable: true } })), - 'Foo [bar] { foo: \'bar\' }'); + "Foo [bar] { foo: 'bar' }"); class ThrowingClass { get [Symbol.toStringTag]() { @@ -1246,19 +1246,19 @@ util.inspect(process); ' 2,', ' [', ' [', - ' \'Lorem ipsum dolor\\nsit amet,\\tconsectetur \' +', - ' \'adipiscing elit, sed do eiusmod tempor \' +', - ' \'incididunt ut labore et dolore magna \' +', - ' \'aliqua.\',', - ' \'test\',', - ' \'foo\'', + " 'Lorem ipsum dolor\\nsit amet,\\tconsectetur ' +", + " 'adipiscing elit, sed do eiusmod tempor ' +", + " 'incididunt ut labore et dolore magna ' +", + " 'aliqua.',", + " 'test',", + " 'foo'", ' ]', ' ],', ' 4', ' ],', ' b: Map {', - ' \'za\' => 1,', - ' \'zb\' => \'test\'', + " 'za' => 1,", + " 'zb' => 'test'", ' }', '}' ].join('\n'); @@ -1266,28 +1266,28 @@ util.inspect(process); out = util.inspect(o.a[2][0][0], { compact: false, breakLength: 30 }); expect = [ - '\'Lorem ipsum dolor\\nsit \' +', - ' \'amet,\\tconsectetur \' +', - ' \'adipiscing elit, sed do \' +', - ' \'eiusmod tempor incididunt \' +', - ' \'ut labore et dolore magna \' +', - ' \'aliqua.\'' + "'Lorem ipsum dolor\\nsit ' +", + " 'amet,\\tconsectetur ' +", + " 'adipiscing elit, sed do ' +", + " 'eiusmod tempor incididunt ' +", + " 'ut labore et dolore magna ' +", + " 'aliqua.'" ].join('\n'); assert.strictEqual(out, expect); out = util.inspect( '12345678901234567890123456789012345678901234567890', { compact: false, breakLength: 3 }); - expect = '\'12345678901234567890123456789012345678901234567890\''; + expect = "'12345678901234567890123456789012345678901234567890'"; assert.strictEqual(out, expect); out = util.inspect( '12 45 78 01 34 67 90 23 56 89 123456789012345678901234567890', { compact: false, breakLength: 3 }); expect = [ - '\'12 45 78 01 34 \' +', - ' \'67 90 23 56 89 \' +', - ' \'123456789012345678901234567890\'' + "'12 45 78 01 34 ' +", + " '67 90 23 56 89 ' +", + " '123456789012345678901234567890'" ].join('\n'); assert.strictEqual(out, expect); @@ -1295,9 +1295,9 @@ util.inspect(process); '12 45 78 01 34 67 90 23 56 89 1234567890123 0', { compact: false, breakLength: 3 }); expect = [ - '\'12 45 78 01 34 \' +', - ' \'67 90 23 56 89 \' +', - ' \'1234567890123 0\'' + "'12 45 78 01 34 ' +", + " '67 90 23 56 89 ' +", + " '1234567890123 0'" ].join('\n'); assert.strictEqual(out, expect); @@ -1305,10 +1305,10 @@ util.inspect(process); '12 45 78 01 34 67 90 23 56 89 12345678901234567 0', { compact: false, breakLength: 3 }); expect = [ - '\'12 45 78 01 34 \' +', - ' \'67 90 23 56 89 \' +', - ' \'12345678901234567 \' +', - ' \'0\'' + "'12 45 78 01 34 ' +", + " '67 90 23 56 89 ' +", + " '12345678901234567 ' +", + " '0'" ].join('\n'); assert.strictEqual(out, expect); @@ -1347,7 +1347,7 @@ util.inspect(process); o[util.inspect.custom] = () => ({ a: '12 45 78 01 34 67 90 23' }); out = util.inspect(o, { compact: false, breakLength: 3 }); - expect = '{\n a: \'12 45 78 01 34 \' +\n \'67 90 23\'\n}'; + expect = "{\n a: '12 45 78 01 34 ' +\n '67 90 23'\n}"; assert.strictEqual(out, expect); } @@ -1424,3 +1424,9 @@ util.inspect(process); assert(longList.includes('[Object: Inspection interrupted ' + 'prematurely. Maximum call stack size exceeded.]')); } + +// Do not escape single quotes if no double quote or backtick is present. +assert.strictEqual(util.inspect("'"), '"\'"'); +assert.strictEqual(util.inspect('"\''), '`"\'`'); +// eslint-disable-next-line no-template-curly-in-string +assert.strictEqual(util.inspect('"\'${a}'), "'\"\\'${a}'");