diff --git a/.changeset/shiny-dodos-relax.md b/.changeset/shiny-dodos-relax.md
new file mode 100644
index 00000000..34489b5b
--- /dev/null
+++ b/.changeset/shiny-dodos-relax.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-jsonc": minor
+---
+
+feat: add `jsonc/no-irregular-whitespace` rule
diff --git a/README.md b/README.md
index 039f02c0..aba4dc47 100644
--- a/README.md
+++ b/README.md
@@ -212,6 +212,7 @@ The rules with the following star :star: are included in the config.
| [jsonc/key-spacing](https://ota-meshi.github.io/eslint-plugin-jsonc/rules/key-spacing.html) | enforce consistent spacing between keys and values in object literal properties | :wrench: | | | |
| [jsonc/no-dupe-keys](https://ota-meshi.github.io/eslint-plugin-jsonc/rules/no-dupe-keys.html) | disallow duplicate keys in object literals | | :star: | :star: | :star: |
| [jsonc/no-floating-decimal](https://ota-meshi.github.io/eslint-plugin-jsonc/rules/no-floating-decimal.html) | disallow leading or trailing decimal points in numeric literals | :wrench: | :star: | :star: | |
+| [jsonc/no-irregular-whitespace](https://ota-meshi.github.io/eslint-plugin-jsonc/rules/no-irregular-whitespace.html) | disallow irregular whitespace | | | | |
| [jsonc/no-multi-str](https://ota-meshi.github.io/eslint-plugin-jsonc/rules/no-multi-str.html) | disallow multiline strings | | :star: | :star: | |
| [jsonc/no-octal-escape](https://ota-meshi.github.io/eslint-plugin-jsonc/rules/no-octal-escape.html) | disallow octal escape sequences in string literals | | | | |
| [jsonc/no-octal](https://ota-meshi.github.io/eslint-plugin-jsonc/rules/no-octal.html) | disallow legacy octal literals | | :star: | :star: | :star: |
diff --git a/docs/rules/README.md b/docs/rules/README.md
index ef505209..246f8666 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -50,6 +50,7 @@ The rules with the following star :star: are included in the `plugin:jsonc/recom
| [jsonc/key-spacing](./key-spacing.md) | enforce consistent spacing between keys and values in object literal properties | :wrench: | | | |
| [jsonc/no-dupe-keys](./no-dupe-keys.md) | disallow duplicate keys in object literals | | :star: | :star: | :star: |
| [jsonc/no-floating-decimal](./no-floating-decimal.md) | disallow leading or trailing decimal points in numeric literals | :wrench: | :star: | :star: | |
+| [jsonc/no-irregular-whitespace](./no-irregular-whitespace.md) | disallow irregular whitespace | | | | |
| [jsonc/no-multi-str](./no-multi-str.md) | disallow multiline strings | | :star: | :star: | |
| [jsonc/no-octal-escape](./no-octal-escape.md) | disallow octal escape sequences in string literals | | | | |
| [jsonc/no-octal](./no-octal.md) | disallow legacy octal literals | | :star: | :star: | :star: |
diff --git a/docs/rules/key-spacing.md b/docs/rules/key-spacing.md
index b888bb17..5656515e 100644
--- a/docs/rules/key-spacing.md
+++ b/docs/rules/key-spacing.md
@@ -47,7 +47,7 @@ This rule enforces consistent spacing between keys and values in object literal
}
```
-Same as [key-spacing] rule option. See [here](https://eslint.org/docs/rules/key-spacing#options) for details.
+Same as [key-spacing] rule option. See [here](https://eslint.org/docs/rules/key-spacing#options) for details.
## :couple: Related rules
diff --git a/docs/rules/no-irregular-whitespace.md b/docs/rules/no-irregular-whitespace.md
new file mode 100644
index 00000000..f5a148b8
--- /dev/null
+++ b/docs/rules/no-irregular-whitespace.md
@@ -0,0 +1,76 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "jsonc/no-irregular-whitespace"
+description: "disallow irregular whitespace"
+---
+
+# jsonc/no-irregular-whitespace
+
+> disallow irregular whitespace
+
+- :exclamation: **_This rule has not been released yet._**
+
+## :book: Rule Details
+
+This rule is aimed at catching invalid whitespace that is not a normal tab and space.
+
+
+
+
+
+```json
+/* eslint jsonc/no-irregular-whitespace: 'error' */
+{
+ /* ✓ GOOD */
+ "GOOD": "",
+
+ /* ✗ BAD */
+ "BAD": "foo",
+}
+```
+
+
+
+ESLint core [no-irregular-whitespace] rule don't work well in JSON. Turn off that rule in JSON files and use `jsonc/no-irregular-whitespace` rule.
+
+## :wrench: Options
+
+Nothing.
+
+```json
+{
+ "overrides": [
+ {
+ "files": ["*.json", "*.json5"],
+ "rules": {
+ "no-irregular-whitespace": "off",
+ "jsonc/no-irregular-whitespace": [
+ "error",
+ {
+ "skipStrings": true,
+ "skipComments": false,
+ "skipRegExps": false,
+ "skipTemplates": false
+ }
+ ]
+ }
+ }
+ ]
+}
+```
+
+Same as [no-irregular-whitespace] rule option. See [here](https://eslint.org/docs/rules/no-irregular-whitespace#options) for details.
+
+## :couple: Related rules
+
+- [no-irregular-whitespace]
+
+[no-irregular-whitespace]: https://eslint.org/docs/rules/no-irregular-whitespace
+
+## :mag: Implementation
+
+- [Rule source](/~https://github.com/ota-meshi/eslint-plugin-jsonc/blob/master/lib/rules/no-irregular-whitespace.ts)
+- [Test source](/~https://github.com/ota-meshi/eslint-plugin-jsonc/blob/master/tests/lib/rules/no-irregular-whitespace.ts)
+
+Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/no-irregular-whitespace)
diff --git a/lib/rules/no-irregular-whitespace.ts b/lib/rules/no-irregular-whitespace.ts
new file mode 100644
index 00000000..17d76c99
--- /dev/null
+++ b/lib/rules/no-irregular-whitespace.ts
@@ -0,0 +1,23 @@
+import { createRule, defineWrapperListener, getCoreRule } from "../utils";
+const coreRule = getCoreRule("no-irregular-whitespace");
+
+export default createRule("no-irregular-whitespace", {
+ meta: {
+ docs: {
+ description: "disallow irregular whitespace",
+ // TODO: We will switch this in the next major version.
+ recommended: null,
+ // recommended: ["json", "jsonc", "json5"], // TODO: We need to turn off core `no-irregular-whitespace` rule in shared config.
+ extensionRule: true,
+ layout: false,
+ },
+ fixable: coreRule.meta?.fixable,
+ hasSuggestions: (coreRule.meta as any)?.hasSuggestions,
+ schema: coreRule.meta!.schema!,
+ messages: coreRule.meta!.messages!,
+ type: coreRule.meta!.type!,
+ },
+ create(context) {
+ return defineWrapperListener(coreRule, context, context.options);
+ },
+});
diff --git a/lib/utils/index.ts b/lib/utils/index.ts
index e76f1a24..f533bc32 100644
--- a/lib/utils/index.ts
+++ b/lib/utils/index.ts
@@ -81,7 +81,7 @@ export function defineWrapperListener(
continue;
}
const jsonKey = key.replace(
- /(?:^|\b)(ExpressionStatement|ArrayExpression|ObjectExpression|Property|Identifier|Literal|UnaryExpression)(?:\b|$)/gu,
+ /(?:^|\b)(ExpressionStatement|(?:Template)?Literal|(?:Array|Object|Unary)Expression|Property|Identifier|TemplateElement)(?:\b|$)/gu,
"JSON$1"
);
jsonListener[jsonKey] = function (node: AST.JSONNode, ...args) {
diff --git a/lib/utils/rules.ts b/lib/utils/rules.ts
index c3b4800a..47518229 100644
--- a/lib/utils/rules.ts
+++ b/lib/utils/rules.ts
@@ -17,6 +17,7 @@ import noEscapeSequenceInIdentifier from "../rules/no-escape-sequence-in-identif
import noFloatingDecimal from "../rules/no-floating-decimal";
import noHexadecimalNumericLiterals from "../rules/no-hexadecimal-numeric-literals";
import noInfinity from "../rules/no-infinity";
+import noIrregularWhitespace from "../rules/no-irregular-whitespace";
import noMultiStr from "../rules/no-multi-str";
import noNan from "../rules/no-nan";
import noNumberProps from "../rules/no-number-props";
@@ -62,6 +63,7 @@ export const rules = [
noFloatingDecimal,
noHexadecimalNumericLiterals,
noInfinity,
+ noIrregularWhitespace,
noMultiStr,
noNan,
noNumberProps,
diff --git a/tests/lib/rules/no-irregular-whitespace.ts b/tests/lib/rules/no-irregular-whitespace.ts
new file mode 100644
index 00000000..a29ee4fb
--- /dev/null
+++ b/tests/lib/rules/no-irregular-whitespace.ts
@@ -0,0 +1,233 @@
+import { RuleTester } from "eslint";
+import rule from "../../../lib/rules/no-irregular-whitespace";
+
+const tester = new RuleTester({
+ parser: require.resolve("jsonc-eslint-parser"),
+ parserOptions: {
+ ecmaVersion: 2020,
+ },
+});
+
+tester.run("no-irregular-whitespace", rule as any, {
+ valid: [
+ `"\u0020"`,
+ `"\u0009"`,
+ `"\\u000B"`,
+ `["\u0009"]`,
+ `["\\u000B"]`,
+ `{"\u0009": "\u0009"}`,
+ `{"\\u000B": "\\u000B"}`,
+
+ // String
+ `"\u000B"`,
+ `["\u000B"]`,
+ `{"\u000B": "\u000B"}`,
+ {
+ code: `"\u000B"`,
+ options: [{ skipStrings: true }],
+ },
+ {
+ code: `["\u000B"]`,
+ options: [{ skipStrings: true }],
+ },
+ {
+ code: `{"\u000B": "\u000B"}`,
+ options: [{ skipStrings: true }],
+ },
+ // Templates
+ {
+ code: `\`\u000B\``,
+ options: [{ skipTemplates: true }],
+ },
+ {
+ code: `[\`\u000B\`]`,
+ options: [{ skipTemplates: true }],
+ },
+ {
+ code: `{"\u000B": \`\u000B\`}`,
+ options: [{ skipTemplates: true }],
+ },
+ // RegExps
+ {
+ code: `/\u000B/`,
+ options: [{ skipRegExps: true }],
+ },
+ {
+ code: `[/\u000B/]`,
+ options: [{ skipRegExps: true }],
+ },
+ {
+ code: `{"\u000B": /\u000B/}`,
+ options: [{ skipRegExps: true }],
+ },
+ // Comments
+ {
+ code: `{} // \u000B`,
+ options: [{ skipComments: true }],
+ },
+ ],
+ invalid: [
+ {
+ code: `{"\u000B": [\`\u000B\`, /\u000B/]} \u000B // \u000B`,
+ errors: [
+ {
+ message: "Irregular whitespace not allowed.",
+ line: 1,
+ column: 9,
+ },
+ {
+ message: "Irregular whitespace not allowed.",
+ line: 1,
+ column: 14,
+ },
+ {
+ message: "Irregular whitespace not allowed.",
+ line: 1,
+ column: 19,
+ },
+ {
+ message: "Irregular whitespace not allowed.",
+ line: 1,
+ column: 24,
+ },
+ ],
+ },
+ {
+ code: `{"\u000B": [\`\u000B\`, /\u000B/]} \u000B // \u000B`,
+ options: [
+ {
+ skipStrings: true,
+ skipComments: true,
+ skipRegExps: true,
+ skipTemplates: true,
+ },
+ ],
+ errors: [
+ {
+ message: "Irregular whitespace not allowed.",
+ line: 1,
+ column: 19,
+ },
+ ],
+ },
+ // String
+ {
+ code: `{"\u000B": "\u000B"}`,
+ options: [{ skipStrings: false }],
+ errors: [
+ {
+ message: "Irregular whitespace not allowed.",
+ line: 1,
+ column: 3,
+ },
+ {
+ message: "Irregular whitespace not allowed.",
+ line: 1,
+ column: 8,
+ },
+ ],
+ },
+ {
+ code: `["\u000B"]`,
+ options: [{ skipStrings: false }],
+ errors: [
+ {
+ message: "Irregular whitespace not allowed.",
+ line: 1,
+ column: 3,
+ },
+ ],
+ },
+ {
+ code: `"\u000B"`,
+ options: [{ skipStrings: false }],
+ errors: [
+ {
+ message: "Irregular whitespace not allowed.",
+ line: 1,
+ column: 2,
+ },
+ ],
+ },
+ // Templates
+ {
+ code: `\`\u000B\``,
+ options: [{ skipTemplates: false }],
+ errors: [
+ {
+ message: "Irregular whitespace not allowed.",
+ line: 1,
+ column: 2,
+ },
+ ],
+ },
+ {
+ code: `[\`\u000B\`]`,
+ options: [{ skipTemplates: false }],
+ errors: [
+ {
+ message: "Irregular whitespace not allowed.",
+ line: 1,
+ column: 3,
+ },
+ ],
+ },
+ {
+ code: `{"\u000B": \`\u000B\`}`,
+ options: [{ skipTemplates: false }],
+ errors: [
+ {
+ message: "Irregular whitespace not allowed.",
+ line: 1,
+ column: 8,
+ },
+ ],
+ },
+ // RegExps
+ {
+ code: `/\u000B/`,
+ options: [{ skipRegExps: false }],
+ errors: [
+ {
+ message: "Irregular whitespace not allowed.",
+ line: 1,
+ column: 2,
+ },
+ ],
+ },
+ {
+ code: `[/\u000B/]`,
+ options: [{ skipRegExps: false }],
+ errors: [
+ {
+ message: "Irregular whitespace not allowed.",
+ line: 1,
+ column: 3,
+ },
+ ],
+ },
+ {
+ code: `{"\u000B": /\u000B/}`,
+ options: [{ skipRegExps: false }],
+ errors: [
+ {
+ message: "Irregular whitespace not allowed.",
+ line: 1,
+ column: 8,
+ },
+ ],
+ },
+ // Comments
+ {
+ code: `{} // \u000B`,
+ options: [{ skipComments: false }],
+ errors: [
+ {
+ message: "Irregular whitespace not allowed.",
+ line: 1,
+ column: 7,
+ },
+ ],
+ },
+ ],
+});