Skip to content

Commit

Permalink
add function to check if an object is serializable
Browse files Browse the repository at this point in the history
  • Loading branch information
bmish committed Jan 17, 2024
1 parent a784f4d commit b400d1e
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 25 deletions.
8 changes: 6 additions & 2 deletions lib/rule-tester/rule-tester.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const ajv = require("../shared/ajv")({ strictDefaults: true });
const parserSymbol = Symbol.for("eslint.RuleTester.parser");
const { SourceCode } = require("../source-code");
const { ConfigArraySymbol } = require("@humanwhocodes/config-array");
const { isSerializable } = require("../shared/serialization");

//------------------------------------------------------------------------------
// Typedefs
Expand Down Expand Up @@ -806,9 +807,12 @@ class RuleTester {
* @private
*/
function checkDuplicateTestCase(item, seenTestCases) {
if ((Array.isArray(item.options) && item.options.some(i => typeof i === "object")) || item.settings || item.languageOptions?.parser || item.languageOptions?.parserOptions || item.plugins) {
if (!isSerializable(item)) {

// Don't check for duplicates if the test case has any properties that could be non-serializable.
/*
* If we can't serialize a test case (because it contains a function, RegExp, etc), skip the check.
* This might happen with properties like: options, plugins, settings, languageOptions.parser, languageOptions.parserOptions.
*/
return;
}

Expand Down
48 changes: 48 additions & 0 deletions lib/shared/serialization.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* @fileoverview Object serialization utils.
* @author Bryan Mishkin
*/

"use strict";

const isPlainObject = require("lodash.isplainobject");

/**
* Check if a value is a primitive or plain object created by the Object constructor.
* @param {any} val the value to check
* @returns {boolean} true if so
* @private
*/
function isPrimitiveOrPlainObject(val) {
return (val === null || typeof val === "string" || typeof val === "boolean" || typeof val === "number" || Array.isArray(val) || isPlainObject(val));
}

/**
* Check if an object is serializable.
* Functions or objects like RegExp cannot be serialized by JSON.stringify().
* Inspired by: https://stackoverflow.com/questions/30579940/reliable-way-to-check-if-objects-is-serializable-in-javascript
* @param {any} obj the object
* @returns {boolean} true if the object is serializable
*/
function isSerializable(obj) {
if (!isPrimitiveOrPlainObject(obj)) {
return false;
}
for (const property in obj) {
if (Object.hasOwn(obj, property)) {
if (!isPrimitiveOrPlainObject(obj[property])) {
return false;
}
if (typeof obj[property] === "object") {
if (!isSerializable(obj[property])) {
return false;
}
}
}
}
return true;
}

module.exports = {
isSerializable
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"is-path-inside": "^3.0.3",
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
"lodash.isplainobject": "^4.0.6",
"lodash.merge": "^4.6.2",
"minimatch": "^3.1.2",
"natural-compare": "^1.4.0",
Expand Down
48 changes: 25 additions & 23 deletions tests/lib/rule-tester/rule-tester.js
Original file line number Diff line number Diff line change
Expand Up @@ -2904,6 +2904,27 @@ describe("RuleTester", () => {
}, "detected duplicate test case");
});

it("throws with duplicate object test cases when options is a nested serializable object", () => {
assert.throws(() => {
ruleTester.run("foo", {
meta: { schema: false },
create(context) {
return {
VariableDeclaration(node) {
context.report(node, "foo bar");
}
};
}
}, {
valid: ["foo"],
invalid: [
{ code: "const x = 123;", errors: [{ message: "foo bar" }], options: [{ foo: [{ a: true, b: [1, 2, 3] }] }] },
{ code: "const x = 123;", errors: [{ message: "foo bar" }], options: [{ foo: [{ a: true, b: [1, 2, 3] }] }] }
]
});
}, "detected duplicate test case");
});

it("throws with duplicate object test cases even when property order differs", () => {
assert.throws(() => {
ruleTester.run("foo", {
Expand All @@ -2925,26 +2946,7 @@ describe("RuleTester", () => {
}, "detected duplicate test case");
});

it("ignores duplicate test case when potentially non-serializable property (e.g. settings) present", () => {
ruleTester.run("foo", {
meta: {},
create(context) {
return {
VariableDeclaration(node) {
context.report(node, "foo bar");
}
};
}
}, {
valid: ["foo"],
invalid: [
{ code: "const x = 123;", errors: [{ message: "foo bar" }], settings: {} },
{ code: "const x = 123;", errors: [{ message: "foo bar" }], settings: {} }
]
});
});

it("ignores duplicate test case when potentially non-serializable property present (e.g. settings)", () => {
it("ignores duplicate test case when non-serializable property present (settings)", () => {
ruleTester.run("foo", {
meta: {},
create(context) {
Expand All @@ -2963,7 +2965,7 @@ describe("RuleTester", () => {
});
});

it("ignores duplicate test case when potentially non-serializable property present (languageOptions.parserOptions)", () => {
it("ignores duplicate test case when non-serializable property present (languageOptions.parserOptions)", () => {
ruleTester.run("foo", {
meta: {},
create(context) {
Expand All @@ -2982,7 +2984,7 @@ describe("RuleTester", () => {
});
});

it("ignores duplicate test case when potentially non-serializable property present (plugins)", () => {
it("ignores duplicate test case when non-serializable property present (plugins)", () => {
ruleTester.run("foo", {
meta: {},
create(context) {
Expand All @@ -3001,7 +3003,7 @@ describe("RuleTester", () => {
});
});

it("ignores duplicate test case when potentially non-serializable property present (options as an object)", () => {
it("ignores duplicate test case when non-serializable property present (options)", () => {
ruleTester.run("foo", {
meta: { schema: false },
create(context) {
Expand Down
84 changes: 84 additions & 0 deletions tests/lib/shared/serialization.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* @fileoverview Tests for object serialization utils.
* @author Bryan Mishkin
*/

"use strict";

const assert = require("chai").assert;
const { isSerializable } = require("../../../lib/shared/serialization");

describe("serialization", () => {
describe("isSerializable", () => {
it("string", () => {
assert.isTrue(isSerializable(""));
assert.isTrue(isSerializable("abc"));
});

it("boolean", () => {
assert.isTrue(isSerializable(true));
assert.isTrue(isSerializable(false));
});

it("number", () => {
assert.isTrue(isSerializable(123));
});

it("function", () => {
assert.isFalse(isSerializable(() => {}));
});

it("RegExp", () => {
assert.isFalse(isSerializable(/abc/u));
});

it("null", () => {
assert.isTrue(isSerializable(null));
});

it("undefined", () => {
assert.isFalse(isSerializable(void 0));
});

describe("object", () => {
it("plain objects", () => {
assert.isTrue(isSerializable({}));
assert.isTrue(isSerializable({ a: 123 }));
assert.isTrue(isSerializable({ a: { b: 456 } }));
});

it("object with function", () => {
assert.isFalse(isSerializable({ a() {} }));
assert.isFalse(isSerializable({ a: { b() {} } }));
});

it("object with RegExp", () => {
assert.isFalse(isSerializable({ a: /abc/u }));
assert.isFalse(isSerializable({ a: { b: /abc/u } }));
});
});

describe("array", () => {
it("plain array", () => {
assert.isTrue(isSerializable([]));
assert.isTrue(isSerializable([1, 2, 3]));
});

it("array with function", () => {
assert.isFalse(isSerializable([function() {}]));
});

it("array with RegExp", () => {
assert.isFalse(isSerializable([/abc/u]));
});

it("array with plain/nested objects", () => {
assert.isTrue(isSerializable([{ a: 1 }, { b: 2 }, { c: { nested: true } }]));
});

it("array with object with nested function", () => {
assert.isFalse(isSerializable([{ a: { fn() {} } }]));
});
});
});
});

0 comments on commit b400d1e

Please sign in to comment.