first commit

This commit is contained in:
2026-08-06 12:00:39 +10:00
commit 91221f5a8e
3259 changed files with 569069 additions and 0 deletions
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+303
View File
@@ -0,0 +1,303 @@
# ESLint JSON Language Plugin
## Overview
This package contains a plugin that allows you to natively lint JSON, JSONC, and JSON5 files using ESLint.
**Important:** This plugin requires ESLint v9.15.0 or higher and you must be using the [new configuration system](https://eslint.org/docs/latest/use/configure/configuration-files).
## Installation
For Node.js and compatible runtimes:
```shell
npm install @eslint/json -D
# or
yarn add @eslint/json -D
# or
pnpm install @eslint/json -D
# or
bun add @eslint/json -D
```
For Deno:
```shell
deno add @eslint/json
```
## Usage
This package exports these languages:
- `"json/json"` is for regular JSON files
- `"json/jsonc"` is for JSON files that support comments ([JSONC](https://github.com/microsoft/node-jsonc-parser)) such as those used for [Visual Studio Code](https://code.visualstudio.com/) configuration files
- `"json/json5"` is for [JSON5](https://json5.org) files
Depending on which types of JSON files you'd like to lint, you can set up your `eslint.config.js` file to include just the files you'd like. Here's an example that lints JSON, JSONC, and JSON5 files:
```js
// eslint.config.js
import { defineConfig } from "eslint/config";
import json from "@eslint/json";
export default defineConfig([
{
plugins: {
json,
},
},
// lint JSON files
{
files: ["**/*.json"],
language: "json/json",
rules: {
"json/no-duplicate-keys": "error",
},
},
// lint JSONC files
{
files: ["**/*.jsonc", ".vscode/*.json"],
language: "json/jsonc",
rules: {
"json/no-duplicate-keys": "error",
},
},
// lint JSON5 files
{
files: ["**/*.json5"],
language: "json/json5",
rules: {
"json/no-duplicate-keys": "error",
},
},
]);
```
In CommonJS format:
```js
// eslint.config.js
const { defineConfig } = require("eslint/config");
const json = require("@eslint/json").default;
module.exports = defineConfig([
{
plugins: {
json,
},
},
// lint JSON files
{
files: ["**/*.json"],
language: "json/json",
rules: {
"json/no-duplicate-keys": "error",
},
},
// lint JSONC files
{
files: ["**/*.jsonc", ".vscode/*.json"],
language: "json/jsonc",
rules: {
"json/no-duplicate-keys": "error",
},
},
// lint JSON5 files
{
files: ["**/*.json5"],
language: "json/json5",
rules: {
"json/no-duplicate-keys": "error",
},
},
]);
```
## Recommended Configuration
To use the recommended configuration for this plugin, specify your matching `files` and then use the `extends: ["json/recommended"]` property, like this:
```js
// eslint.config.js
import { defineConfig } from "eslint/config";
import json from "@eslint/json";
export default defineConfig([
// lint JSON files
{
files: ["**/*.json"],
ignores: ["package-lock.json"],
plugins: { json },
language: "json/json",
extends: ["json/recommended"],
},
// lint JSONC files
{
files: ["**/*.jsonc"],
plugins: { json },
language: "json/jsonc",
extends: ["json/recommended"],
},
// lint JSON5 files
{
files: ["**/*.json5"],
plugins: { json },
language: "json/json5",
extends: ["json/recommended"],
},
]);
```
**Note:** You generally want to ignore `package-lock.json` because it is auto-generated and you typically will not want to manually make changes to it.
## Rules
<!-- NOTE: The following table is autogenerated. Do not manually edit. -->
<!-- Rule Table Start -->
| **Rule Name** | **Description** | **Recommended** |
| :------------------------------------------------------------- | :-------------------------------------------------------- | :-------------: |
| [`no-duplicate-keys`](./docs/rules/no-duplicate-keys.md) | Disallow duplicate keys in JSON objects | yes |
| [`no-empty-keys`](./docs/rules/no-empty-keys.md) | Disallow empty keys in JSON objects | yes |
| [`no-unnormalized-keys`](./docs/rules/no-unnormalized-keys.md) | Disallow JSON keys that are not normalized | yes |
| [`no-unsafe-values`](./docs/rules/no-unsafe-values.md) | Disallow JSON values that are unsafe for interchange | yes |
| [`sort-keys`](./docs/rules/sort-keys.md) | Require JSON object keys to be sorted | no |
| [`top-level-interop`](./docs/rules/top-level-interop.md) | Require the JSON top-level value to be an array or object | no |
<!-- Rule Table End -->
## Configuration Comments
In JSONC and JSON5 files, you can also use [rule configurations comments](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments) and [disable directives](https://eslint.org/docs/latest/use/configure/rules#disabling-rules).
```jsonc
/* eslint json/no-empty-keys: "error" */
{
"foo": {
"": 1, // eslint-disable-line json/no-empty-keys -- We want an empty key here
},
"bar": {
// eslint-disable-next-line json/no-empty-keys -- We want an empty key here too
"": 2,
},
/* eslint-disable json/no-empty-keys -- Empty keys are allowed in the following code as well */
"baz": [
{
"": 3,
},
{
"": 4,
},
],
/* eslint-enable json/no-empty-keys -- re-enable now */
}
```
Both line and block comments can be used for all kinds of configuration comments.
## Allowing trailing commas in JSONC
The Microsoft implementation of JSONC optionally allows for trailing commas in objects and arrays (files like `tsconfig.json` have this option enabled by default in Visual Studio Code). To enable trailing commas in JSONC files, use the `allowTrailingCommas` language option, as in this example:
```js
// eslint.config.js
import { defineConfig } from "eslint/config";
import json from "@eslint/json";
export default defineConfig([
// lint JSONC files
{
files: ["**/*.jsonc"],
plugins: { json },
language: "json/jsonc",
extends: ["json/recommended"],
},
// lint JSONC files and allow trailing commas
{
files: ["**/tsconfig.json", ".vscode/*.json"],
plugins: { json },
language: "json/jsonc",
languageOptions: {
allowTrailingCommas: true,
},
extends: ["json/recommended"],
},
]);
```
**Note:** The `allowTrailingCommas` option is only valid for the `json/jsonc` language.
## Frequently Asked Questions
### How does this relate to `eslint-plugin-json` and `eslint-plugin-jsonc`?
This plugin implements JSON parsing for ESLint using the language plugins API, which is the official way of supporting non-JavaScript languages in ESLint. This differs from the other plugins:
- [`eslint-plugin-json`](https://github.com/azeemba/eslint-plugin-json) uses a processor to parse the JSON, meaning it doesn't create an AST and you can't write custom rules for it.
- [`eslint-plugin-jsonc`](https://github.com/ota-meshi/eslint-plugin-jsonc) uses a parser that still goes through the JavaScript linting functionality and requires several rules to disallow valid JavaScript syntax that is invalid in JSON.
As such, this plugin is more robust and faster than the others. You can write your own custom rules when using the languages in this plugin, too.
### What about missing rules that are available in `eslint-plugin-json` and `eslint-plugin-jsonc`?
Most of the rules in `eslint-plugin-json` are actually syntax errors that are caught automatically by the parser used in this plugin.
Similarly, many of the rules in `eslint-plugin-jsonc` specifically disallow valid JavaScript syntax that is invalid in the context of JSON. These are also automatically caught by the parser in this plugin.
Any other rules that catch potential problems in JSON are welcome to be implemented. You can [open an issue](https://github.com/eslint/json/issues/new/choose) to propose a new rule.
## Editor and IDE Setup
### Visual Studio Code
First, ensure you have the [ESLint plugin](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) installed.
Then, edit `eslint.validate` in your `settings.json` file to include `json`, `jsonc`, and `json5`:
```json
{
"eslint.validate": ["json", "jsonc", "json5"]
}
```
### JetBrains WebStorm
For any [JetBrains WebStorm](https://www.jetbrains.com/webstorm/), configure the [ESLint scope](https://www.jetbrains.com/help/webstorm/eslint.html#ws_eslint_configure_scope) to include `json`, `jsonc`, and `json5`, such as:
```text
**/*.{js,ts,jsx,tsx,cjs,cts,mjs,mts,html,vue,json,jsonc,json5}
```
## License
Apache 2.0
<!-- NOTE: This section is autogenerated. Do not manually edit.-->
<!--sponsorsstart-->
## Sponsors
The following companies, organizations, and individuals support ESLint's ongoing maintenance and development. [Become a Sponsor](https://eslint.org/donate)
to get your logo on our READMEs and [website](https://eslint.org/sponsors).
<h3>Platinum Sponsors</h3>
<p><a href="https://automattic.com"><img src="https://images.opencollective.com/automattic/d0ef3e1/logo.png" alt="Automattic" height="128"></a></p><h3>Gold Sponsors</h3>
<p><a href="https://qlty.sh/"><img src="https://images.opencollective.com/qltysh/33d157d/logo.png" alt="Qlty Software" height="96"></a> <a href="https://shopify.engineering/"><img src="https://avatars.githubusercontent.com/u/8085" alt="Shopify" height="96"></a> <a href="https://www.coderabbit.ai/?utm_source=cr_org&utm_medium=github"><img src="https://avatars.githubusercontent.com/u/132028505" alt="CodeRabbit" height="96"></a></p><h3>Silver Sponsors</h3>
<p><a href="https://vite.dev/"><img src="https://images.opencollective.com/vite/d472863/logo.png" alt="Vite" height="64"></a> <a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/2d6c3b6/logo.png" alt="Liftoff" height="64"></a> <a href="https://stackblitz.com"><img src="https://avatars.githubusercontent.com/u/28635252" alt="StackBlitz" height="64"></a></p><h3>Bronze Sponsors</h3>
<p><a href="https://cybozu.co.jp/"><img src="https://images.opencollective.com/cybozu/933e46d/logo.png" alt="Cybozu" height="32"></a> <a href="https://opensource.sap.com"><img src="https://avatars.githubusercontent.com/u/2531208" alt="SAP" height="32"></a> <a href="https://www.crawljobs.com/"><img src="https://images.opencollective.com/crawljobs-poland/fa43a17/logo.png" alt="CrawlJobs" height="32"></a> <a href="https://depot.dev"><img src="https://images.opencollective.com/depot/39125a1/logo.png" alt="Depot" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://www.gitbook.com"><img src="https://avatars.githubusercontent.com/u/7111340" alt="GitBook" height="32"></a> <a href="https://citadel.co.jp"><img src="https://avatars.githubusercontent.com/u/75781367" alt="Citadel AI" height="32"></a></p>
<h3>Technology Sponsors</h3>
Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.
<p><a href="https://netlify.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/netlify-icon.svg" alt="Netlify" height="32"></a> <a href="https://algolia.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/algolia-icon.svg" alt="Algolia" height="32"></a> <a href="https://1password.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/1password-icon.svg" alt="1Password" height="32"></a></p>
<!--sponsorsend-->
@@ -0,0 +1,7 @@
export default rules;
declare const rules: {
readonly "json/no-duplicate-keys": "error";
readonly "json/no-empty-keys": "error";
readonly "json/no-unnormalized-keys": "error";
readonly "json/no-unsafe-values": "error";
};
@@ -0,0 +1,7 @@
const rules = /** @type {const} */ ({
"json/no-duplicate-keys": "error",
"json/no-empty-keys": "error",
"json/no-unnormalized-keys": "error",
"json/no-unsafe-values": "error"
});
export default rules;
+9
View File
@@ -0,0 +1,9 @@
declare const _default: {
"no-duplicate-keys": import("../rules/no-duplicate-keys.js").NoDuplicateKeysRuleDefinition;
"no-empty-keys": import("../rules/no-empty-keys.js").NoEmptyKeysRuleDefinition;
"no-unnormalized-keys": import("../rules/no-unnormalized-keys.js").NoUnnormalizedKeysRuleDefinition;
"no-unsafe-values": import("../rules/no-unsafe-values.js").NoUnsafeValuesRuleDefinition;
"sort-keys": import("../rules/sort-keys.js").SortKeysRuleDefinition;
"top-level-interop": import("../rules/top-level-interop.js").TopLevelInteropRuleDefinition;
};
export default _default;
+14
View File
@@ -0,0 +1,14 @@
import rule0 from "../rules/no-duplicate-keys.js";
import rule1 from "../rules/no-empty-keys.js";
import rule2 from "../rules/no-unnormalized-keys.js";
import rule3 from "../rules/no-unsafe-values.js";
import rule4 from "../rules/sort-keys.js";
import rule5 from "../rules/top-level-interop.js";
export default {
"no-duplicate-keys": rule0,
"no-empty-keys": rule1,
"no-unnormalized-keys": rule2,
"no-unsafe-values": rule3,
"sort-keys": rule4,
"top-level-interop": rule5,
};
+29
View File
@@ -0,0 +1,29 @@
export default plugin;
export { JSONSourceCode };
export * from "./languages/json-language.js";
export * from "./types.js";
declare namespace plugin {
export namespace meta {
let name: string;
let namespace: string;
let version: string;
}
export namespace languages {
let json: JSONLanguage;
let jsonc: JSONLanguage;
let json5: JSONLanguage;
}
export { rules };
export namespace configs {
namespace recommended {
let name_1: string;
export { name_1 as name };
export let plugins: {};
export { recommendedRules as rules };
}
}
}
import { JSONSourceCode } from "./languages/json-source-code.js";
import { JSONLanguage } from "./languages/json-language.js";
import rules from "./build/rules.js";
import recommendedRules from "./build/recommended-config.js";
+39
View File
@@ -0,0 +1,39 @@
/**
* @fileoverview JSON plugin.
* @author Nicholas C. Zakas
*/
//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
import { JSONLanguage } from "./languages/json-language.js";
import { JSONSourceCode } from "./languages/json-source-code.js";
import recommendedRules from "./build/recommended-config.js";
import rules from "./build/rules.js";
//-----------------------------------------------------------------------------
// Plugin
//-----------------------------------------------------------------------------
const plugin = {
meta: {
name: "@eslint/json",
namespace: "json",
version: "2.0.1", // x-release-please-version
},
languages: {
json: new JSONLanguage({ mode: "json" }),
jsonc: new JSONLanguage({ mode: "jsonc" }),
json5: new JSONLanguage({ mode: "json5" }),
},
rules,
configs: {
recommended: {
name: "@eslint/json/recommended",
plugins: {},
rules: recommendedRules,
},
},
};
Object.assign(plugin.configs.recommended.plugins, { json: plugin });
export default plugin;
export { JSONSourceCode };
export * from "./languages/json-language.js";
export * from "./types.js";
@@ -0,0 +1,85 @@
/**
* @import { DocumentNode, AnyNode } from "@humanwhocodes/momoa";
* @import { Language, OkParseResult, ParseResult, File } from "@eslint/core";
* @typedef {OkParseResult<DocumentNode>} JSONOkParseResult
* @typedef {ParseResult<DocumentNode>} JSONParseResult
* @typedef {Object} JSONLanguageOptions
* @property {boolean} [allowTrailingCommas] Whether to allow trailing commas in JSONC mode.
*/
/**
* JSON Language Object
* @implements {Language<{ LangOptions: JSONLanguageOptions; Code: JSONSourceCode; RootNode: DocumentNode; Node: AnyNode }>}
*/
export class JSONLanguage implements Language {
/**
* Creates a new instance.
* @param {Object} options The options to use for this instance.
* @param {"json"|"jsonc"|"json5"} options.mode The parser mode to use.
*/
constructor({ mode }: {
mode: "json" | "jsonc" | "json5";
});
/**
* The type of file to read.
* @type {"text"}
*/
fileType: "text";
/**
* The line number at which the parser starts counting.
* @type {0|1}
*/
lineStart: 0 | 1;
/**
* The column number at which the parser starts counting.
* @type {0|1}
*/
columnStart: 0 | 1;
/**
* The name of the key that holds the type of the node.
* @type {string}
*/
nodeTypeKey: string;
/**
* The visitor keys.
* @type {Record<string, string[]>}
*/
visitorKeys: Record<string, string[]>;
/**
* Validates the language options.
* @param {JSONLanguageOptions} languageOptions The language options to validate.
* @returns {void}
* @throws {Error} When the language options are invalid.
*/
validateLanguageOptions(languageOptions: JSONLanguageOptions): void;
/**
* Parses the given file into an AST.
* @param {File} file The virtual file to parse.
* @param {{languageOptions: JSONLanguageOptions}} context The options to use for parsing.
* @returns {JSONParseResult} The result of parsing.
*/
parse(file: File, context: {
languageOptions: JSONLanguageOptions;
}): JSONParseResult;
/**
* Creates a new `JSONSourceCode` object from the given information.
* @param {File} file The virtual file to create a `JSONSourceCode` object from.
* @param {JSONOkParseResult} parseResult The result returned from `parse()`.
* @returns {JSONSourceCode} The new `JSONSourceCode` object.
*/
createSourceCode(file: File, parseResult: JSONOkParseResult): JSONSourceCode;
#private;
}
export type JSONOkParseResult = OkParseResult<DocumentNode>;
export type JSONParseResult = ParseResult<DocumentNode>;
export type JSONLanguageOptions = {
/**
* Whether to allow trailing commas in JSONC mode.
*/
allowTrailingCommas?: boolean;
};
import type { Language } from "@eslint/core";
import type { File } from "@eslint/core";
import { JSONSourceCode } from "./json-source-code.js";
import type { DocumentNode } from "@humanwhocodes/momoa";
import type { OkParseResult } from "@eslint/core";
import type { ParseResult } from "@eslint/core";
+143
View File
@@ -0,0 +1,143 @@
/**
* @fileoverview The JSONLanguage class.
* @author Nicholas C. Zakas
*/
//------------------------------------------------------------------------------
// Imports
//------------------------------------------------------------------------------
import { parse } from "@humanwhocodes/momoa";
import { JSONSourceCode } from "./json-source-code.js";
import { visitorKeys } from "@humanwhocodes/momoa";
//-----------------------------------------------------------------------------
// Types
//-----------------------------------------------------------------------------
/**
* @import { DocumentNode, AnyNode } from "@humanwhocodes/momoa";
* @import { Language, OkParseResult, ParseResult, File } from "@eslint/core";
* @typedef {OkParseResult<DocumentNode>} JSONOkParseResult
* @typedef {ParseResult<DocumentNode>} JSONParseResult
* @typedef {Object} JSONLanguageOptions
* @property {boolean} [allowTrailingCommas] Whether to allow trailing commas in JSONC mode.
*/
//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------
/**
* JSON Language Object
* @implements {Language<{ LangOptions: JSONLanguageOptions; Code: JSONSourceCode; RootNode: DocumentNode; Node: AnyNode }>}
*/
export class JSONLanguage {
/**
* The type of file to read.
* @type {"text"}
*/
fileType = "text";
/**
* The line number at which the parser starts counting.
* @type {0|1}
*/
lineStart = 1;
/**
* The column number at which the parser starts counting.
* @type {0|1}
*/
columnStart = 1;
/**
* The name of the key that holds the type of the node.
* @type {string}
*/
nodeTypeKey = "type";
/**
* The parser mode.
* @type {"json"|"jsonc"|"json5"}
*/
#mode = "json";
/**
* The visitor keys.
* @type {Record<string, string[]>}
*/
visitorKeys = Object.fromEntries([...visitorKeys]);
/**
* Creates a new instance.
* @param {Object} options The options to use for this instance.
* @param {"json"|"jsonc"|"json5"} options.mode The parser mode to use.
*/
constructor({ mode }) {
this.#mode = mode;
}
/**
* Validates the language options.
* @param {JSONLanguageOptions} languageOptions The language options to validate.
* @returns {void}
* @throws {Error} When the language options are invalid.
*/
validateLanguageOptions(languageOptions) {
if (languageOptions.allowTrailingCommas !== undefined) {
if (typeof languageOptions.allowTrailingCommas !== "boolean") {
throw new Error("allowTrailingCommas must be a boolean if provided.");
}
// we know that allowTrailingCommas is a boolean here
// only allowed in JSONC mode
if (this.#mode !== "jsonc") {
throw new Error("allowTrailingCommas option is only available in JSONC.");
}
}
}
/**
* Parses the given file into an AST.
* @param {File} file The virtual file to parse.
* @param {{languageOptions: JSONLanguageOptions}} context The options to use for parsing.
* @returns {JSONParseResult} The result of parsing.
*/
parse(file, context) {
// Note: BOM already removed
const text = /** @type {string} */ (file.body);
const allowTrailingCommas = context?.languageOptions?.allowTrailingCommas;
/*
* Check for parsing errors first. If there's a parsing error, nothing
* else can happen. However, a parsing error does not throw an error
* from this method - it's just considered a fatal error message, a
* problem that ESLint identified just like any other.
*/
try {
const root = parse(text, {
mode: this.#mode,
ranges: true,
tokens: true,
allowTrailingCommas,
});
return {
ok: true,
ast: root,
};
}
catch (ex) {
// error messages end with (line:column) so we strip that off for ESLint
const message = ex.message
.slice(0, ex.message.lastIndexOf("("))
.trim();
return {
ok: false,
errors: [
{
...ex,
message,
},
],
};
}
}
/* eslint-disable class-methods-use-this -- Required to complete interface. */
/**
* Creates a new `JSONSourceCode` object from the given information.
* @param {File} file The virtual file to create a `JSONSourceCode` object from.
* @param {JSONOkParseResult} parseResult The result returned from `parse()`.
* @returns {JSONSourceCode} The new `JSONSourceCode` object.
*/
createSourceCode(file, parseResult) {
return new JSONSourceCode({
text: /** @type {string} */ (file.body),
ast: parseResult.ast,
});
}
}
@@ -0,0 +1,123 @@
/**
* JSON Source Code Object
* @extends {TextSourceCodeBase<{LangOptions: JSONLanguageOptions, RootNode: DocumentNode, SyntaxElementWithLoc: JSONSyntaxElement, ConfigNode: Token}>}
*/
export class JSONSourceCode extends TextSourceCodeBase<{
LangOptions: JSONLanguageOptions;
RootNode: DocumentNode;
SyntaxElementWithLoc: JSONSyntaxElement;
ConfigNode: Token;
}> {
/**
* Creates a new instance.
* @param {Object} options The options for the instance.
* @param {string} options.text The source code text.
* @param {DocumentNode} options.ast The root AST node.
*/
constructor({ text, ast }: {
text: string;
ast: DocumentNode;
});
/**
* The comment tokens in the source code.
* @type {Array<Token>|undefined}
*/
comments: Array<Token> | undefined;
/**
* Returns an array of all inline configuration nodes found in the
* source code.
* @returns {Array<Token>} An array of all inline configuration nodes.
*/
getInlineConfigNodes(): Array<Token>;
/**
* Returns directives that enable or disable rules along with any problems
* encountered while parsing the directives.
* @returns {{problems:Array<FileProblem>,directives:Array<Directive>}} Information
* that ESLint needs to further process the directives.
*/
getDisableDirectives(): {
problems: Array<FileProblem>;
directives: Array<Directive>;
};
/**
* Returns inline rule configurations along with any problems
* encountered while parsing the configurations.
* @returns {{problems:Array<FileProblem>,configs:Array<{config:{rules:RulesConfig},loc:LocationRange}>}} Information
* that ESLint needs to further process the rule configurations.
*/
applyInlineConfig(): {
problems: Array<FileProblem>;
configs: Array<{
config: {
rules: RulesConfig;
};
loc: LocationRange;
}>;
};
/**
* Returns the parent of the given node.
* @param {AnyNode} node The node to get the parent of.
* @returns {AnyNode|undefined} The parent of the node.
*/
getParent(node: AnyNode): AnyNode | undefined;
/**
* Traverse the source code and return the steps that were taken.
* @returns {Iterable<JSONTraversalStep>} The steps that were taken while traversing the source code.
*/
traverse(): Iterable<JSONTraversalStep>;
/**
* Gets the token before the given node or token, optionally including comments.
* @param {AnyNode|Token} nodeOrToken The node or token to get the previous token for.
* @param {Object} [options] Options object.
* @param {boolean} [options.includeComments] If true, return comments when they are present.
* @returns {Token|null} The previous token or comment, or null if there is none.
*/
getTokenBefore(nodeOrToken: AnyNode | Token, { includeComments }?: {
includeComments?: boolean;
}): Token | null;
/**
* Gets the token after the given node or token, skipping any comments unless includeComments is true.
* @param {AnyNode|Token} nodeOrToken The node or token to get the next token for.
* @param {Object} [options] Options object.
* @param {boolean} [options.includeComments=false] If true, return comments when they are present.
* @returns {Token|null} The next token or comment, or null if there is none.
*/
getTokenAfter(nodeOrToken: AnyNode | Token, { includeComments }?: {
includeComments?: boolean;
}): Token | null;
#private;
}
import type { JSONLanguageOptions } from "./json-language.js";
import type { DocumentNode } from "@humanwhocodes/momoa";
import type { JSONSyntaxElement } from "../types.js";
import type { Token } from "@humanwhocodes/momoa";
import { TextSourceCodeBase } from "@eslint/plugin-kit";
import type { FileProblem } from "@eslint/core";
import { Directive } from "@eslint/plugin-kit";
import type { RulesConfig } from "@eslint/core";
import type { LocationRange } from "@humanwhocodes/momoa";
import type { AnyNode } from "@humanwhocodes/momoa";
/**
* A class to represent a step in the traversal process.
*/
declare class JSONTraversalStep extends VisitNodeStep {
/**
* Creates a new instance.
* @param {Object} options The options for the step.
* @param {AnyNode} options.target The target of the step.
* @param {1|2} options.phase The phase of the step.
* @param {Array<any>} options.args The arguments of the step.
*/
constructor({ target, phase, args }: {
target: AnyNode;
phase: 1 | 2;
args: Array<any>;
});
/**
* The target of the step.
* @type {AnyNode}
*/
target: AnyNode;
}
import { VisitNodeStep } from "@eslint/plugin-kit";
export {};
@@ -0,0 +1,327 @@
/**
* @fileoverview The JSONSourceCode class.
* @author Nicholas C. Zakas
*/
//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
import { iterator } from "@humanwhocodes/momoa";
import { VisitNodeStep, TextSourceCodeBase, ConfigCommentParser, Directive, } from "@eslint/plugin-kit";
//-----------------------------------------------------------------------------
// Types
//-----------------------------------------------------------------------------
/**
* @import { DocumentNode, AnyNode, Token, LocationRange } from "@humanwhocodes/momoa";
* @import { FileProblem, DirectiveType, RulesConfig } from "@eslint/core";
* @import { JSONSyntaxElement } from "../types.js";
* @import { JSONLanguageOptions } from "./json-language.js";
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
const commentParser = new ConfigCommentParser();
const INLINE_CONFIG = /^\s*eslint(?:-enable|-disable(?:(?:-next)?-line)?)?(?:\s|$)/u;
/**
* A class to represent a step in the traversal process.
*/
class JSONTraversalStep extends VisitNodeStep {
/**
* The target of the step.
* @type {AnyNode}
*/
target = undefined;
/**
* Creates a new instance.
* @param {Object} options The options for the step.
* @param {AnyNode} options.target The target of the step.
* @param {1|2} options.phase The phase of the step.
* @param {Array<any>} options.args The arguments of the step.
*/
constructor({ target, phase, args }) {
super({ target, phase, args });
this.target = target;
}
}
/**
* Processes tokens to extract comments and their starting tokens.
* @param {Array<Token>} tokens The tokens to process.
* @returns {{ comments: Array<Token>, starts: Map<number, number>, ends: Map<number, number>}}
* An object containing an array of comments, a map of starting token range to token index, and
* a map of ending token range to token index.
*/
function processTokens(tokens) {
/** @type {Array<Token>} */
const comments = [];
/** @type {Map<number, number>} */
const starts = new Map();
/** @type {Map<number, number>} */
const ends = new Map();
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.type.endsWith("Comment")) {
comments.push(token);
}
starts.set(token.range[0], i);
ends.set(token.range[1], i);
}
return { comments, starts, ends };
}
//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------
/**
* JSON Source Code Object
* @extends {TextSourceCodeBase<{LangOptions: JSONLanguageOptions, RootNode: DocumentNode, SyntaxElementWithLoc: JSONSyntaxElement, ConfigNode: Token}>}
*/
export class JSONSourceCode extends TextSourceCodeBase {
/**
* Cached traversal steps.
* @type {Array<JSONTraversalStep>|undefined}
*/
#steps;
/**
* Cache of parent nodes.
* @type {WeakMap<AnyNode, AnyNode>}
*/
#parents = new WeakMap();
/**
* Collection of inline configuration comments.
* @type {Array<Token>}
*/
#inlineConfigComments;
/**
* The AST of the source code.
* @type {DocumentNode}
*/
ast = undefined;
/**
* The comment tokens in the source code.
* @type {Array<Token>|undefined}
*/
comments;
/**
* A map of token start positions to their corresponding index.
* @type {Map<number, number>}
*/
#tokenStarts;
/**
* A map of token end positions to their corresponding index.
* @type {Map<number, number>}
*/
#tokenEnds;
/**
* Creates a new instance.
* @param {Object} options The options for the instance.
* @param {string} options.text The source code text.
* @param {DocumentNode} options.ast The root AST node.
*/
constructor({ text, ast }) {
super({ text, ast, lineEndingPattern: /\r\n|[\r\n]/u });
this.ast = ast;
const { comments, starts, ends } = processTokens(this.ast.tokens ?? []);
this.comments = comments;
this.#tokenStarts = starts;
this.#tokenEnds = ends;
}
/**
* Returns the value of the given comment.
* @param {Token} comment The comment to get the value of.
* @returns {string} The value of the comment.
* @throws {Error} When an unexpected comment type is passed.
*/
#getCommentValue(comment) {
if (comment.type === "LineComment") {
return this.getText(comment).slice(2); // strip leading `//`
}
if (comment.type === "BlockComment") {
return this.getText(comment).slice(2, -2); // strip leading `/*` and trailing `*/`
}
throw new Error(`Unexpected comment type '${comment.type}'`);
}
/**
* Returns an array of all inline configuration nodes found in the
* source code.
* @returns {Array<Token>} An array of all inline configuration nodes.
*/
getInlineConfigNodes() {
if (!this.#inlineConfigComments) {
this.#inlineConfigComments = this.comments.filter(comment => INLINE_CONFIG.test(this.#getCommentValue(comment)));
}
return this.#inlineConfigComments ?? [];
}
/**
* Returns directives that enable or disable rules along with any problems
* encountered while parsing the directives.
* @returns {{problems:Array<FileProblem>,directives:Array<Directive>}} Information
* that ESLint needs to further process the directives.
*/
getDisableDirectives() {
/** @type {Array<FileProblem>} */
const problems = [];
/** @type {Array<Directive>} */
const directives = [];
this.getInlineConfigNodes().forEach(comment => {
const { label, value, justification } = commentParser.parseDirective(this.#getCommentValue(comment));
// `eslint-disable-line` directives are not allowed to span multiple lines as it would be confusing to which lines they apply
if (label === "eslint-disable-line" &&
comment.loc.start.line !== comment.loc.end.line) {
const message = `${label} comment should not span multiple lines.`;
problems.push({
ruleId: null,
message,
loc: comment.loc,
});
return;
}
switch (label) {
case "eslint-disable":
case "eslint-enable":
case "eslint-disable-next-line":
case "eslint-disable-line": {
const directiveType = label.slice("eslint-".length);
directives.push(new Directive({
type: /** @type {DirectiveType} */ (directiveType),
node: comment,
value,
justification,
}));
}
// no default
}
});
return { problems, directives };
}
/**
* Returns inline rule configurations along with any problems
* encountered while parsing the configurations.
* @returns {{problems:Array<FileProblem>,configs:Array<{config:{rules:RulesConfig},loc:LocationRange}>}} Information
* that ESLint needs to further process the rule configurations.
*/
applyInlineConfig() {
/** @type {Array<FileProblem>} */
const problems = [];
/** @type {Array<{config:{rules:RulesConfig},loc:LocationRange}>} */
const configs = [];
this.getInlineConfigNodes().forEach(comment => {
const { label, value } = commentParser.parseDirective(this.#getCommentValue(comment));
if (label === "eslint") {
const parseResult = commentParser.parseJSONLikeConfig(value);
if (parseResult.ok) {
configs.push({
config: {
rules: parseResult.config,
},
loc: comment.loc,
});
}
else {
problems.push({
ruleId: null,
message:
/** @type {{ok: false, error: { message: string }}} */ (parseResult).error.message,
loc: comment.loc,
});
}
}
});
return {
configs,
problems,
};
}
/**
* Returns the parent of the given node.
* @param {AnyNode} node The node to get the parent of.
* @returns {AnyNode|undefined} The parent of the node.
*/
getParent(node) {
return this.#parents.get(node);
}
/**
* Traverse the source code and return the steps that were taken.
* @returns {Iterable<JSONTraversalStep>} The steps that were taken while traversing the source code.
*/
traverse() {
// Because the AST doesn't mutate, we can cache the steps
if (this.#steps) {
return this.#steps.values();
}
/** @type {Array<JSONTraversalStep>} */
const steps = (this.#steps = []);
for (const { node, parent, phase } of iterator(this.ast)) {
if (parent) {
this.#parents.set(
/** @type {AnyNode} */ (node),
/** @type {AnyNode} */ (parent));
}
steps.push(new JSONTraversalStep({
target: /** @type {AnyNode} */ (node),
phase: phase === "enter" ? 1 : 2,
args: [node, parent],
}));
}
return steps;
}
/**
* Gets the token before the given node or token, optionally including comments.
* @param {AnyNode|Token} nodeOrToken The node or token to get the previous token for.
* @param {Object} [options] Options object.
* @param {boolean} [options.includeComments] If true, return comments when they are present.
* @returns {Token|null} The previous token or comment, or null if there is none.
*/
getTokenBefore(nodeOrToken, { includeComments = false } = {}) {
const index = this.#tokenStarts.get(nodeOrToken.range[0]);
if (index === undefined) {
return null;
}
let previousIndex = index - 1;
if (previousIndex < 0) {
return null;
}
const tokens = this.ast.tokens;
let tokenOrComment = tokens[previousIndex];
if (includeComments) {
return tokenOrComment;
}
// skip comments
while (tokenOrComment?.type.endsWith("Comment")) {
previousIndex--;
if (previousIndex < 0) {
return null;
}
tokenOrComment = tokens[previousIndex];
}
return tokenOrComment;
}
/**
* Gets the token after the given node or token, skipping any comments unless includeComments is true.
* @param {AnyNode|Token} nodeOrToken The node or token to get the next token for.
* @param {Object} [options] Options object.
* @param {boolean} [options.includeComments=false] If true, return comments when they are present.
* @returns {Token|null} The next token or comment, or null if there is none.
*/
getTokenAfter(nodeOrToken, { includeComments = false } = {}) {
const index = this.#tokenEnds.get(nodeOrToken.range[1]);
if (index === undefined) {
return null;
}
let nextIndex = index + 1;
const tokens = this.ast.tokens;
if (nextIndex >= tokens.length) {
return null;
}
let tokenOrComment = tokens[nextIndex];
if (includeComments) {
return tokenOrComment;
}
// skip comments
while (tokenOrComment?.type.endsWith("Comment")) {
nextIndex++;
if (nextIndex >= tokens.length) {
return null;
}
tokenOrComment = tokens[nextIndex];
}
return tokenOrComment;
}
}
@@ -0,0 +1,14 @@
export default rule;
export type NoDuplicateKeysMessageIds = "duplicateKey";
export type NoDuplicateKeysRuleDefinition = JSONRuleDefinition<{
MessageIds: NoDuplicateKeysMessageIds;
}>;
/**
* @import { MemberNode } from "@humanwhocodes/momoa";
* @import { JSONRuleDefinition } from "../types.js";
* @typedef {"duplicateKey"} NoDuplicateKeysMessageIds
* @typedef {JSONRuleDefinition<{ MessageIds: NoDuplicateKeysMessageIds }>} NoDuplicateKeysRuleDefinition
*/
/** @type {NoDuplicateKeysRuleDefinition} */
declare const rule: NoDuplicateKeysRuleDefinition;
import type { JSONRuleDefinition } from "../types.js";
+68
View File
@@ -0,0 +1,68 @@
/**
* @fileoverview Rule to prevent duplicate keys in JSON.
* @author Nicholas C. Zakas
*/
//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
import { getKey, getRawKey } from "../util.js";
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { MemberNode } from "@humanwhocodes/momoa";
* @import { JSONRuleDefinition } from "../types.js";
* @typedef {"duplicateKey"} NoDuplicateKeysMessageIds
* @typedef {JSONRuleDefinition<{ MessageIds: NoDuplicateKeysMessageIds }>} NoDuplicateKeysRuleDefinition
*/
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
/** @type {NoDuplicateKeysRuleDefinition} */
const rule = {
meta: {
type: "problem",
languages: ["json/json", "json/jsonc", "json/json5"],
docs: {
recommended: true,
description: "Disallow duplicate keys in JSON objects",
dialects: ["JSON", "JSONC", "JSON5"],
url: "https://github.com/eslint/json/tree/main/docs/rules/no-duplicate-keys.md",
},
messages: {
duplicateKey: 'Duplicate key "{{key}}" found.',
},
},
create(context) {
/** @type {Array<Map<string, MemberNode>|undefined>} */
const objectKeys = [];
/** @type {Map<string, MemberNode>|undefined} */
let keys;
return {
Object() {
objectKeys.push(keys);
keys = new Map();
},
Member(node) {
const key = getKey(node);
const rawKey = getRawKey(node, context.sourceCode);
if (keys.has(key)) {
context.report({
loc: node.name.loc,
messageId: "duplicateKey",
data: {
key: rawKey,
},
});
}
else {
keys.set(key, node);
}
},
"Object:exit"() {
keys = objectKeys.pop();
},
};
},
};
export default rule;
+13
View File
@@ -0,0 +1,13 @@
export default rule;
export type NoEmptyKeysMessageIds = "emptyKey";
export type NoEmptyKeysRuleDefinition = JSONRuleDefinition<{
MessageIds: NoEmptyKeysMessageIds;
}>;
/**
* @import { JSONRuleDefinition } from "../types.js";
* @typedef {"emptyKey"} NoEmptyKeysMessageIds
* @typedef {JSONRuleDefinition<{ MessageIds: NoEmptyKeysMessageIds }>} NoEmptyKeysRuleDefinition
*/
/** @type {NoEmptyKeysRuleDefinition} */
declare const rule: NoEmptyKeysRuleDefinition;
import type { JSONRuleDefinition } from "../types.js";
+49
View File
@@ -0,0 +1,49 @@
/**
* @fileoverview Rule to prevent empty keys in JSON.
* @author Nicholas C. Zakas
*/
//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
import { getKey } from "../util.js";
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { JSONRuleDefinition } from "../types.js";
* @typedef {"emptyKey"} NoEmptyKeysMessageIds
* @typedef {JSONRuleDefinition<{ MessageIds: NoEmptyKeysMessageIds }>} NoEmptyKeysRuleDefinition
*/
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
/** @type {NoEmptyKeysRuleDefinition} */
const rule = {
meta: {
type: "problem",
languages: ["json/json", "json/jsonc", "json/json5"],
docs: {
recommended: true,
description: "Disallow empty keys in JSON objects",
dialects: ["JSON", "JSONC", "JSON5"],
url: "https://github.com/eslint/json/tree/main/docs/rules/no-empty-keys.md",
},
messages: {
emptyKey: "Empty key found.",
},
},
create(context) {
return {
Member(node) {
const key = getKey(node);
if (key.trim() === "") {
context.report({
loc: node.name.loc,
messageId: "emptyKey",
});
}
},
};
},
};
export default rule;
@@ -0,0 +1,18 @@
export default rule;
export type NoUnnormalizedKeysMessageIds = "unnormalizedKey";
export type NoUnnormalizedKeysOptions = {
form: string;
};
export type NoUnnormalizedKeysRuleDefinition = JSONRuleDefinition<{
RuleOptions: [NoUnnormalizedKeysOptions];
MessageIds: NoUnnormalizedKeysMessageIds;
}>;
/**
* @import { JSONRuleDefinition } from "../types.js";
* @typedef {"unnormalizedKey"} NoUnnormalizedKeysMessageIds
* @typedef {{ form: string }} NoUnnormalizedKeysOptions
* @typedef {JSONRuleDefinition<{ RuleOptions: [NoUnnormalizedKeysOptions], MessageIds: NoUnnormalizedKeysMessageIds }>} NoUnnormalizedKeysRuleDefinition
*/
/** @type {NoUnnormalizedKeysRuleDefinition} */
declare const rule: NoUnnormalizedKeysRuleDefinition;
import type { JSONRuleDefinition } from "../types.js";
@@ -0,0 +1,83 @@
/**
* @fileoverview Rule to detect unnormalized keys in JSON.
* @author Bradley Meck Farias
*/
//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
import { getKey, getRawKey } from "../util.js";
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { JSONRuleDefinition } from "../types.js";
* @typedef {"unnormalizedKey"} NoUnnormalizedKeysMessageIds
* @typedef {{ form: string }} NoUnnormalizedKeysOptions
* @typedef {JSONRuleDefinition<{ RuleOptions: [NoUnnormalizedKeysOptions], MessageIds: NoUnnormalizedKeysMessageIds }>} NoUnnormalizedKeysRuleDefinition
*/
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
/** @type {NoUnnormalizedKeysRuleDefinition} */
const rule = {
meta: {
type: "problem",
languages: ["json/json", "json/jsonc", "json/json5"],
fixable: "code",
docs: {
recommended: true,
description: "Disallow JSON keys that are not normalized",
dialects: ["JSON", "JSONC", "JSON5"],
url: "https://github.com/eslint/json/tree/main/docs/rules/no-unnormalized-keys.md",
},
messages: {
unnormalizedKey: "Unnormalized key '{{key}}' found.",
},
schema: [
{
type: "object",
properties: {
form: {
enum: ["NFC", "NFD", "NFKC", "NFKD"],
},
},
additionalProperties: false,
},
],
defaultOptions: [
{
form: "NFC",
},
],
},
create(context) {
const [{ form }] = context.options;
return {
Member(node) {
const key = getKey(node);
const rawKey = getRawKey(node, context.sourceCode);
const normalizedKey = key.normalize(form);
if (normalizedKey !== key) {
const { name } = node;
context.report({
loc: name.loc,
messageId: "unnormalizedKey",
data: {
key: rawKey,
},
fix(fixer) {
if (key !== rawKey) {
// Do not perform auto-fix when the raw key contains escape sequences.
return null;
}
return fixer.replaceTextRange(name.type === "String"
? [name.range[0] + 1, name.range[1] - 1]
: name.range, normalizedKey);
},
});
}
},
};
},
};
export default rule;
@@ -0,0 +1,8 @@
export default rule;
export type NoUnsafeValuesMessageIds = "unsafeNumber" | "unsafeInteger" | "unsafeZero" | "subnormal" | "loneSurrogate";
export type NoUnsafeValuesRuleDefinition = JSONRuleDefinition<{
MessageIds: NoUnsafeValuesMessageIds;
}>;
/** @type {NoUnsafeValuesRuleDefinition} */
declare const rule: NoUnsafeValuesRuleDefinition;
import type { JSONRuleDefinition } from "../types.js";
+141
View File
@@ -0,0 +1,141 @@
/**
* @fileoverview Rule to detect unsafe values in JSON.
* @author Bradley Meck Farias
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { JSONRuleDefinition } from "../types.js";
* @typedef {"unsafeNumber"|"unsafeInteger"|"unsafeZero"|"subnormal"|"loneSurrogate"} NoUnsafeValuesMessageIds
* @typedef {JSONRuleDefinition<{ MessageIds: NoUnsafeValuesMessageIds }>} NoUnsafeValuesRuleDefinition
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
/*
* This rule is based on the JSON grammar from RFC 8259, section 6.
* https://tools.ietf.org/html/rfc8259#section-6
*
* Also, this rule is based on the JSON5 grammar from json5.org, section 6.
* https://spec.json5.org/#numbers
*
* We separately capture the integer and fractional parts of a number, so that
* we can check for unsafe numbers that will evaluate to Infinity.
*/
const NUMBER = /^[+-]?(?<int>0|([1-9]\d*))?(?:\.(?<frac>\d*))?(?:e[+-]?\d+)?$/iu;
const NON_ZERO = /[1-9]/u;
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
/** @type {NoUnsafeValuesRuleDefinition} */
const rule = {
meta: {
type: "problem",
languages: ["json/json", "json/jsonc", "json/json5"],
docs: {
recommended: true,
description: "Disallow JSON values that are unsafe for interchange",
dialects: ["JSON", "JSONC", "JSON5"],
url: "https://github.com/eslint/json/tree/main/docs/rules/no-unsafe-values.md",
},
messages: {
unsafeNumber: "The number '{{ value }}' will evaluate to Infinity.",
unsafeInteger: "The integer '{{ value }}' is outside the safe integer range.",
unsafeZero: "The number '{{ value }}' will evaluate to zero.",
subnormal: "Unexpected subnormal number '{{ value }}' found, which may cause interoperability issues.",
loneSurrogate: "Lone surrogate '{{ surrogate }}' found.",
},
},
create(context) {
return {
Number(node) {
const value = context.sourceCode.getText(node);
if (Number.isFinite(node.value) !== true) {
context.report({
loc: node.loc,
messageId: "unsafeNumber",
data: { value },
});
}
else {
// Also matches -0, intentionally
if (node.value === 0) {
// If the value has been rounded down to 0, but there was some
// fraction or non-zero part before the e-, this is a very small
// number that doesn't fit inside an f64.
const match = value.match(NUMBER);
if (match === null) {
return;
}
// If any part of the number other than the exponent has a
// non-zero digit in it, this number was not intended to be
// evaluated down to a zero.
if (NON_ZERO.test(match.groups.int) ||
NON_ZERO.test(match.groups.frac)) {
context.report({
loc: node.loc,
messageId: "unsafeZero",
data: { value },
});
}
}
else if (!/[.e]/iu.test(value)) {
// Intended to be an integer
if (node.value > Number.MAX_SAFE_INTEGER ||
node.value < Number.MIN_SAFE_INTEGER) {
context.report({
loc: node.loc,
messageId: "unsafeInteger",
data: { value },
});
}
}
else {
// Floating point. Check for subnormal.
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setFloat64(0, node.value, false);
const asBigInt = view.getBigUint64(0, false);
// Subnormals have an 11-bit exponent of 0 and a non-zero mantissa.
if ((asBigInt & 0x7ff0000000000000n) === 0n) {
context.report({
loc: node.loc,
messageId: "subnormal",
// Value included so that it's seen in scientific notation
data: {
value,
},
});
}
}
}
},
String(node) {
if (node.value.isWellFormed) {
if (node.value.isWellFormed()) {
return;
}
}
// match any high surrogate and, if it exists, a paired low surrogate
// match any low surrogate not already matched
const surrogatePattern = /[\uD800-\uDBFF][\uDC00-\uDFFF]?|[\uDC00-\uDFFF]/gu;
/** @type {RegExpExecArray | null} */
let match;
while ((match = surrogatePattern.exec(node.value)) !== null) {
// only need to report non-paired surrogates
if (match[0].length < 2) {
context.report({
loc: node.loc,
messageId: "loneSurrogate",
data: {
surrogate: JSON.stringify(match[0]).slice(1, -1),
},
});
}
}
},
};
},
};
export default rule;
+34
View File
@@ -0,0 +1,34 @@
export default rule;
export type SortOptions = {
/**
* Whether key comparisons are case-sensitive.
*/
caseSensitive: boolean;
/**
* Whether to use natural sort order instead of purely alphanumeric.
*/
natural: boolean;
/**
* Minimum number of keys in an object before enforcing sorting.
*/
minKeys: number;
/**
* Whether a blank line between properties starts a new group that is independently sorted.
*/
allowLineSeparatedGroups: boolean;
};
export type SortKeysMessageIds = "sortKeys";
export type SortDirection = "asc" | "desc";
export type SortKeysRuleOptions = [SortDirection, SortOptions];
export type SortKeysRuleDefinition = JSONRuleDefinition<{
RuleOptions: SortKeysRuleOptions;
MessageIds: SortKeysMessageIds;
}>;
export type Comparator = (a: string, b: string) => boolean;
export type DirectionName = "ascending" | "descending";
export type SortName = "alphanumeric" | "natural";
export type Sensitivity = "sensitive" | "insensitive";
export type ComparatorMap = Record<DirectionName, Record<SortName, Record<Sensitivity, Comparator>>>;
/** @type {SortKeysRuleDefinition} */
declare const rule: SortKeysRuleDefinition;
import type { JSONRuleDefinition } from "../types.js";
+221
View File
@@ -0,0 +1,221 @@
/**
* @fileoverview Rule to require JSON object keys to be sorted.
* Copied largely from https://github.com/eslint/eslint/blob/main/lib/rules/sort-keys.js
* @author Robin Thomas
*/
//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
import naturalCompare from "natural-compare";
import { getKey, getRawKey } from "../util.js";
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { JSONRuleDefinition } from "../types.js";
* @import { MemberNode } from "@humanwhocodes/momoa";
* @typedef {Object} SortOptions
* @property {boolean} caseSensitive Whether key comparisons are case-sensitive.
* @property {boolean} natural Whether to use natural sort order instead of purely alphanumeric.
* @property {number} minKeys Minimum number of keys in an object before enforcing sorting.
* @property {boolean} allowLineSeparatedGroups Whether a blank line between properties starts a new group that is independently sorted.
* @typedef {"sortKeys"} SortKeysMessageIds
* @typedef {"asc"|"desc"} SortDirection
* @typedef {[SortDirection, SortOptions]} SortKeysRuleOptions
* @typedef {JSONRuleDefinition<{ RuleOptions: SortKeysRuleOptions, MessageIds: SortKeysMessageIds }>} SortKeysRuleDefinition
* @typedef {(a:string,b:string) => boolean} Comparator
* @typedef {"ascending"|"descending"} DirectionName
* @typedef {"alphanumeric"|"natural"} SortName
* @typedef {"sensitive"|"insensitive"} Sensitivity
* @typedef {Record<DirectionName, Record<SortName, Record<Sensitivity, Comparator>>>} ComparatorMap
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
const hasNonWhitespace = /\S/u;
const commentTypes = new Set(["LineComment", "BlockComment"]);
/** @type {ComparatorMap} */
const comparators = {
ascending: {
alphanumeric: {
sensitive: (a, b) => a <= b,
insensitive: (a, b) => a.toLowerCase() <= b.toLowerCase(),
},
natural: {
sensitive: (a, b) => naturalCompare(a, b) <= 0,
insensitive: (a, b) => naturalCompare(a.toLowerCase(), b.toLowerCase()) <= 0,
},
},
descending: {
alphanumeric: {
sensitive: (a, b) => comparators.ascending.alphanumeric.sensitive(b, a),
insensitive: (a, b) => comparators.ascending.alphanumeric.insensitive(b, a),
},
natural: {
sensitive: (a, b) => comparators.ascending.natural.sensitive(b, a),
insensitive: (a, b) => comparators.ascending.natural.insensitive(b, a),
},
},
};
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
/** @type {SortKeysRuleDefinition} */
const rule = {
meta: {
type: "suggestion",
languages: ["json/json", "json/jsonc", "json/json5"],
fixable: "code",
defaultOptions: [
"asc",
{
allowLineSeparatedGroups: false,
caseSensitive: true,
minKeys: 2,
natural: false,
},
],
docs: {
recommended: false,
description: `Require JSON object keys to be sorted`,
dialects: ["JSON", "JSONC", "JSON5"],
url: "https://github.com/eslint/json/tree/main/docs/rules/sort-keys.md",
},
messages: {
sortKeys: "Expected object keys to be in {{sortName}} case-{{sensitivity}} {{direction}} order. '{{thisName}}' should be before '{{prevName}}'.",
},
schema: [
{
enum: ["asc", "desc"],
},
{
type: "object",
properties: {
caseSensitive: {
type: "boolean",
},
natural: {
type: "boolean",
},
minKeys: {
type: "integer",
minimum: 2,
},
allowLineSeparatedGroups: {
type: "boolean",
},
},
additionalProperties: false,
},
],
},
create(context) {
const { sourceCode } = context;
const [directionShort, { allowLineSeparatedGroups, caseSensitive, natural, minKeys },] = context.options;
/** @type {DirectionName} */
const direction = directionShort === "asc" ? "ascending" : "descending";
/** @type {SortName} */
const sortName = natural ? "natural" : "alphanumeric";
/** @type {Sensitivity} */
const sensitivity = caseSensitive ? "sensitive" : "insensitive";
/** @type {Comparator} */
const isValidOrder = comparators[direction][sortName][sensitivity];
// Note that @humanwhocodes/momoa doesn't include comments in the object.members tree, so we can't just see if a member is preceded by a comment
const commentLineNums = new Set();
for (const comment of sourceCode.comments) {
for (let lineNum = comment.loc.start.line; lineNum <= comment.loc.end.line; lineNum += 1) {
commentLineNums.add(lineNum);
}
}
/**
* Checks if two members are line-separated.
* @param {MemberNode} prevMember The previous member.
* @param {MemberNode} member The current member.
* @returns {boolean} True if the members are separated by at least one blank line (ignoring comment-only lines).
*/
function isLineSeparated(prevMember, member) {
// Note that there can be comments *inside* members, e.g. `{"foo: /* comment *\/ "bar"}`, but these are ignored when calculating line-separated groups
const prevMemberEndLine = prevMember.loc.end.line;
const thisStartLine = member.loc.start.line;
if (thisStartLine - prevMemberEndLine < 2) {
return false;
}
for (let lineNum = prevMemberEndLine + 1; lineNum < thisStartLine; lineNum += 1) {
if (!commentLineNums.has(lineNum) &&
!hasNonWhitespace.test(sourceCode.lines[lineNum - 1])) {
return true;
}
}
return false;
}
/**
* Checks if a member has a comment before or after it.
* @param {MemberNode} member The member to check.
* @returns {boolean} True if a comment is adjacent to the member.
*/
function hasAdjacentComment(member) {
const before = sourceCode.getTokenBefore(member, {
includeComments: true,
});
let after = sourceCode.getTokenAfter(member, {
includeComments: true,
});
if (after.type === "Comma") {
after = sourceCode.getTokenAfter(after, {
includeComments: true,
});
}
return (commentTypes.has(before.type) || commentTypes.has(after.type));
}
return {
Object(node) {
/** @type {MemberNode} */
let prevMember;
/** @type {string} */
let prevName;
/** @type {string} */
let prevRawName;
if (node.members.length < minKeys) {
return;
}
for (const member of node.members) {
const thisName = getKey(member);
const thisRawName = getRawKey(member, sourceCode);
// Capture `prevMember` for this iteration so the fixer closure uses the
// intended node even though `prevMember` is reassigned in the loop.
const prevMemberNode = prevMember;
if (prevMember &&
!isValidOrder(prevName, thisName) &&
(!allowLineSeparatedGroups ||
!isLineSeparated(prevMember, member))) {
context.report({
loc: member.name.loc,
messageId: "sortKeys",
data: {
thisName: thisRawName,
prevName: prevRawName,
direction,
sensitivity,
sortName,
},
fix(fixer) {
if (hasAdjacentComment(member) ||
hasAdjacentComment(prevMemberNode)) {
return null;
}
return [
fixer.replaceText(member, sourceCode.getText(prevMemberNode)),
fixer.replaceText(prevMemberNode, sourceCode.getText(member)),
];
},
});
}
prevMember = member;
prevName = thisName;
prevRawName = thisRawName;
}
},
};
},
};
export default rule;
@@ -0,0 +1,17 @@
export default rule;
export type TopLevelInteropMessageIds = "topLevel";
export type TopLevelInteropRuleDefinition = JSONRuleDefinition<{
MessageIds: TopLevelInteropMessageIds;
}>;
/**
* @fileoverview Rule to ensure top-level items are either an array or object.
* @author Joe Hildebrand
*/
/**
* @import { JSONRuleDefinition } from "../types.js";
* @typedef {"topLevel"} TopLevelInteropMessageIds
* @typedef {JSONRuleDefinition<{ MessageIds: TopLevelInteropMessageIds }>} TopLevelInteropRuleDefinition
*/
/** @type {TopLevelInteropRuleDefinition} */
declare const rule: TopLevelInteropRuleDefinition;
import type { JSONRuleDefinition } from "../types.js";
+46
View File
@@ -0,0 +1,46 @@
/**
* @fileoverview Rule to ensure top-level items are either an array or object.
* @author Joe Hildebrand
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { JSONRuleDefinition } from "../types.js";
* @typedef {"topLevel"} TopLevelInteropMessageIds
* @typedef {JSONRuleDefinition<{ MessageIds: TopLevelInteropMessageIds }>} TopLevelInteropRuleDefinition
*/
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
/** @type {TopLevelInteropRuleDefinition} */
const rule = {
meta: {
type: "problem",
languages: ["json/json", "json/jsonc", "json/json5"],
docs: {
recommended: false,
description: "Require the JSON top-level value to be an array or object",
dialects: ["JSON", "JSONC", "JSON5"],
url: "https://github.com/eslint/json/tree/main/docs/rules/top-level-interop.md",
},
messages: {
topLevel: "Top level item should be array or object, got '{{type}}'.",
},
},
create(context) {
return {
Document(node) {
const { type } = node.body;
if (type !== "Object" && type !== "Array") {
context.report({
loc: node.loc,
messageId: "topLevel",
data: { type },
});
}
},
};
},
};
export default rule;
+39
View File
@@ -0,0 +1,39 @@
/**
* @fileoverview Additional types for this package.
* @author Nicholas C. Zakas
*/
import type { RuleVisitor } from "@eslint/core";
import type { CustomRuleDefinitionType, CustomRuleTypeDefinitions, CustomRuleVisitorWithExit } from "@eslint/plugin-kit";
import type { DocumentNode, MemberNode, ElementNode, ObjectNode, ArrayNode, StringNode, NullNode, NumberNode, BooleanNode, NaNNode, InfinityNode, IdentifierNode, AnyNode, Token } from "@humanwhocodes/momoa";
import type { JSONLanguageOptions, JSONSourceCode } from "./index.js";
type ValueNodeParent = DocumentNode | MemberNode | ElementNode;
/**
* A JSON syntax element, including nodes and tokens.
*/
export type JSONSyntaxElement = Token | AnyNode;
/**
* The visitor format returned from rules in this package.
*/
export interface JSONRuleVisitor extends RuleVisitor, CustomRuleVisitorWithExit<{
Document?(node: DocumentNode): void;
Member?(node: MemberNode, parent?: ObjectNode): void;
Element?(node: ElementNode, parent?: ArrayNode): void;
Object?(node: ObjectNode, parent?: ValueNodeParent): void;
Array?(node: ArrayNode, parent?: ValueNodeParent): void;
String?(node: StringNode, parent?: ValueNodeParent): void;
Null?(node: NullNode, parent?: ValueNodeParent): void;
Number?(node: NumberNode, parent?: ValueNodeParent): void;
Boolean?(node: BooleanNode, parent?: ValueNodeParent): void;
NaN?(node: NaNNode, parent?: ValueNodeParent): void;
Infinity?(node: InfinityNode, parent?: ValueNodeParent): void;
Identifier?(node: IdentifierNode, parent?: ValueNodeParent): void;
}> {
}
export type JSONRuleDefinitionTypeOptions = CustomRuleTypeDefinitions;
export type JSONRuleDefinition<Options extends Partial<JSONRuleDefinitionTypeOptions> = {}> = CustomRuleDefinitionType<{
LangOptions: JSONLanguageOptions;
Code: JSONSourceCode;
Visitor: JSONRuleVisitor;
Node: JSONSyntaxElement;
}, Options>;
export {};
+5
View File
@@ -0,0 +1,5 @@
/**
* @fileoverview Additional types for this package.
* @author Nicholas C. Zakas
*/
export {};
+23
View File
@@ -0,0 +1,23 @@
/**
* @fileoverview Utility Library
* @author 루밀LuMir(lumirlumir)
*/
/**
* @import { MemberNode } from "@humanwhocodes/momoa";
* @import { JSONSourceCode } from "./languages/json-source-code.js";
*/
/**
* Gets the `MemberNode`'s key value.
* @param {MemberNode} node The node to get the key from.
* @returns {string} The key value.
*/
export function getKey(node: MemberNode): string;
/**
* Gets the `MemberNode`'s raw key value.
* @param {MemberNode} node The node to get the raw key from.
* @param {JSONSourceCode} sourceCode The JSON source code object.
* @returns {string} The raw key value.
*/
export function getRawKey(node: MemberNode, sourceCode: JSONSourceCode): string;
import type { MemberNode } from "@humanwhocodes/momoa";
import type { JSONSourceCode } from "./languages/json-source-code.js";
+33
View File
@@ -0,0 +1,33 @@
/**
* @fileoverview Utility Library
* @author 루밀LuMir(lumirlumir)
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { MemberNode } from "@humanwhocodes/momoa";
* @import { JSONSourceCode } from "./languages/json-source-code.js";
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
/**
* Gets the `MemberNode`'s key value.
* @param {MemberNode} node The node to get the key from.
* @returns {string} The key value.
*/
export function getKey(node) {
return node.name.type === "String" ? node.name.value : node.name.name;
}
/**
* Gets the `MemberNode`'s raw key value.
* @param {MemberNode} node The node to get the raw key from.
* @param {JSONSourceCode} sourceCode The JSON source code object.
* @returns {string} The raw key value.
*/
export function getRawKey(node, sourceCode) {
return node.name.type === "String"
? sourceCode.getText(node.name, -1, -1)
: sourceCode.getText(node.name);
}
+100
View File
@@ -0,0 +1,100 @@
{
"name": "@eslint/json",
"version": "2.0.1",
"description": "JSON linting plugin for ESLint",
"author": "Nicholas C. Zakas",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"gitHooks": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.js": [
"eslint --fix",
"prettier --write"
],
"!(*.js)": "prettier --write --ignore-unknown",
"{src/rules/*.js,tools/update-rules-docs.js,README.md}": [
"npm run build:update-rules-docs",
"git add README.md"
]
},
"repository": {
"type": "git",
"url": "git+https://github.com/eslint/json.git"
},
"bugs": {
"url": "https://github.com/eslint/json/issues"
},
"homepage": "https://github.com/eslint/json#readme",
"scripts": {
"build": "npm run build:rules && npm run build:types && npm run build:update-rules-docs",
"build:rules": "node tools/build-rules.js",
"build:types": "tsc",
"build:update-rules-docs": "node tools/update-rules-docs.js",
"prepare": "npm run build",
"pretest": "npm run build",
"lint": "eslint",
"lint:fix": "eslint --fix",
"lint:types": "attw --pack --profile esm-only",
"lint:unused": "knip",
"fmt": "prettier --write .",
"fmt:check": "prettier --check .",
"test": "mocha \"tests/**/*.test.js\"",
"test:coverage": "c8 npm test",
"test:jsr": "npx -y jsr@latest publish --dry-run",
"test:types": "tsc -p tests/types/tsconfig.json",
"test:types:5.3": "npx -p typescript@5.3 -y -- tsc -p tests/types/tsconfig.legacy.json",
"test:types:5.x": "npx -p typescript@5.x -y -- tsc -p tests/types/tsconfig.json",
"test:types:7.x": "npx -p @typescript/native-preview@latest -y -- tsgo -p tests/types/tsconfig.json",
"test:types:all": "npm run test:types && npm run test:types:5.3 && npm run test:types:5.x && npm run test:types:7.x"
},
"keywords": [
"eslint",
"eslint-plugin",
"eslintplugin",
"json",
"linting"
],
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^1.2.1",
"@eslint/plugin-kit": "^0.7.2",
"@humanwhocodes/momoa": "^3.3.10",
"natural-compare": "^1.4.0"
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.18.3",
"@types/natural-compare": "^1.4.3",
"@types/node": "^20.19.0",
"c8": "^11.0.0",
"dedent": "^1.5.3",
"eslint": "^10.4.0",
"eslint-config-eslint": "^14.0.0",
"eslint-plugin-eslint-plugin": "^7.3.2",
"globals": "^17.0.0",
"knip": "^6.0.0",
"lint-staged": "^16.0.0",
"mdast-util-from-markdown": "^2.0.2",
"mocha": "^11.3.0",
"prettier": "3.8.4",
"typescript": "^6.0.2",
"yorkie": "^2.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
}