Repository: eslint/eslint (JavaScript)

Issue:
Config clone before validating kills Object data (7.3.0)
* **ESLint Version: 7.3.0**

The latest update breaks [eslint-plugin-import](https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-cycle.md) and I believe some other eslint plugins due to configuration cloning by `JSON.parse(JSON.stringify(Object))`
related PR: https://github.com/eslint/eslint/pull/13034

Any plugin that use any global property like "Infinity" wont work properly.

`JSON.parse(JSON.stringify(Infinity)) => null`

Configuration example https://github.com/airbnb/javascript/blob/master/packages/eslint-config-airbnb-base/rules/imports.js#L236


Discussion:
oof, yes, JSON cloning is a terrible way to clone objects :-/

JSON cloning should never be used, especially since eslint config values can be regexes, Infinity, NaN, and other things that don't survive JSON serialization.
Thanks for the report. The fact that `Infinity` worked at all is actually a bug because it can only ever work in the JS format of config file and not in JSON or YAML, and we tried to make all the config file formats equivalent. Our intent was for rules to only have JSON-serializable configs for this reason. In fact, if people are using special JS values in their configs and we aren’t catching it, chances are there are other underlying bugs we aren’t aware of because the codebase assumes a config can be completely JSON serialized in a few places.

We did try a few other cloning techniques before deciding on this. If anyone has a suggestion for an alternative, we are happy to consider it. 

We do need to decide if we should formally allow JS special values, though, as this was completely unintentional. 


Lots of plugins take regexes as a config, which yes, only work in a JS config file.

If eslint wants to forbid non-JSON values in a config, then I'd expect it would be a validation error that's not left up to plugin authors, but that'd probably be a pretty disruptive breaking change.

I'm happy to make a PR that uses `extend` or `node.extend` or `lodash.clonedeep` to employ a proper deep copy algorithm, if that's desirable.
@ljharb we tried https://github.com/eslint/eslint/pull/13034/commits/1453116c5e31338d747e0770a6032412538e2ede, maybe this can be helpful. 
sure, that would work, but i'm not sure why it'd be inlined instead of just being a normal dependency? (also, you'd want to use a WeakSet instead of a Set)

edit: ah, i see it's to wrap the "customizer" stuff. sure, that works fine!
@ljharb to help us estimate impact here, do you have some examples of plugin rules that use regexes in configs? In built-in rules like [id-match](https://eslint.org/docs/rules/id-match), the regex is represented as a string so it's still serializable. If we're lucky, those might do the same.
There is also an issue with `Infinity`  that is being converted into `null` as well I guess. 
just to provide an example: https://github.com/benmosher/eslint-plugin-import/blob/v2.21.2/docs/rules/no-cycle.md uses `Infinity` as a possible value, which breaks with the new approach

this is the `Infinity` issue that @anikethsaha is mentioning, which is I think the issue that everyone is facing since it is being used by airbnb eslint config
https://github.com/benmosher/eslint-plugin-import/blob/055389d425caae7219659fb97b6d0b992d2f1aaa/README.md#importcache also allows the string `"∞"` or the literal `Infinity` for "lifetime"; while no-cycle could and should add `"∞"`, this is just one plugin in the ecosystem that might be relying on this.

There's also tons of internal configs in companies that are using functions and regex literals in their JS configs.
I was surprised this use of `Infinity` was being allowed by the rule's JSON schema before such that we only caught it now, so I did some digging.

```
      maxDepth:{
        description: 'maximum dependency depth to traverse',
        type: 'integer',
        minimum: 1,
      },
```

`eslint-plugin-import/no-cycle`'s [`maxDepth` option schema requires an `integer` type](https://github.com/benmosher/eslint-plugin-import/blob/296262842b52a50c5b107ba91bb2d13c05b2a104/src/rules/no-cycle.js#L17-L21).

> This includes cycles of depth 1 (imported module imports me) to Infinity, if the maxDepth option is not set.

> There is a maxDepth option available to prevent full expansion of very deep dependency trees:
> This is not necessarily recommended, but available as a cost/benefit tradeoff mechanism for reducing total project lint time, if needed.

In the [no-cycle docs](https://github.com/benmosher/eslint-plugin-import/blob/296262842b52a50c5b107ba91bb2d13c05b2a104/docs/rules/no-cycle.md), it mentions `Infinity` as the default if `maxDepth` is unset but says that `maxDepth` is only needed in order to limit search depth for performance reasons. (Does this mean that removing [`, { maxDepth: Infinity }`](https://github.com/airbnb/javascript/blob/54955410ee53fd82e6caa5879ccd0c413640706a/packages/eslint-config-airbnb-base/rules/imports.js#L236) would maintain the existing behavior?)

ESLint uses [ajv](https://github.com/ajv-validator/ajv) to validate rules' JSON schemas, and its current version passes `NaN` and `Infinity` for `integer`. [That is expected to change in the next major](https://github.com/ajv-validator/ajv/issues/182#issuecomment-559609135), and until then, there's an unreleased [new `strictNumbers` option](https://github.com/ajv-validator/ajv/pull/1219) to opt-in to the updated behavior.
@btmills aha, thanks, that does explain it. It's been `Infinity` for like 2 years without issue, fwiw.

Yes, removing the explicit default would work, but the airbnb config prefers to never rely on defaults. If this isn't something eslint is going to fix quickly, i could certainly update eslint-plugin-import to also allow `"∞"` as a default, and then update the airbnb config to do so as well, but i'd rather not have to do that under time pressure.
If JSON remains enforced, I think it'd be nice if there could be a recommended serialization process for the common special objects above-mentioned, and maybe even a utility for converting them back (e.g., regexes being expected in a string format like "/(abc.*)/i" and then converted to RegExp objects) (schemas therefor will presumably still need to fall to authors\*). Then users could expect a more uniform experience.

\*FWIW, draft 7 of json-schema at least allows for "regex" as a `format` though I've left a [comment](https://github.com/json-schema-org/json-schema-spec/issues/821#issuecomment-646917003) asking for delimited regex support to allow for flags as in the above example).
@ljharb to possibly help with the time pressure, if you do decide to go for allowing the `"∞"` string, the [schema](https://github.com/benmosher/eslint-plugin-import/blob/296262842b52a50c5b107ba91bb2d13c05b2a104/src/rules/no-cycle.js#L17-L21) may end up looking something like

```
      maxDepth:{
        description: 'maximum dependency depth to traverse',
        type: ['integer', 'string'],
        minimum: 1,
        pattern: "^∞$",
      },
``` 
Hey guys, what's the plan with this? Are we really saying to everyone using the Airbnb preset that they can't upgrade eslint anymore?

Thx
This is breaking even Neutrino, effectively blocking people to start new projects without hiccups. Shouldn't this be a priority?
Eslint is an awesome tool to ensure code style consistency, especially when paired with airbnb's stricter rules. The eslint-config-airbnb-base, with some 3 million weekly downloads, now breaks with 7.3.0 as pointed out. A humble suggestion for the future is to integrate tests that represents how eslint is used by the most popular repos in its ecosystem (of which eslint-config-airbnb-base is one) and not break compatibility until the next major rev
@boeric +1 on your suggestion
Yea, I guess having integration tests with popular configs/plugins can be done to address these changes in the future. 

+1 for that. 

> _also, apologizes for this regression as this was introduced due to my PR._
Any update on this issue?
We are still discussing this. 

It seems like an easy solution is for airbnb to replace Infinity with Number.MAX_SAFE_INTEGER. That should effectively be the same thing but is serializable and doesn’t require updates elsewhere in the ecosystem. 

We dug in and found both —print-config and —cache do not work properly with the airbnb config right now because they both depend on serialization to work. So, I’d like to find a way to keep this change in to prevent any other configs from introducing special values that could also break this functionality. 



Given the rarity of users using print-config or cache, and considering this has been in the airbnb config for two years, it seems likely that there's a significant amount of the ecosystem that is unaware that rule schemas must be JSON-serializable values.

I implore you to reconsider and delay this restriction until the next semver-major, and consider this a missed opportunity from the v7.0.0 release.
@ljharb you've made comments in this thread that are untrue, from regular expressions being common in rule configs and the rarity of people using `--print-config` and `--cache`. I'd ask that you refrain from speculating about what features other users of the ecosystem make use of as it's distracting and may confuse those who aren't as familiar with ESLint. I completely understand that you are frustrated, but let's please stick to the facts.

In an effort to get our users unblocked, I've submitted https://github.com/eslint/eslint/pull/13435 that will replace `Infinity` with `Number.MAX_SAFE_INTEGER` in rule configs during validation (but not in other parts of the codebase that rely on serialization). This will effectively restore the v7.2.0 behavior. That means things like `--cache` and `--print-config` won't work correctly for any config including `Infinity`.

Our position on `eslint-config-airbnb` is that it should be updated to use `Number.MAX_SAFE_INTEGER` as soon as possible. The change we are making is to buy you some time to make that change. Please do so at your earliest convenience.

We will plan on switching to a strict integer check in the next major release to ensure this bug doesn't continue.

@nzakas it's fair that I don't have facts about how many people use these features; nor is it possible to have them, since enterprise usage of things like this are common. Airbnb, for example, has internal rules that use functions, and regexes, in their config, so that's one hard data point.

Thanks for that fix; that will at least fix the airbnb config and revert the breaking change, but it won't fix any users that *may* be relying on non-JSON-serializable config values (a large number of folks requesting rules on eslint-plugin-import, eslint-plugin-react, and eslint-plugin-jsx-a11y all expect and request regex literals to be usable in configs, for another data point).

I'm happy about the strict integer check in v8; hopefully it will also ensure that functions and regexes and Dates and other non-serializeable values are treated as invalid as well.



Patch:
diff --git a/lib/cli-engine/config-array-factory.js b/lib/cli-engine/config-array-factory.js
index 6d0992151ada..39f62bb6da1b 100644
--- a/lib/cli-engine/config-array-factory.js
+++ b/lib/cli-engine/config-array-factory.js
@@ -722,7 +722,12 @@ class ConfigArrayFactory {
          *
          * Refer https://github.com/eslint/eslint/issues/12592
          */
-        const clonedRulesConfig = rules && JSON.parse(JSON.stringify((rules)));
+        const clonedRulesConfig = rules && JSON.parse(
+            JSON.stringify(
+                rules,
+                (key, value) => (value === Infinity ? Number.MAX_SAFE_INTEGER : value)
+            )
+        );
 
         // Flatten `extends`.
         for (const extendName of extendList.filter(Boolean)) {


Test patch:
diff --git a/tests/fixtures/config-file/cloned-config/configWithInfinity.js b/tests/fixtures/config-file/cloned-config/configWithInfinity.js
new file mode 100644
index 000000000000..73e3c93c085f
--- /dev/null
+++ b/tests/fixtures/config-file/cloned-config/configWithInfinity.js
@@ -0,0 +1,6 @@
+module.exports = {
+
+  rules: {
+      "max-len": [ "error", { code: Infinity }]
+  }
+};
diff --git a/tests/lib/cli.js b/tests/lib/cli.js
index afb225f1f7ca..2f84900eefb1 100644
--- a/tests/lib/cli.js
+++ b/tests/lib/cli.js
@@ -1172,9 +1172,7 @@ describe("cli", () => {
 
                 assert.strictEqual(exit, 0);
             });
-        });
 
-        describe("config file and input file", () => {
             it("should exit with 1 as camelcase has wrong property type", async () => {
                 const configPath = getFixturePath("config-file", "cloned-config", "eslintConfigFail.js");
                 const filePath = getFixturePath("config-file", "cloned-config", "index.js");
@@ -1187,6 +1185,16 @@ describe("cli", () => {
                 }
 
             });
+
+            it("should not cause an error when a rule configuration has `Infinity`", async () => {
+                const configPath = getFixturePath("config-file", "cloned-config", "configWithInfinity.js");
+                const filePath = getFixturePath("config-file", "cloned-config", "index.js");
+                const args = `--config ${configPath} ${filePath}`;
+
+                const exit = await cli.execute(args);
+
+                assert.strictEqual(exit, 0);
+            });
         });
 
         describe("inline config and input file", () => {