From 9d76c4da980f251acf34e885ee1d0fe043145d53 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Thu, 4 Mar 2021 13:10:52 +1300 Subject: [PATCH 01/12] Support setting array indexes Signed-off-by: Richie Bendall --- index.d.ts | 7 +++++ index.js | 8 +++++- readme.md | 11 ++++++-- test.js | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 3 deletions(-) diff --git a/index.d.ts b/index.d.ts index 2976791..a5d11de 100644 --- a/index.d.ts +++ b/index.d.ts @@ -21,6 +21,9 @@ declare const dotProp: { dotProp.get({foo: {'dot.dot': 'unicorn'}}, 'foo.dot\\.dot'); //=> 'unicorn' + + dotProp.get({foo: [{bar: 'unicorn'}]}, 'foo.0.bar'); + //=> 'unicorn' ``` */ get( @@ -57,6 +60,10 @@ declare const dotProp: { dotProp.set(object, 'foo.baz', 'x'); console.log(object); //=> {foo: {bar: 'b', baz: 'x'}} + + dotProp.set(object, 'foo.bar.0', 'a'); + console.log(object); + //=> {foo: {bar: ['a']}} ``` */ set( diff --git a/index.js b/index.js index e1f316f..99c06d4 100644 --- a/index.js +++ b/index.js @@ -74,7 +74,13 @@ module.exports = { const p = pathArray[i]; if (!isObject(object[p])) { - object[p] = {}; + const numericalPath = Number(pathArray[i + 1]); + + if (Number.isInteger(numericalPath) && numericalPath >= 0) { + object[p] = []; + } else { + object[p] = {}; + } } if (i === pathArray.length - 1) { diff --git a/readme.md b/readme.md index 3556493..6ee7fc1 100644 --- a/readme.md +++ b/readme.md @@ -26,6 +26,9 @@ dotProp.get({foo: {bar: 'a'}}, 'foo.notDefined.deep', 'default value'); dotProp.get({foo: {'dot.dot': 'unicorn'}}, 'foo.dot\\.dot'); //=> 'unicorn' +dotProp.get({foo: [{bar: 'unicorn'}]}, 'foo.0.bar'); +//=> 'unicorn' + // Setter const object = {foo: {bar: 'a'}}; dotProp.set(object, 'foo.bar', 'b'); @@ -40,6 +43,10 @@ dotProp.set(object, 'foo.baz', 'x'); console.log(object); //=> {foo: {bar: 'b', baz: 'x'}} +dotProp.set(object, 'foo.bar.0', 'a'); +console.log(object); +//=> {foo: {bar: ['a']}} + // Has dotProp.has({foo: {bar: 'unicorn'}}, 'foo.bar'); //=> true @@ -84,9 +91,9 @@ Returns a boolean of whether the property existed before being deleted. #### object -Type: `object` +Type: `object | array` -Object to get, set, or delete the `path` value. +Object or array to get, set, or delete the `path` value. You are allowed to pass in `undefined` as the object to the `get` and `has` functions. diff --git a/test.js b/test.js index 9a3f6e9..1e65b5e 100644 --- a/test.js +++ b/test.js @@ -26,6 +26,22 @@ test('get', t => { t.is(dotProp.get({'foo\\.bar': true}, 'foo\\\\.bar'), true); t.is(dotProp.get({foo: 1}, 'foo.bar'), undefined); + t.true(dotProp.get([true, false, false], '0')); + t.true(dotProp.get([{foo: [true]}], '0.foo.0')); + t.true(dotProp.get({foo: [0, {bar: true}]}, 'foo.1.bar')); + + t.false(dotProp.get(['a', 'b', 'c'], '3', false)); + t.false(dotProp.get([{foo: [1]}], '0.bar.0', false)); + t.false(dotProp.get([{foo: [1]}], '0.foo.1', false)); + t.false(dotProp.get({foo: [0, {bar: 2}]}, 'foo.0.bar', false)); + t.false(dotProp.get({foo: [0, {bar: 2}]}, 'foo.2.bar', false)); + t.false(dotProp.get({foo: [0, {bar: 2}]}, 'foo.1.biz', false)); + t.false(dotProp.get({foo: [0, {bar: 2}]}, 'bar.0.bar', false)); + + t.false(dotProp.get([], 'foo.0.bar', false)); + t.true(dotProp.get({foo: [{bar: true}]}, 'foo.0.bar')); + t.false(dotProp.get({foo: ['bar']}, 'foo.1', false)); + const fixture2 = {}; Object.defineProperty(fixture2, 'foo', { value: 'bar', @@ -117,6 +133,24 @@ test('set', t => { const output4 = dotProp.set(fixture4, 'foo.bar', 2); t.is(fixture4, 'noobject'); t.is(output4, fixture4); + + const fixture5 = []; + + dotProp.set(fixture5, '1', true); + t.true(fixture5[1]); + + dotProp.set(fixture5, '0.foo.0', true); + t.true(fixture5[0].foo[0]); + + const fixture6 = {}; + + dotProp.set(fixture6, 'foo.0.bar', true); + t.true(fixture6.foo[0].bar); + t.deepEqual(fixture6, { + foo: [{ + bar: true + }] + }); }); test('delete', t => { @@ -180,6 +214,37 @@ test('delete', t => { const fixture3 = {foo: null}; t.false(dotProp.delete(fixture3, 'foo.bar')); t.deepEqual(fixture3, {foo: null}); + + const fixture4 = [{ + top: { + dog: 'sindre' + } + }]; + + t.true(dotProp.delete(fixture4, '0.top.dog')); + t.deepEqual(fixture4, [{top: {}}]); + + const fixture5 = { + foo: [{ + bar: ['foo', 'bar'] + }] + }; + + dotProp.delete(fixture5, 'foo.0.bar.0'); + + const fixtureArray = []; + fixtureArray[1] = 'bar'; + + t.deepEqual(fixture5, { + foo: [{ + bar: fixtureArray + }] + }); + + const fixture6 = {}; + + dotProp.set(fixture6, 'foo.bar.0', 'fizz'); + t.is(fixture6.foo.bar[0], 'fizz'); }); test('has', t => { @@ -205,6 +270,16 @@ test('has', t => { t.true(dotProp.has({'foo.baz': {bar: true}}, 'foo\\.baz.bar')); t.true(dotProp.has({'fo.ob.az': {bar: true}}, 'fo\\.ob\\.az.bar')); t.false(dotProp.has(undefined, 'fo\\.ob\\.az.bar')); + + t.true(dotProp.has({ + foo: [{bar: ['bar', 'bizz']}] + }, 'foo.0.bar.1')); + t.false(dotProp.has({ + foo: [{bar: ['bar', 'bizz']}] + }, 'foo.0.bar.2')); + t.false(dotProp.has({ + foo: [{bar: ['bar', 'bizz']}] + }, 'foo.1.bar.1')); }); test('prevent setting/getting `__proto__`', t => { From d31ce1dfa281526d9766dfe75eb503d89d13989b Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Thu, 4 Mar 2021 13:37:16 +1300 Subject: [PATCH 02/12] Update readme.md --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 6ee7fc1..01d36af 100644 --- a/readme.md +++ b/readme.md @@ -43,9 +43,9 @@ dotProp.set(object, 'foo.baz', 'x'); console.log(object); //=> {foo: {bar: 'b', baz: 'x'}} -dotProp.set(object, 'foo.bar.0', 'a'); +dotProp.set(object, 'foo.biz.0', 'a'); console.log(object); -//=> {foo: {bar: ['a']}} +//=> {foo: {bar: 'b', baz: 'x', biz: ['a']}} // Has dotProp.has({foo: {bar: 'unicorn'}}, 'foo.bar'); From 1c4c946281588d5e3871ca40b0ab5e9aebe76f12 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Thu, 4 Mar 2021 13:37:37 +1300 Subject: [PATCH 03/12] Update index.d.ts --- index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index a5d11de..18b0476 100644 --- a/index.d.ts +++ b/index.d.ts @@ -61,9 +61,9 @@ declare const dotProp: { console.log(object); //=> {foo: {bar: 'b', baz: 'x'}} - dotProp.set(object, 'foo.bar.0', 'a'); + dotProp.set(object, 'foo.biz.0', 'a'); console.log(object); - //=> {foo: {bar: ['a']}} + //=> {foo: {bar: 'b', baz: 'x', biz: ['a']}} ``` */ set( From 996eb30b9275a59f3cb08124df6e9d6fe6fd39be Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Wed, 8 Sep 2021 03:44:02 +1200 Subject: [PATCH 04/12] Parse the path for real Signed-off-by: Richie Bendall --- index.d.ts | 4 +- index.js | 149 +++++++++++++++++++++++++++++++++++++++++++++++------ readme.md | 2 +- test.js | 49 ++++++++++-------- 4 files changed, 164 insertions(+), 40 deletions(-) diff --git a/index.d.ts b/index.d.ts index c65b162..cb1ee76 100644 --- a/index.d.ts +++ b/index.d.ts @@ -24,7 +24,7 @@ declare const dotProp: { dotProp.get({foo: {'dot.dot': 'unicorn'}}, 'foo.dot\\.dot'); //=> 'unicorn' - dotProp.get({foo: [{bar: 'unicorn'}]}, 'foo.0.bar'); + dotProp.get({foo: [{bar: 'unicorn'}]}, 'foo[0].bar'); //=> 'unicorn' ``` */ @@ -32,7 +32,7 @@ declare const dotProp: { object: ObjectType, path: PathType, defaultValue?: DefaultValue - ) => ObjectType extends Record ? (Get extends unknown ? DefaultValue : Get) : undefined; // TODO: When adding array index support (https://github.com/sindresorhus/dot-prop/issues/71) add ` | unknown[]` after `Record` + ) => ObjectType extends Record | unknown[] ? (Get extends unknown ? DefaultValue : Get) : undefined; /** Set the property at the given path to the given value. diff --git a/index.js b/index.js index 99c06d4..26e8c9d 100644 --- a/index.js +++ b/index.js @@ -7,25 +7,136 @@ const disallowedKeys = new Set([ 'constructor' ]); -const isValidPath = pathSegments => !pathSegments.some(segment => disallowedKeys.has(segment)); - function getPathSegments(path) { - const pathArray = path.split('.'); const parts = []; - for (let i = 0; i < pathArray.length; i++) { - let p = pathArray[i]; + let isIgnoring = false; + let isPath = true; + let isIndex = false; + let currentPathSegment = ''; + + for (const character of path) { + switch (character) { + case '\\': + if (isIgnoring) { + isIgnoring = false; + currentPathSegment += '\\'; + } + + isIgnoring = !isIgnoring; + break; + + case '.': + if (isIgnoring) { + isIgnoring = false; + currentPathSegment += '.'; + break; + } + + if (isIndex) { + isIndex = false; + currentPathSegment = `[${currentPathSegment}`; + } + + if (isPath && currentPathSegment.length > 0) { + if (disallowedKeys.has(currentPathSegment)) { + return []; + } + + parts.push(currentPathSegment); + currentPathSegment = ''; + } + + isPath = true; + break; + + case '[': + if (isIgnoring) { + isIgnoring = false; + currentPathSegment += '['; + break; + } + + if (isPath) { + if (currentPathSegment !== '' || parts.length === 0) { + isPath = false; + isIndex = true; + + if (currentPathSegment.length > 0) { + if (disallowedKeys.has(currentPathSegment)) { + return []; + } + + parts.push(currentPathSegment); + currentPathSegment = ''; + } + } else { + currentPathSegment += '['; + } + + break; + } + + if (isIndex) { + isPath = true; + currentPathSegment = `[${currentPathSegment}`; + } + + isIndex = !isIndex; + break; + + case ']': + if (isIgnoring && isIndex) { + isIgnoring = false; + isIndex = false; + currentPathSegment += ']'; + break; + } + + if (isIndex) { + isIndex = false; + isPath = true; + const index = Number.parseInt(currentPathSegment, 10); + if (Number.isNaN(index)) { + if (disallowedKeys.has(currentPathSegment)) { + return []; + } + + parts.push(currentPathSegment); + } else { + parts.push(index); + } + + currentPathSegment = ''; + break; + } + + // Falls through + + default: + if (isIgnoring) { + isIgnoring = false; + currentPathSegment += '\\'; + } - while (p[p.length - 1] === '\\' && pathArray[i + 1] !== undefined) { - p = p.slice(0, -1) + '.'; - p += pathArray[++i]; + currentPathSegment += character; } + } - parts.push(p); + if (isIndex) { + currentPathSegment = `[${currentPathSegment}`; } - if (!isValidPath(parts)) { - return []; + if (isIgnoring) { + currentPathSegment += '\\'; + } + + if (currentPathSegment.length > 0 || parts.length === 0) { + if (disallowedKeys.has(currentPathSegment)) { + return []; + } + + parts.push(currentPathSegment); } return parts; @@ -43,12 +154,20 @@ module.exports = { } for (let i = 0; i < pathArray.length; i++) { - object = object[pathArray[i]]; + const key = pathArray[i]; + const index = Number.parseInt(key, 10); + + // Disallow string indexes + if (!Number.isInteger(key) && Array.isArray(object) && !Number.isNaN(index) && object[index] === object[key]) { + object = i === pathArray.length - 1 ? undefined : null; + } else { + object = object[key]; + } if (object === undefined || object === null) { // `object` is either `undefined` or `null` so we want to stop the loop, and // if this is not the last bit of the path, and - // if it did't return `undefined` + // if it didn't return `undefined` // it would return `null` if `object` is `null` // but we want `get({foo: null}, 'foo.bar')` to equal `undefined`, or the supplied value, not `null` if (i !== pathArray.length - 1) { @@ -74,9 +193,7 @@ module.exports = { const p = pathArray[i]; if (!isObject(object[p])) { - const numericalPath = Number(pathArray[i + 1]); - - if (Number.isInteger(numericalPath) && numericalPath >= 0) { + if (Number.isInteger(pathArray[i + 1])) { object[p] = []; } else { object[p] = {}; diff --git a/readme.md b/readme.md index 01d36af..e63f301 100644 --- a/readme.md +++ b/readme.md @@ -26,7 +26,7 @@ dotProp.get({foo: {bar: 'a'}}, 'foo.notDefined.deep', 'default value'); dotProp.get({foo: {'dot.dot': 'unicorn'}}, 'foo.dot\\.dot'); //=> 'unicorn' -dotProp.get({foo: [{bar: 'unicorn'}]}, 'foo.0.bar'); +dotProp.get({foo: [{bar: 'unicorn'}]}, 'foo[0].bar'); //=> 'unicorn' // Setter diff --git a/test.js b/test.js index be058b9..9c6c224 100644 --- a/test.js +++ b/test.js @@ -26,21 +26,28 @@ test('get', t => { t.true(dotProp.get({'foo\\.bar': true}, 'foo\\\\.bar')); t.is(dotProp.get({foo: 1}, 'foo.bar'), undefined); - t.true(dotProp.get([true, false, false], '0')); - t.true(dotProp.get([{foo: [true]}], '0.foo.0')); - t.true(dotProp.get({foo: [0, {bar: true}]}, 'foo.1.bar')); + t.true(dotProp.get([true, false, false], '[0]')); + t.true(dotProp.get([{foo: [true]}], '[0].foo[0]')); + t.true(dotProp.get({foo: [0, {bar: true}]}, 'foo[1].bar')); t.false(dotProp.get(['a', 'b', 'c'], '3', false)); - t.false(dotProp.get([{foo: [1]}], '0.bar.0', false)); - t.false(dotProp.get([{foo: [1]}], '0.foo.1', false)); - t.false(dotProp.get({foo: [0, {bar: 2}]}, 'foo.0.bar', false)); - t.false(dotProp.get({foo: [0, {bar: 2}]}, 'foo.2.bar', false)); - t.false(dotProp.get({foo: [0, {bar: 2}]}, 'foo.1.biz', false)); - t.false(dotProp.get({foo: [0, {bar: 2}]}, 'bar.0.bar', false)); + t.false(dotProp.get([{foo: [1]}], '[0].bar[0]', false)); + t.false(dotProp.get([{foo: [1]}], '[0].foo[1]', false)); + t.false(dotProp.get({foo: [0, {bar: 2}]}, 'foo[0].bar', false)); + t.false(dotProp.get({foo: [0, {bar: 2}]}, 'foo[2].bar', false)); + t.false(dotProp.get({foo: [0, {bar: 2}]}, 'foo[1].biz', false)); + t.false(dotProp.get({foo: [0, {bar: 2}]}, 'bar[0].bar', false)); + t.true(dotProp.get({ + bar: { + '[0]': true + } + }, 'bar.[0]')); + + t.false(dotProp.get([], 'foo[0].bar', false)); + t.true(dotProp.get({foo: [{bar: true}]}, 'foo[0].bar')); + t.false(dotProp.get({foo: ['bar']}, 'foo[1]', false)); - t.false(dotProp.get([], 'foo.0.bar', false)); - t.true(dotProp.get({foo: [{bar: true}]}, 'foo.0.bar')); - t.false(dotProp.get({foo: ['bar']}, 'foo.1', false)); + t.false(dotProp.get([true], '0', false)); const fixture2 = {}; Object.defineProperty(fixture2, 'foo', { @@ -50,7 +57,7 @@ test('get', t => { t.is(dotProp.get(fixture2, 'foo'), 'bar'); t.is(dotProp.get({}, 'hasOwnProperty'), Object.prototype.hasOwnProperty); - function fn() {} + function fn() { } fn.foo = {bar: 1}; t.is(dotProp.get(fn), fn); t.is(dotProp.get(fn, 'foo'), fn.foo); @@ -68,7 +75,7 @@ test('get', t => { t.false(dotProp.get([], 'foo.bar', false)); t.false(dotProp.get(undefined, 'foo.bar', false)); - class F4Class {} + class F4Class { } F4Class.prototype.foo = 1; const f4 = new F4Class(); t.is(dotProp.get(f4, 'foo'), 1); // #46 @@ -107,7 +114,7 @@ test('set', t => { dotProp.set(fixture1, 'foo.function', func); t.is(fixture1.foo.function, func); - function fn() {} + function fn() { } dotProp.set(fn, 'foo.bar', 1); t.is(fn.foo.bar, 1); @@ -144,7 +151,7 @@ test('set', t => { const fixture6 = {}; - dotProp.set(fixture6, 'foo.0.bar', true); + dotProp.set(fixture6, 'foo[0].bar', true); t.true(fixture6.foo[0].bar); t.deepEqual(fixture6, { foo: [{ @@ -230,7 +237,7 @@ test('delete', t => { }] }; - dotProp.delete(fixture5, 'foo.0.bar.0'); + dotProp.delete(fixture5, 'foo[0].bar[0]'); const fixtureArray = []; fixtureArray[1] = 'bar'; @@ -261,7 +268,7 @@ test('has', t => { t.false(dotProp.has({foo: null}, 'foo.bar')); t.false(dotProp.has({foo: ''}, 'foo.bar')); - function fn() {} + function fn() { } fn.foo = {bar: 1}; t.false(dotProp.has(fn)); t.true(dotProp.has(fn, 'foo')); @@ -273,13 +280,13 @@ test('has', t => { t.true(dotProp.has({ foo: [{bar: ['bar', 'bizz']}] - }, 'foo.0.bar.1')); + }, 'foo[0].bar.1')); t.false(dotProp.has({ foo: [{bar: ['bar', 'bizz']}] - }, 'foo.0.bar.2')); + }, 'foo[0].bar.2')); t.false(dotProp.has({ foo: [{bar: ['bar', 'bizz']}] - }, 'foo.1.bar.1')); + }, 'foo[1].bar.1')); }); test('prevent setting/getting `__proto__`', t => { From 502af844d55d397991fee1d77a4c951e789cb0e1 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Wed, 8 Sep 2021 03:47:52 +1200 Subject: [PATCH 05/12] Fix TypeScript typings Signed-off-by: Richie Bendall --- index.d.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/index.d.ts b/index.d.ts index cb1ee76..0edfe85 100644 --- a/index.d.ts +++ b/index.d.ts @@ -4,7 +4,7 @@ declare const dotProp: { /** Get the value of the property at the given path. - @param object - Object to get the `path` value. + @param object - Object or array to get the `path` value. @param path - Path of the property in the object, using `.` to separate each nested key. Use `\\.` if you have a `.` in the key. @param defaultValue - Default value. @@ -37,7 +37,7 @@ declare const dotProp: { /** Set the property at the given path to the given value. - @param object - Object to set the `path` value. + @param object - Object or array to set the `path` value. @param path - Path of the property in the object, using `.` to separate each nested key. Use `\\.` if you have a `.` in the key. @param value - Value to set at `path`. @returns The object. @@ -73,7 +73,7 @@ declare const dotProp: { /** Check whether the property at the given path exists. - @param object - Object to test the `path` value. + @param object - Object or array to test the `path` value. @param path - Path of the property in the object, using `.` to separate each nested key. Use `\\.` if you have a `.` in the key. @example @@ -89,7 +89,7 @@ declare const dotProp: { /** Delete the property at the given path. - @param object - Object to delete the `path` value. + @param object - Object or array to delete the `path` value. @param path - Path of the property in the object, using `.` to separate each nested key. Use `\\.` if you have a `.` in the key. @returns A boolean of whether the property existed before being deleted. From 7c6e8ac743501ace4bf869931206321ab54eaa96 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Wed, 8 Sep 2021 03:48:33 +1200 Subject: [PATCH 06/12] Fix lint Signed-off-by: Richie Bendall --- test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test.js b/test.js index 9c6c224..ebe1bf7 100644 --- a/test.js +++ b/test.js @@ -57,7 +57,7 @@ test('get', t => { t.is(dotProp.get(fixture2, 'foo'), 'bar'); t.is(dotProp.get({}, 'hasOwnProperty'), Object.prototype.hasOwnProperty); - function fn() { } + function fn() {} fn.foo = {bar: 1}; t.is(dotProp.get(fn), fn); t.is(dotProp.get(fn, 'foo'), fn.foo); @@ -75,7 +75,7 @@ test('get', t => { t.false(dotProp.get([], 'foo.bar', false)); t.false(dotProp.get(undefined, 'foo.bar', false)); - class F4Class { } + class F4Class {} F4Class.prototype.foo = 1; const f4 = new F4Class(); t.is(dotProp.get(f4, 'foo'), 1); // #46 @@ -114,7 +114,7 @@ test('set', t => { dotProp.set(fixture1, 'foo.function', func); t.is(fixture1.foo.function, func); - function fn() { } + function fn() {} dotProp.set(fn, 'foo.bar', 1); t.is(fn.foo.bar, 1); @@ -268,7 +268,7 @@ test('has', t => { t.false(dotProp.has({foo: null}, 'foo.bar')); t.false(dotProp.has({foo: ''}, 'foo.bar')); - function fn() { } + function fn() {} fn.foo = {bar: 1}; t.false(dotProp.has(fn)); t.true(dotProp.has(fn, 'foo')); From ae7107036272ae61727db9b0c59740554ec17a00 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Wed, 8 Sep 2021 15:07:46 +1200 Subject: [PATCH 07/12] Full coverage Signed-off-by: Richie Bendall --- index.js | 10 ++++++---- test.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 26e8c9d..27f636d 100644 --- a/index.js +++ b/index.js @@ -35,7 +35,8 @@ function getPathSegments(path) { if (isIndex) { isIndex = false; - currentPathSegment = `[${currentPathSegment}`; + parts.push(`${parts.pop() || ''}[${currentPathSegment}`); + currentPathSegment = ''; } if (isPath && currentPathSegment.length > 0) { @@ -79,7 +80,7 @@ function getPathSegments(path) { if (isIndex) { isPath = true; - currentPathSegment = `[${currentPathSegment}`; + currentPathSegment = `${parts.pop() || ''}[${currentPathSegment}[`; } isIndex = !isIndex; @@ -89,7 +90,8 @@ function getPathSegments(path) { if (isIgnoring && isIndex) { isIgnoring = false; isIndex = false; - currentPathSegment += ']'; + isPath = true; + currentPathSegment = `${parts.pop()}[${currentPathSegment}]`; break; } @@ -124,7 +126,7 @@ function getPathSegments(path) { } if (isIndex) { - currentPathSegment = `[${currentPathSegment}`; + currentPathSegment = `${parts.pop()}[${currentPathSegment}`; } if (isIgnoring) { diff --git a/test.js b/test.js index ebe1bf7..c89ec96 100644 --- a/test.js +++ b/test.js @@ -42,6 +42,36 @@ test('get', t => { '[0]': true } }, 'bar.[0]')); + t.true(dotProp.get({ + 'foo[5[': true + }, 'foo[5[')); + t.true(dotProp.get({ + 'foo[5': { + bar: true + } + }, 'foo[5.bar')); + t.true(dotProp.get({ + 'foo[5]': { + bar: true + } + }, 'foo\\[5].bar')); + t.true(dotProp.get({ + 'foo[5]': { + bar: true + } + }, 'foo[5\\].bar')); + t.true(dotProp.get({ + 'foo[5': { + bar: true + } + }, 'foo[5')); + t.true(dotProp.get({ + foo: { + bar: true + } + }, 'foo[bar]')); + t.false(dotProp.get({}, '__proto__[0]', false)); + t.false(dotProp.get({}, 'foo[__proto__]', false)); t.false(dotProp.get([], 'foo[0].bar', false)); t.true(dotProp.get({foo: [{bar: true}]}, 'foo[0].bar')); From 008ad61f8302e40a761ae0e8fc1b53f67051aaad Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Wed, 8 Sep 2021 17:17:30 +1200 Subject: [PATCH 08/12] Update test.js --- test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test.js b/test.js index c89ec96..25f134c 100644 --- a/test.js +++ b/test.js @@ -70,8 +70,8 @@ test('get', t => { bar: true } }, 'foo[bar]')); - t.false(dotProp.get({}, '__proto__[0]', false)); - t.false(dotProp.get({}, 'foo[__proto__]', false)); + t.false(dotProp.get({}, 'constructor[0]', false)); + t.false(dotProp.get({}, 'foo[constructor]', false)); t.false(dotProp.get([], 'foo[0].bar', false)); t.true(dotProp.get({foo: [{bar: true}]}, 'foo[0].bar')); From 669e36d7bb26b72245f17b1303a5c0a6694125ec Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Wed, 8 Sep 2021 17:20:11 +1200 Subject: [PATCH 09/12] Update test.js --- test.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test.js b/test.js index dfb0b6c..2adaa1e 100644 --- a/test.js +++ b/test.js @@ -61,9 +61,7 @@ test('get', t => { } }, 'foo[5\\].bar')); t.true(dotProp.get({ - 'foo[5': { - bar: true - } + 'foo[5': true }, 'foo[5')); t.true(dotProp.get({ foo: { From 26dabdd4ccac9f515c23ca27d09e076bac739cb1 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Fri, 10 Sep 2021 19:22:17 +1200 Subject: [PATCH 10/12] Parse `foo[bar]` literally and use better integer checks Signed-off-by: Richie Bendall --- index.d.ts | 2 +- index.js | 17 ++++++++--------- test.js | 4 +--- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/index.d.ts b/index.d.ts index 0edfe85..780daab 100644 --- a/index.d.ts +++ b/index.d.ts @@ -59,7 +59,7 @@ declare const dotProp: { console.log(object); //=> {foo: {bar: 'b', baz: 'x'}} - dotProp.set(object, 'foo.biz.0', 'a'); + dotProp.set(object, 'foo.biz[0]', 'a'); console.log(object); //=> {foo: {bar: 'b', baz: 'x', biz: ['a']}} ``` diff --git a/index.js b/index.js index a977bc4..5ec8973 100644 --- a/index.js +++ b/index.js @@ -98,15 +98,18 @@ function getPathSegments(path) { if (isIndex) { isIndex = false; isPath = true; - const index = Number.parseInt(currentPathSegment, 10); - if (Number.isNaN(index)) { + const index = Number(currentPathSegment); + + if (Number.isInteger(index)) { + parts.push(index); + } else { + currentPathSegment = `${parts.pop() || ''}[${currentPathSegment}]`; + if (disallowedKeys.has(currentPathSegment)) { return []; } parts.push(currentPathSegment); - } else { - parts.push(index); } currentPathSegment = ''; @@ -195,11 +198,7 @@ module.exports = { const p = pathArray[i]; if (!isObject(object[p])) { - if (Number.isInteger(pathArray[i + 1])) { - object[p] = []; - } else { - object[p] = {}; - } + object[p] = Number.isInteger(pathArray[i + 1]) ? [] : {}; } if (i === pathArray.length - 1) { diff --git a/test.js b/test.js index 2adaa1e..3b65641 100644 --- a/test.js +++ b/test.js @@ -64,9 +64,7 @@ test('get', t => { 'foo[5': true }, 'foo[5')); t.true(dotProp.get({ - foo: { - bar: true - } + 'foo[bar]': true }, 'foo[bar]')); t.false(dotProp.get({}, 'constructor[0]', false)); t.false(dotProp.get({}, 'foo[constructor]', false)); From 4d50be0a3c97317eec4d71bc9ccf14788060d4aa Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 21 Nov 2021 05:03:57 +1300 Subject: [PATCH 11/12] Refactor Signed-off-by: Richie Bendall --- index.js | 138 +++++++++++++++++++++++++++---------------------------- test.js | 88 +++++++++++++++++++++++------------ 2 files changed, 125 insertions(+), 101 deletions(-) diff --git a/index.js b/index.js index 5ec8973..ea01546 100644 --- a/index.js +++ b/index.js @@ -9,139 +9,135 @@ const disallowedKeys = new Set([ function getPathSegments(path) { const parts = []; - - let isIgnoring = false; - let isPath = true; + let currentSegment = ''; + let currentIndex = ''; let isIndex = false; - let currentPathSegment = ''; + let isIgnoring = false; - for (const character of path) { + for (const [index, character] of Object.entries(path)) { switch (character) { case '\\': - if (isIgnoring) { - isIgnoring = false; - currentPathSegment += '\\'; + if (isIndex) { + isIndex = false; + currentSegment += `[${currentIndex}`; + currentIndex = ''; + } else if (isIgnoring) { + // If `\\` was escaped + currentSegment += '\\'; } isIgnoring = !isIgnoring; + break; case '.': if (isIgnoring) { + // If `.` was escaped isIgnoring = false; - currentPathSegment += '.'; + currentSegment += character; break; } if (isIndex) { + currentSegment += `[${currentIndex}`; + currentIndex = ''; isIndex = false; - parts.push(`${parts.pop() || ''}[${currentPathSegment}`); - currentPathSegment = ''; } - if (isPath && currentPathSegment.length > 0) { - if (disallowedKeys.has(currentPathSegment)) { - return []; - } + if (path[index - 1] === ']' && typeof parts[parts.length - 1] === 'number') { + // If the dot immediately proceeds an index, skip saving the empty string + break; + } - parts.push(currentPathSegment); - currentPathSegment = ''; + if (disallowedKeys.has(currentSegment)) { + return []; } - isPath = true; + parts.push(currentSegment); + currentSegment = ''; + break; case '[': if (isIgnoring) { + // If `[` was escaped isIgnoring = false; - currentPathSegment += '['; + currentSegment += character; break; } - if (isPath) { - if (currentPathSegment !== '' || parts.length === 0) { - isPath = false; - isIndex = true; - - if (currentPathSegment.length > 0) { - if (disallowedKeys.has(currentPathSegment)) { - return []; - } - - parts.push(currentPathSegment); - currentPathSegment = ''; - } - } else { - currentPathSegment += '['; - } - + if (path[index - 1] === '.') { + currentSegment += character; break; } - if (isIndex) { - isPath = true; - currentPathSegment = `${parts.pop() || ''}[${currentPathSegment}[`; + if (!isIndex) { + isIndex = true; + break; } - isIndex = !isIndex; + isIndex = false; + currentSegment += `[${currentIndex}[`; + currentIndex = ''; break; case ']': - if (isIgnoring && isIndex) { - isIgnoring = false; - isIndex = false; - isPath = true; - currentPathSegment = `${parts.pop()}[${currentPathSegment}]`; - break; - } - if (isIndex) { - isIndex = false; - isPath = true; - const index = Number(currentPathSegment); - - if (Number.isInteger(index)) { - parts.push(index); - } else { - currentPathSegment = `${parts.pop() || ''}[${currentPathSegment}]`; + const index = Number.parseFloat(currentIndex); + if (Number.isInteger(index) && index >= 0) { + if (currentSegment) { + if (disallowedKeys.has(currentSegment)) { + return []; + } - if (disallowedKeys.has(currentPathSegment)) { - return []; + parts.push(currentSegment); + currentSegment = ''; } - parts.push(currentPathSegment); + parts.push(index); + } else { + currentSegment += `[${currentIndex}]`; } - currentPathSegment = ''; + currentIndex = ''; + isIndex = false; + break; + } else if (isIgnoring) { + currentSegment += ']'; + isIgnoring = false; break; } // Falls through default: + if (isIndex) { + currentIndex += character; + break; + } + if (isIgnoring) { + // If no character was escaped isIgnoring = false; - currentPathSegment += '\\'; + currentSegment += '\\'; } - currentPathSegment += character; + currentSegment += character; } } - if (isIndex) { - currentPathSegment = `${parts.pop()}[${currentPathSegment}`; - } - - if (isIgnoring) { - currentPathSegment += '\\'; + if (currentIndex) { + currentSegment += `[${currentIndex}`; + } else if (isIgnoring) { + currentSegment += '\\'; } - if (currentPathSegment.length > 0 || parts.length === 0) { - if (disallowedKeys.has(currentPathSegment)) { + if (currentSegment.length > 0 || parts.length === 0) { + if (disallowedKeys.has(currentSegment)) { return []; } - parts.push(currentPathSegment); + parts.push(currentSegment); } return parts; diff --git a/test.js b/test.js index 3b65641..b78c394 100644 --- a/test.js +++ b/test.js @@ -19,14 +19,56 @@ test('get', t => { t.is(dotProp.get({foo: {}}, 'foo.fake', 'some value'), 'some value'); t.true(dotProp.get({'\\': true}, '\\')); t.true(dotProp.get({'\\foo': true}, '\\foo')); + t.true(dotProp.get({'\\foo': true}, '\\\\foo')); + t.true(dotProp.get({'foo\\': true}, 'foo\\\\')); t.true(dotProp.get({'bar\\': true}, 'bar\\')); t.true(dotProp.get({'foo\\bar': true}, 'foo\\bar')); - t.true(dotProp.get({'\\.foo': true}, '\\\\.foo')); - t.true(dotProp.get({'bar\\.': true}, 'bar\\\\.')); - t.true(dotProp.get({'foo\\.bar': true}, 'foo\\\\.bar')); + t.true(dotProp.get({'\\': {foo: true}}, '\\\\.foo')); + t.true(dotProp.get({'bar\\.': true}, 'bar\\\\\\.')); + t.true(dotProp.get({'foo\\': { + bar: true + }}, 'foo\\\\.bar')); t.is(dotProp.get({foo: 1}, 'foo.bar'), undefined); + t.true(dotProp.get({'foo\\': true}, 'foo\\')); + const fixture2 = {}; + Object.defineProperty(fixture2, 'foo', { + value: 'bar', + enumerable: false + }); + t.is(dotProp.get(fixture2, 'foo'), 'bar'); + t.is(dotProp.get({}, 'hasOwnProperty'), Object.prototype.hasOwnProperty); + + function fn() {} + fn.foo = {bar: 1}; + t.is(dotProp.get(fn), fn); + t.is(dotProp.get(fn, 'foo'), fn.foo); + t.is(dotProp.get(fn, 'foo.bar'), 1); + + const f3 = {foo: null}; + t.is(dotProp.get(f3, 'foo.bar'), undefined); + t.is(dotProp.get(f3, 'foo.bar', 'some value'), 'some value'); + + t.true(dotProp.get({'foo.baz': {bar: true}}, 'foo\\.baz.bar')); + t.true(dotProp.get({'fo.ob.az': {bar: true}}, 'fo\\.ob\\.az.bar')); + + t.false(dotProp.get(null, 'foo.bar', false)); + t.false(dotProp.get('foo', 'foo.bar', false)); + t.false(dotProp.get([], 'foo.bar', false)); + t.false(dotProp.get(undefined, 'foo.bar', false)); + + class F4Class {} + F4Class.prototype.foo = 1; + const f4 = new F4Class(); + t.is(dotProp.get(f4, 'foo'), 1); // #46 + + t.true(dotProp.get({'': {'': true}}, '..')); + t.true(dotProp.get({'': true}, '.')); +}); + +test('get with array indexes', t => { t.true(dotProp.get([true, false, false], '[0]')); + t.true(dotProp.get([[false, true, false], false, false], '[0][1]')); t.true(dotProp.get([{foo: [true]}], '[0].foo[0]')); t.true(dotProp.get({foo: [0, {bar: true}]}, 'foo[1].bar')); @@ -75,36 +117,22 @@ test('get', t => { t.false(dotProp.get([true], '0', false)); - const fixture2 = {}; - Object.defineProperty(fixture2, 'foo', { - value: 'bar', - enumerable: false - }); - t.is(dotProp.get(fixture2, 'foo'), 'bar'); - t.is(dotProp.get({}, 'hasOwnProperty'), Object.prototype.hasOwnProperty); - - function fn() {} - fn.foo = {bar: 1}; - t.is(dotProp.get(fn), fn); - t.is(dotProp.get(fn, 'foo'), fn.foo); - t.is(dotProp.get(fn, 'foo.bar'), 1); - - const f3 = {foo: null}; - t.is(dotProp.get(f3, 'foo.bar'), undefined); - t.is(dotProp.get(f3, 'foo.bar', 'some value'), 'some value'); + t.false(dotProp.get({foo: [true]}, 'foo.0', false)); + t.true(dotProp.get({foo: { + 0: true + }}, 'foo.0')); - t.true(dotProp.get({'foo.baz': {bar: true}}, 'foo\\.baz.bar')); - t.true(dotProp.get({'fo.ob.az': {bar: true}}, 'fo\\.ob\\.az.bar')); + t.true(dotProp.get([{ + '[1]': true + }, false, false], '[0].[1]')); - t.false(dotProp.get(null, 'foo.bar', false)); - t.false(dotProp.get('foo', 'foo.bar', false)); - t.false(dotProp.get([], 'foo.bar', false)); - t.false(dotProp.get(undefined, 'foo.bar', false)); + t.true(dotProp.get({foo: {'[0]': true}}, 'foo.\\[0]')); + t.true(dotProp.get({foo: {'[0]': true}}, 'foo.[0\\]')); + t.true(dotProp.get({foo: {'\\': [true]}}, 'foo.\\\\[0]')); + t.true(dotProp.get({foo: {'[0]': true}}, 'foo.[0\\]')); - class F4Class {} - F4Class.prototype.foo = 1; - const f4 = new F4Class(); - t.is(dotProp.get(f4, 'foo'), 1); // #46 + t.true(dotProp.get({'foo[0': {'9]': true}}, 'foo[0.9]')); + t.true(dotProp.get({'foo[-1]': true}, 'foo[-1]')); }); test('set', t => { From e93ae0be04886375d92611f05a288acef35feb06 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Thu, 20 Jan 2022 04:38:08 +1300 Subject: [PATCH 12/12] Refactor Signed-off-by: Richie Bendall --- index.js | 152 ++++++++++++++++++++++++++++++------------------------- test.js | 92 +++++++++++++++++++++++---------- 2 files changed, 149 insertions(+), 95 deletions(-) diff --git a/index.js b/index.js index ea01546..307430e 100644 --- a/index.js +++ b/index.js @@ -7,45 +7,46 @@ const disallowedKeys = new Set([ 'constructor' ]); +const digits = new Set('0123456789'); + function getPathSegments(path) { const parts = []; let currentSegment = ''; - let currentIndex = ''; - let isIndex = false; + let currentPart = 'start'; let isIgnoring = false; - for (const [index, character] of Object.entries(path)) { + for (const character of path) { switch (character) { case '\\': - if (isIndex) { - isIndex = false; - currentSegment += `[${currentIndex}`; - currentIndex = ''; - } else if (isIgnoring) { - // If `\\` was escaped - currentSegment += '\\'; + if (currentPart === 'index') { + throw new Error('Invalid character in an index'); } - isIgnoring = !isIgnoring; + if (currentPart === 'indexEnd') { + throw new Error('Invalid character after an index'); + } + + if (isIgnoring) { + currentSegment += character; + } + currentPart = 'property'; + isIgnoring = !isIgnoring; break; case '.': - if (isIgnoring) { - // If `.` was escaped - isIgnoring = false; - currentSegment += character; - break; + if (currentPart === 'index') { + throw new Error('Invalid character in an index'); } - if (isIndex) { - currentSegment += `[${currentIndex}`; - currentIndex = ''; - isIndex = false; + if (currentPart === 'indexEnd') { + currentPart = 'property'; + break; } - if (path[index - 1] === ']' && typeof parts[parts.length - 1] === 'number') { - // If the dot immediately proceeds an index, skip saving the empty string + if (isIgnoring) { + isIgnoring = false; + currentSegment += character; break; } @@ -55,69 +56,65 @@ function getPathSegments(path) { parts.push(currentSegment); currentSegment = ''; - + currentPart = 'property'; break; case '[': - if (isIgnoring) { - // If `[` was escaped - isIgnoring = false; - currentSegment += character; + if (currentPart === 'index') { + throw new Error('Invalid character in an index'); + } + + if (currentPart === 'indexEnd') { + currentPart = 'index'; break; } - if (path[index - 1] === '.') { + if (isIgnoring) { + isIgnoring = false; currentSegment += character; break; } - if (!isIndex) { - isIndex = true; - break; + if (currentPart === 'property') { + if (disallowedKeys.has(currentSegment)) { + return []; + } + + parts.push(currentSegment); + currentSegment = ''; } - isIndex = false; - currentSegment += `[${currentIndex}[`; - currentIndex = ''; + currentPart = 'index'; break; case ']': - if (isIndex) { - const index = Number.parseFloat(currentIndex); - if (Number.isInteger(index) && index >= 0) { - if (currentSegment) { - if (disallowedKeys.has(currentSegment)) { - return []; - } - - parts.push(currentSegment); - currentSegment = ''; - } - - parts.push(index); - } else { - currentSegment += `[${currentIndex}]`; - } - - currentIndex = ''; - isIndex = false; - break; - } else if (isIgnoring) { - currentSegment += ']'; - isIgnoring = false; + if (currentPart === 'index') { + parts.push(Number.parseInt(currentSegment, 10)); + currentSegment = ''; + currentPart = 'indexEnd'; break; } + if (currentPart === 'indexEnd') { + throw new Error('Invalid character after an index'); + } + // Falls through default: - if (isIndex) { - currentIndex += character; - break; + if (currentPart === 'index' && !digits.has(character)) { + throw new Error('Invalid character in an index'); + } + + if (currentPart === 'indexEnd') { + throw new Error('Invalid character after an index'); + } + + if (currentPart === 'start') { + currentPart = 'property'; } if (isIgnoring) { - // If no character was escaped isIgnoring = false; currentSegment += '\\'; } @@ -126,23 +123,40 @@ function getPathSegments(path) { } } - if (currentIndex) { - currentSegment += `[${currentIndex}`; - } else if (isIgnoring) { + if (isIgnoring) { currentSegment += '\\'; } - if (currentSegment.length > 0 || parts.length === 0) { + if (currentPart === 'property') { if (disallowedKeys.has(currentSegment)) { return []; } parts.push(currentSegment); + } else if (currentPart === 'index') { + throw new Error('Index was not closed'); + } else if (currentPart === 'start') { + parts.push(''); } return parts; } +function isStringIndex(object, key) { + if (typeof key !== 'number' && Array.isArray(object)) { + const index = Number.parseInt(key, 10); + return Number.isInteger(index) && object[index] === object[key]; + } + + return false; +} + +function assertNotStringIndex(object, key) { + if (isStringIndex(object, key)) { + throw new Error('Cannot use string index'); + } +} + module.exports = { get(object, path, value) { if (!isObject(object) || typeof path !== 'string') { @@ -156,10 +170,8 @@ module.exports = { for (let i = 0; i < pathArray.length; i++) { const key = pathArray[i]; - const index = Number.parseInt(key, 10); - // Disallow string indexes - if (!Number.isInteger(key) && Array.isArray(object) && !Number.isNaN(index) && object[index] === object[key]) { + if (isStringIndex(object, key)) { object = i === pathArray.length - 1 ? undefined : null; } else { object = object[key]; @@ -192,6 +204,7 @@ module.exports = { for (let i = 0; i < pathArray.length; i++) { const p = pathArray[i]; + assertNotStringIndex(object, p); if (!isObject(object[p])) { object[p] = Number.isInteger(pathArray[i + 1]) ? [] : {}; @@ -216,6 +229,7 @@ module.exports = { for (let i = 0; i < pathArray.length; i++) { const p = pathArray[i]; + assertNotStringIndex(object, p); if (i === pathArray.length - 1) { delete object[p]; @@ -243,7 +257,7 @@ module.exports = { // eslint-disable-next-line unicorn/no-for-loop for (let i = 0; i < pathArray.length; i++) { if (isObject(object)) { - if (!(pathArray[i] in object)) { + if (!(pathArray[i] in object && !isStringIndex(object, pathArray[i]))) { return false; } diff --git a/test.js b/test.js index b78c394..4bb6fed 100644 --- a/test.js +++ b/test.js @@ -62,8 +62,8 @@ test('get', t => { const f4 = new F4Class(); t.is(dotProp.get(f4, 'foo'), 1); // #46 - t.true(dotProp.get({'': {'': true}}, '..')); - t.true(dotProp.get({'': true}, '.')); + t.true(dotProp.get({'': {'': {'': true}}}, '..')); + t.true(dotProp.get({'': {'': true}}, '.')); }); test('get with array indexes', t => { @@ -83,33 +83,50 @@ test('get with array indexes', t => { bar: { '[0]': true } - }, 'bar.[0]')); + }, 'bar.\\[0]')); t.true(dotProp.get({ + bar: { + '': [true] + } + }, 'bar.[0]')); + t.throws(() => dotProp.get({ 'foo[5[': true - }, 'foo[5[')); - t.true(dotProp.get({ + }, 'foo[5['), { + message: 'Invalid character in an index' + }); + t.throws(() => dotProp.get({ 'foo[5': { bar: true } - }, 'foo[5.bar')); + }, 'foo[5.bar'), { + message: 'Invalid character in an index' + }); t.true(dotProp.get({ 'foo[5]': { bar: true } }, 'foo\\[5].bar')); - t.true(dotProp.get({ - 'foo[5]': { + t.throws(() => dotProp.get({ + 'foo[5\\]': { bar: true } - }, 'foo[5\\].bar')); - t.true(dotProp.get({ + }, 'foo[5\\].bar'), { + message: 'Invalid character in an index' + }); + t.throws(() => dotProp.get({ 'foo[5': true - }, 'foo[5')); - t.true(dotProp.get({ + }, 'foo[5'), { + message: 'Index was not closed' + }); + t.throws(() => dotProp.get({ 'foo[bar]': true - }, 'foo[bar]')); + }, 'foo[bar]'), { + message: 'Invalid character in an index' + }); t.false(dotProp.get({}, 'constructor[0]', false)); - t.false(dotProp.get({}, 'foo[constructor]', false)); + t.throws(() => dotProp.get({}, 'foo[constructor]', false), { + message: 'Invalid character in an index' + }); t.false(dotProp.get([], 'foo[0].bar', false)); t.true(dotProp.get({foo: [{bar: true}]}, 'foo[0].bar')); @@ -124,15 +141,23 @@ test('get with array indexes', t => { t.true(dotProp.get([{ '[1]': true - }, false, false], '[0].[1]')); + }, false, false], '[0].\\[1]')); t.true(dotProp.get({foo: {'[0]': true}}, 'foo.\\[0]')); - t.true(dotProp.get({foo: {'[0]': true}}, 'foo.[0\\]')); + t.throws(() => dotProp.get({foo: {'[0]': true}}, 'foo.[0\\]'), { + message: 'Invalid character in an index' + }); t.true(dotProp.get({foo: {'\\': [true]}}, 'foo.\\\\[0]')); - t.true(dotProp.get({foo: {'[0]': true}}, 'foo.[0\\]')); + t.throws(() => dotProp.get({foo: {'[0]': true}}, 'foo.[0\\]'), { + message: 'Invalid character in an index' + }); - t.true(dotProp.get({'foo[0': {'9]': true}}, 'foo[0.9]')); - t.true(dotProp.get({'foo[-1]': true}, 'foo[-1]')); + t.throws(() => dotProp.get({'foo[0': {'9]': true}}, 'foo[0.9]'), { + message: 'Invalid character in an index' + }); + t.throws(() => dotProp.get({'foo[-1]': true}, 'foo[-1]'), { + message: 'Invalid character in an index' + }); }); test('set', t => { @@ -197,11 +222,19 @@ test('set', t => { const fixture5 = []; - dotProp.set(fixture5, '1', true); - t.true(fixture5[1]); + dotProp.set(fixture5, '[1]', true); + t.is(fixture5[1], true); + + dotProp.set(fixture5, '[0].foo[0]', true); + t.is(fixture5[0].foo[0], true); + + t.throws(() => dotProp.set(fixture5, '1', true), { + message: 'Cannot use string index' + }); - dotProp.set(fixture5, '0.foo.0', true); - t.true(fixture5[0].foo[0]); + t.throws(() => dotProp.set(fixture5, '0.foo.0', true), { + message: 'Cannot use string index' + }); const fixture6 = {}; @@ -282,7 +315,10 @@ test('delete', t => { } }]; - t.true(dotProp.delete(fixture4, '0.top.dog')); + t.throws(() => dotProp.delete(fixture4, '0.top.dog'), { + message: 'Cannot use string index' + }); + t.true(dotProp.delete(fixture4, '[0].top.dog')); t.deepEqual(fixture4, [{top: {}}]); const fixture5 = { @@ -305,7 +341,6 @@ test('delete', t => { const fixture6 = {}; dotProp.set(fixture6, 'foo.bar.0', 'fizz'); - t.is(fixture6.foo.bar[0], 'fizz'); }); test('has', t => { @@ -332,7 +367,7 @@ test('has', t => { t.true(dotProp.has({'fo.ob.az': {bar: true}}, 'fo\\.ob\\.az.bar')); t.false(dotProp.has(undefined, 'fo\\.ob\\.az.bar')); - t.true(dotProp.has({ + t.false(dotProp.has({ foo: [{bar: ['bar', 'bizz']}] }, 'foo[0].bar.1')); t.false(dotProp.has({ @@ -341,6 +376,11 @@ test('has', t => { t.false(dotProp.has({ foo: [{bar: ['bar', 'bizz']}] }, 'foo[1].bar.1')); + t.true(dotProp.has({ + foo: [{bar: { + 1: 'bar' + }}] + }, 'foo[0].bar.1')); }); test('prevent setting/getting `__proto__`', t => {