Skip to content

Commit

Permalink
Fix circular false-positive
Browse files Browse the repository at this point in the history
Fixes #2
  • Loading branch information
sindresorhus committed Mar 19, 2023
1 parent 2765d31 commit ca3945d
Show file tree
Hide file tree
Showing 6 changed files with 334 additions and 28 deletions.
25 changes: 17 additions & 8 deletions index.js
@@ -1,19 +1,28 @@
const makeCircularReplacer = () => {
const seen = new WeakMap();

return (key, value) => {
function safeStringifyReplacer(seen) {
return function (key, value) {
if (value !== null && typeof value === 'object') {
if (seen.has(value) && seen.get(value) !== key) {
if (seen.has(value)) {
return '[Circular]';
}

seen.set(value, key);
seen.add(value);

const newValue = Array.isArray(value) ? [] : {};

for (const [key2, value2] of Object.entries(value)) {
newValue[key2] = safeStringifyReplacer(seen)(key2, value2);
}

seen.delete(value);

return newValue;
}

return value;
};
};
}

export default function safeStringify(object, {indentation} = {}) {
return JSON.stringify(object, makeCircularReplacer(), indentation);
const seen = new WeakSet();
return JSON.stringify(object, safeStringifyReplacer(seen), indentation);
}
6 changes: 3 additions & 3 deletions package.json
Expand Up @@ -34,8 +34,8 @@
"object"
],
"devDependencies": {
"ava": "^4.1.0",
"tsd": "^0.20.0",
"xo": "^0.48.0"
"ava": "^5.2.0",
"tsd": "^0.28.0",
"xo": "^0.53.1"
}
}
8 changes: 4 additions & 4 deletions readme.md
Expand Up @@ -35,6 +35,8 @@ Returns a string.

#### value

Type: `unknown`

The value to convert to a JSON string.

#### options
Expand All @@ -47,12 +49,10 @@ Type: `'string' | 'number'`

The indentation of the JSON.

By default, the JSON is not indented.

Set it to `'\t'` for tab indentation or the number of spaces you want.
By default, the JSON is not indented. Set it to `'\t'` for tab indentation or the number of spaces you want.

## FAQ

### Why another safe stringify package?

The existing ones either did too much, did it incorrectly, or used inefficient code (not using `WeakMap`). For example, many packages incorrectly replaced all duplicate objects, not just circular references, and did not handle circular arrays.
The existing ones either did too much, did it incorrectly, or used inefficient code (not using `WeakSet`). For example, many packages incorrectly replaced all duplicate objects, not just circular references, and did not handle circular arrays.
164 changes: 154 additions & 10 deletions test.js
Expand Up @@ -32,18 +32,18 @@ test('circular object', t => {
t.snapshot(safeStringify(fixture, options));
});

/// test('circular object 2', t => {
// const fixture2 = {
// c: true,
// };
test('circular object 2', t => {
const fixture2 = {
c: true,
};

// const fixture = {
// a: fixture2,
// b: fixture2,
// };
const fixture = {
a: fixture2,
b: fixture2,
};

// t.snapshot(safeStringify(fixture, options));
// });
t.snapshot(safeStringify(fixture, options));
});

test('circular array', t => {
const fixture = [1];
Expand Down Expand Up @@ -72,3 +72,147 @@ test('multiple circular objects in object', t => {

t.snapshot(safeStringify({x: fixture, y: fixture}, options));
});

test('nested non-circular object', t => {
const fixture = {
a: {
b: {
c: {
d: 1,
},
},
},
};

t.is(safeStringify(fixture, options), JSON.stringify(fixture, undefined, '\t'));
});

test('nested circular object', t => {
const fixture = {
a: {
b: {
c: {},
},
},
};

fixture.a.b.c.d = fixture.a;

t.snapshot(safeStringify(fixture, options));
});

test('complex object with circular and non-circular references', t => {
const shared = {x: 1};
const circular = {y: 2};
circular.self = circular;

const fixture = {
a: shared,
b: {
c: shared,
d: circular,
},
e: circular,
};

t.snapshot(safeStringify(fixture, options));
});

test('object with circular references at different depths', t => {
const fixture = {
a: {
b: {
c: {},
},
},
};

fixture.a.b.c.d = fixture.a;
fixture.a.b.c.e = fixture.a.b;

t.snapshot(safeStringify(fixture, options));
});

test('object with value as a circular reference', t => {
const fixture = {
a: 1,
b: 2,
};

fixture.self = fixture;

t.snapshot(safeStringify(fixture, options));
});

test('empty object', t => {
const fixture = {};

t.is(safeStringify(fixture, options), JSON.stringify(fixture, undefined, '\t'));
});

test('object with null value', t => {
const fixture = {
a: null,
};

t.is(safeStringify(fixture, options), JSON.stringify(fixture, undefined, '\t'));
});

test('object with undefined value', t => {
const fixture = {
a: undefined,
};

t.is(safeStringify(fixture, options), JSON.stringify(fixture, undefined, '\t'));
});

test('circular object with multiple nested circular references', t => {
const fixture = {
a: {
b: {
c: {},
},
},
};

fixture.a.b.c.d = fixture.a;
fixture.a.b.c.e = fixture.a.b;
fixture.a.b.c.f = fixture.a.b.c;

t.snapshot(safeStringify(fixture, options));
});

test('circular array with nested circular arrays', t => {
const fixture = [[1, 2, 3]];

fixture.push(fixture, [fixture, fixture]);

t.snapshot(safeStringify(fixture, options));
});

test('object with circular reference to parent and grandparent', t => {
const fixture = {
a: {
b: {
c: {},
},
},
};

fixture.a.b.c.parent = fixture.a.b;
fixture.a.b.c.grandparent = fixture.a;

t.snapshot(safeStringify(fixture, options));
});

test('array containing objects with the same circular reference', t => {
const circular = {a: 1};
circular.self = circular;

const fixture = [
{b: 2, c: circular},
{d: 3, e: circular},
];

t.snapshot(safeStringify(fixture, options));
});

0 comments on commit ca3945d

Please sign in to comment.