├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── bug-report.yml
│ ├── change.yml
│ ├── config.yml
│ ├── docs.yml
│ ├── new-rule.yml
│ └── rule-change.yml
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── bun-test.yml
│ ├── ci.yml
│ ├── release-please.yml
│ └── update-readme.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .release-please-manifest.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── eslint.config.js
├── jsr.json
├── package.json
├── prettier.config.js
├── release-please-config.json
├── rollup.config.js
├── src
├── index.js
├── languages
│ ├── json-language.js
│ └── json-source-code.js
├── rules
│ ├── no-duplicate-keys.js
│ ├── no-empty-keys.js
│ ├── no-unnormalized-keys.js
│ ├── no-unsafe-values.js
│ ├── sort-keys.js
│ └── top-level-interop.js
└── types.ts
├── tests
├── languages
│ ├── json-language.test.js
│ └── json-source-code.test.js
├── package
│ └── exports.js
├── plugin
│ └── eslint.test.js
├── rules
│ ├── no-duplicate-keys.test.js
│ ├── no-empty-keys.test.js
│ ├── no-unnormalized-keys.test.js
│ ├── no-unsafe-values.test.js
│ ├── sort-keys.test.js
│ └── top-level-interop.test.js
└── types
│ ├── cjs-import.test.cts
│ ├── tsconfig.json
│ └── types.test.ts
├── tools
├── build-cts.js
├── commit-readme.sh
├── dedupe-types.js
└── update-readme.js
├── tsconfig.esm.json
└── tsconfig.json
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Convert text file line endings to lf
2 | * text=auto
3 |
4 | *.js text eol=lf
5 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report.yml:
--------------------------------------------------------------------------------
1 | name: "\U0001F41E Report a problem"
2 | description: "Report something that isn't working the way you expected."
3 | title: "Bug: (fill in)"
4 | labels:
5 | - bug
6 | - "repro:needed"
7 | body:
8 | - type: markdown
9 | attributes:
10 | value: By opening an issue, you agree to abide by the [OpenJS Foundation Code of Conduct](https://eslint.org/conduct).
11 | - type: textarea
12 | attributes:
13 | label: Environment
14 | description: |
15 | Please tell us about how you're running ESLint (Run `npx eslint --env-info`.)
16 | value: |
17 | ESLint version:
18 | @eslint/json version:
19 | Node version:
20 | npm version:
21 | Operating System:
22 | validations:
23 | required: true
24 | - type: dropdown
25 | attributes:
26 | label: Which language are you using?
27 | description: |
28 | Just tell us which language mode you're using.
29 | options:
30 | - json
31 | - jsonc
32 | validations:
33 | required: true
34 | - type: textarea
35 | attributes:
36 | label: What did you do?
37 | description: |
38 | Please include a *minimal* reproduction case.
39 | value: |
40 |
41 | Configuration
42 |
43 | ```
44 |
45 | ```
46 |
47 |
48 | ```js
49 |
50 | ```
51 | validations:
52 | required: true
53 | - type: textarea
54 | attributes:
55 | label: What did you expect to happen?
56 | validations:
57 | required: true
58 | - type: textarea
59 | attributes:
60 | label: What actually happened?
61 | description: |
62 | Please copy-paste the actual ESLint output.
63 | validations:
64 | required: true
65 | - type: input
66 | attributes:
67 | label: Link to Minimal Reproducible Example
68 | description: "Link to a [StackBlitz](https://stackblitz.com), or GitHub repo with a minimal reproduction of the problem. **A minimal reproduction is required** so that others can help debug your issue. If a report is vague (e.g. just a generic error message) and has no reproduction, it may be auto-closed."
69 | placeholder: "https://stackblitz.com/abcd1234"
70 | validations:
71 | required: true
72 | - type: checkboxes
73 | attributes:
74 | label: Participation
75 | options:
76 | - label: I am willing to submit a pull request for this issue.
77 | required: false
78 | - type: textarea
79 | attributes:
80 | label: Additional comments
81 | description: Is there anything else that's important for the team to know?
82 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/change.yml:
--------------------------------------------------------------------------------
1 | name: "\U0001F680 Request a change (not rule-related)"
2 | description: "Request a change that is not a bug fix, rule change, or new rule"
3 | title: "Change Request: (fill in)"
4 | labels:
5 | - enhancement
6 | - core
7 | body:
8 | - type: markdown
9 | attributes:
10 | value: By opening an issue, you agree to abide by the [OpenJS Foundation Code of Conduct](https://eslint.org/conduct).
11 | - type: textarea
12 | attributes:
13 | label: Environment
14 | description: |
15 | Please tell us about how you're running ESLint (Run `npx eslint --env-info`.)
16 | value: |
17 | ESLint version:
18 | @eslint/json version:
19 | Node version:
20 | npm version:
21 | Operating System:
22 | validations:
23 | required: true
24 | - type: textarea
25 | attributes:
26 | label: What problem do you want to solve?
27 | description: |
28 | Please explain your use case in as much detail as possible.
29 | placeholder: |
30 | The JSON plugin currently...
31 | validations:
32 | required: true
33 | - type: textarea
34 | attributes:
35 | label: What do you think is the correct solution?
36 | description: |
37 | Please explain how you'd like to change the JSON plugin to address the problem.
38 | placeholder: |
39 | I'd like the JSON plugin to...
40 | validations:
41 | required: true
42 | - type: checkboxes
43 | attributes:
44 | label: Participation
45 | options:
46 | - label: I am willing to submit a pull request for this change.
47 | required: false
48 | - type: textarea
49 | attributes:
50 | label: Additional comments
51 | description: Is there anything else that's important for the team to know?
52 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: 🗣 Ask a Question, Discuss
4 | url: https://github.com/eslint/json/discussions
5 | about: Get help using this plugin
6 | - name: Discord Server
7 | url: https://eslint.org/chat
8 | about: Talk with the team
9 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/docs.yml:
--------------------------------------------------------------------------------
1 | name: "\U0001F4DD Docs"
2 | description: "Request an improvement to documentation"
3 | title: "Docs: (fill in)"
4 | labels:
5 | - documentation
6 | body:
7 | - type: markdown
8 | attributes:
9 | value: By opening an issue, you agree to abide by the [OpenJS Foundation Code of Conduct](https://eslint.org/conduct).
10 | - type: textarea
11 | attributes:
12 | label: Docs page(s)
13 | description: |
14 | What page(s) are you suggesting be changed or created?
15 | placeholder: |
16 | e.g. https://eslint.org/docs/latest/use/getting-started
17 | validations:
18 | required: true
19 | - type: textarea
20 | attributes:
21 | label: What documentation issue do you want to solve?
22 | description: |
23 | Please explain your issue in as much detail as possible.
24 | placeholder: |
25 | The docs currently...
26 | validations:
27 | required: true
28 | - type: textarea
29 | attributes:
30 | label: What do you think is the correct solution?
31 | description: |
32 | Please explain how you'd like to change the docs to address the problem.
33 | placeholder: |
34 | I'd like the docs to...
35 | validations:
36 | required: true
37 | - type: checkboxes
38 | attributes:
39 | label: Participation
40 | options:
41 | - label: I am willing to submit a pull request for this change.
42 | required: false
43 | - type: textarea
44 | attributes:
45 | label: Additional comments
46 | description: Is there anything else that's important for the team to know?
47 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/new-rule.yml:
--------------------------------------------------------------------------------
1 | name: "\U0001F680 Propose a new rule"
2 | description: "Propose a new rule to be added to the plugin"
3 | title: "New Rule: (fill in)"
4 | labels:
5 | - rule
6 | - feature
7 | body:
8 | - type: markdown
9 | attributes:
10 | value: By opening an issue, you agree to abide by the [OpenJS Foundation Code of Conduct](https://eslint.org/conduct).
11 | - type: input
12 | attributes:
13 | label: Rule details
14 | description: What should the new rule do?
15 | validations:
16 | required: true
17 | - type: dropdown
18 | attributes:
19 | label: What type of rule is this?
20 | options:
21 | - Warns about a potential problem
22 | - Suggests an alternate way of doing something
23 | validations:
24 | required: true
25 | - type: textarea
26 | attributes:
27 | label: Example code
28 | description: Please provide some example code that this rule will warn about. This field will render as JSON.
29 | render: jsonc
30 | validations:
31 | required: true
32 | - type: checkboxes
33 | attributes:
34 | label: Participation
35 | options:
36 | - label: I am willing to submit a pull request to implement this rule.
37 | required: false
38 | - type: textarea
39 | attributes:
40 | label: Additional comments
41 | description: Is there anything else that's important for the team to know?
42 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/rule-change.yml:
--------------------------------------------------------------------------------
1 | name: "\U0001F4DD Request a rule change"
2 | description: "Request a change to an existing rule"
3 | title: "Rule Change: (fill in)"
4 | labels:
5 | - enhancement
6 | - rule
7 | body:
8 | - type: markdown
9 | attributes:
10 | value: By opening an issue, you agree to abide by the [OpenJS Foundation Code of Conduct](https://eslint.org/conduct).
11 | - type: input
12 | attributes:
13 | label: What rule do you want to change?
14 | validations:
15 | required: true
16 | - type: dropdown
17 | attributes:
18 | label: What change do you want to make?
19 | options:
20 | - Generate more warnings
21 | - Generate fewer warnings
22 | - Implement autofix
23 | - Implement suggestions
24 | validations:
25 | required: true
26 | - type: dropdown
27 | attributes:
28 | label: How do you think the change should be implemented?
29 | options:
30 | - A new option
31 | - A new default behavior
32 | - Other
33 | validations:
34 | required: true
35 | - type: textarea
36 | attributes:
37 | label: Example code
38 | description: Please provide some example code that this change will affect. This field will render as JSON.
39 | render: jsonc
40 | validations:
41 | required: true
42 | - type: textarea
43 | attributes:
44 | label: What does the rule currently do for this code?
45 | validations:
46 | required: true
47 | - type: textarea
48 | attributes:
49 | label: What will the rule do after it's changed?
50 | validations:
51 | required: true
52 | - type: checkboxes
53 | attributes:
54 | label: Participation
55 | options:
56 | - label: I am willing to submit a pull request to implement this change.
57 | required: false
58 | - type: textarea
59 | attributes:
60 | label: Additional comments
61 | description: Is there anything else that's important for the team to know?
62 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | #### Prerequisites checklist
8 |
9 | - [ ] I have read the [contributing guidelines](https://github.com/eslint/eslint/blob/HEAD/CONTRIBUTING.md).
10 |
11 |
19 |
20 |
23 |
24 | #### What is the purpose of this pull request?
25 |
26 | #### What changes did you make? (Give an overview)
27 |
28 | #### Related Issues
29 |
30 |
31 |
32 | #### Is there anything you'd like reviewers to focus on?
33 |
34 |
35 |
--------------------------------------------------------------------------------
/.github/workflows/bun-test.yml:
--------------------------------------------------------------------------------
1 | name: Bun CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | build:
13 | runs-on: ${{ matrix.os }}
14 |
15 | strategy:
16 | matrix:
17 | os: [windows-latest, macOS-latest, ubuntu-latest]
18 | bun: [latest]
19 |
20 | steps:
21 | - uses: actions/checkout@v4
22 | - name: Use Bun ${{ matrix.bun }} ${{ matrix.os }}
23 | uses: oven-sh/setup-bun@v1
24 | with:
25 | bun-version: ${{ matrix.bun }}
26 | - name: bun install, build, and test
27 | run: |
28 | bun install
29 | bun run --bun test
30 | env:
31 | CI: true
32 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 | jobs:
10 | verify_files:
11 | name: Verify Files
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 | - name: Setup Node.js
16 | uses: actions/setup-node@v4
17 | with:
18 | node-version: "lts/*"
19 | - name: Install dependencies
20 | run: npm install
21 | - name: Build
22 | run: npm run build
23 | - name: Lint files
24 | run: npm run lint
25 | - name: Check Formatting
26 | run: npm run fmt:check
27 | test:
28 | name: Test
29 | strategy:
30 | matrix:
31 | os: [ubuntu-latest]
32 | node: [24.x, 22.x, 20.x, 18.x, "18.18.0"]
33 | include:
34 | - os: windows-latest
35 | node: "lts/*"
36 | - os: macOS-latest
37 | node: "lts/*"
38 | runs-on: ${{ matrix.os }}
39 | steps:
40 | - uses: actions/checkout@v4
41 | - uses: actions/setup-node@v4
42 | with:
43 | node-version: ${{ matrix.node }}
44 | - name: Install dependencies
45 | run: npm install
46 | - name: Run tests
47 | run: npm run test
48 | test_types:
49 | name: Test Types
50 | runs-on: ubuntu-latest
51 | steps:
52 | - uses: actions/checkout@v4
53 | - name: Setup Node.js
54 | uses: actions/setup-node@v4
55 | with:
56 | node-version: "lts/*"
57 | - name: Install dependencies
58 | run: npm install
59 | - name: Build
60 | run: npm run build
61 | - name: Check Types
62 | run: npm run test:types
63 | jsr_test:
64 | name: Verify JSR Publish
65 | runs-on: ubuntu-latest
66 | steps:
67 | - uses: actions/checkout@v4
68 | - uses: actions/setup-node@v4
69 | with:
70 | node-version: "lts/*"
71 | - name: Install Packages
72 | run: npm install
73 | - name: Run --dry-run
74 | run: |
75 | npm run build
76 | npm run test:jsr
77 |
--------------------------------------------------------------------------------
/.github/workflows/release-please.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - main
5 | name: release-please
6 | jobs:
7 | release-please:
8 | runs-on: ubuntu-latest
9 | permissions:
10 | contents: write
11 | pull-requests: write
12 | id-token: write
13 | models: read
14 | steps:
15 | - uses: googleapis/release-please-action@v4
16 | id: release
17 | - uses: actions/checkout@v4
18 | if: ${{ steps.release.outputs.release_created }}
19 | - uses: actions/setup-node@v4
20 | with:
21 | node-version: lts/*
22 | registry-url: https://registry.npmjs.org
23 | if: ${{ steps.release.outputs.release_created }}
24 |
25 | - name: Publish to npm
26 | run: |
27 | npm install
28 | npm run build --if-present
29 | npm publish --provenance
30 | env:
31 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
32 | if: ${{ steps.release.outputs.release_created }}
33 |
34 | - name: Publish to JSR
35 | run: |
36 | npm run build --if-present
37 | npx jsr publish
38 | if: ${{ steps.release.outputs.release_created }}
39 |
40 | # Generates the social media post
41 | - run: npx @humanwhocodes/social-changelog --org ${{ github.repository_owner }} --repo ${{ github.event.repository.name }} --tag ${{ steps.release.outputs.tag_name }} > social-post.txt
42 | if: ${{ steps.release.outputs.release_created }}
43 | env:
44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45 |
46 | - name: Post release announcement
47 | run: npx @humanwhocodes/crosspost -t -b -m --discord-webhook --file social-post.txt
48 | if: ${{ steps.release.outputs.release_created }}
49 | env:
50 | TWITTER_API_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }}
51 | TWITTER_API_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }}
52 | TWITTER_ACCESS_TOKEN_KEY: ${{ secrets.TWITTER_ACCESS_TOKEN_KEY }}
53 | TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
54 | MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }}
55 | MASTODON_HOST: ${{ secrets.MASTODON_HOST }}
56 | BLUESKY_IDENTIFIER: ${{ vars.BLUESKY_IDENTIFIER }}
57 | BLUESKY_PASSWORD: ${{ secrets.BLUESKY_PASSWORD }}
58 | BLUESKY_HOST: ${{ vars.BLUESKY_HOST }}
59 | DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
60 |
--------------------------------------------------------------------------------
/.github/workflows/update-readme.yml:
--------------------------------------------------------------------------------
1 | name: Data Fetch
2 |
3 | on:
4 | schedule:
5 | - cron: "0 8 * * *" # Every day at 1am PDT
6 | workflow_dispatch:
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Check out repo
13 | uses: actions/checkout@v4
14 | with:
15 | token: ${{ secrets.WORKFLOW_PUSH_BOT_TOKEN }}
16 |
17 | - name: Set up Node.js
18 | uses: actions/setup-node@v4
19 |
20 | - name: Install npm packages
21 | run: npm install
22 |
23 | - name: Update README with latest sponsor data
24 | run: npm run build:readme
25 |
26 | - name: Setup Git
27 | run: |
28 | git config user.name "GitHub Actions Bot"
29 | git config user.email ""
30 |
31 | - name: Save updated files
32 | run: |
33 | chmod +x ./tools/commit-readme.sh
34 | ./tools/commit-readme.sh
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # yarn v2
11 | .yarn/cache
12 | .yarn/unplugged
13 | .yarn/build-state.yml
14 | .yarn/install-state.gz
15 | .pnp.*
16 |
17 | # Diagnostic reports (https://nodejs.org/api/report.html)
18 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
19 |
20 | # Runtime data
21 | pids
22 | *.pid
23 | *.seed
24 | *.pid.lock
25 |
26 | # Coverage directory used by tools like istanbul
27 | coverage
28 | *.lcov
29 |
30 | # Dependency directories
31 | node_modules/
32 |
33 | # JSR build directory
34 | dist
35 |
36 | # TypeScript cache
37 | *.tsbuildinfo
38 |
39 | # Optional npm cache directory
40 | .npm
41 |
42 | # Optional eslint cache
43 | .eslintcache
44 |
45 | # Optional REPL history
46 | .node_repl_history
47 |
48 | # Output of 'npm pack'
49 | *.tgz
50 |
51 | # dotenv environment variable files
52 | .env
53 | .env.development.local
54 | .env.test.local
55 | .env.production.local
56 | .env.local
57 |
58 | # Editor config files
59 | .vscode
60 | *.code-workspace
61 | .idea
62 | .cursor
63 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock = false
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | CHANGELOG.md
3 | jsr.json
4 |
--------------------------------------------------------------------------------
/.release-please-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | ".": "0.12.0"
3 | }
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [0.12.0](https://github.com/eslint/json/compare/json-v0.11.0...json-v0.12.0) (2025-04-16)
4 |
5 |
6 | ### ⚠ BREAKING CHANGES
7 |
8 | * Update package types for better reuse ([#91](https://github.com/eslint/json/issues/91))
9 |
10 | ### Features
11 |
12 | * Update package types for better reuse ([#91](https://github.com/eslint/json/issues/91)) ([dce4601](https://github.com/eslint/json/commit/dce4601b1a40ceaeb4b61fbcbef0170a67b73e37))
13 |
14 |
15 | ### Bug Fixes
16 |
17 | * Update `types.ts` for compatibility with `verbatimModuleSyntax` ([#88](https://github.com/eslint/json/issues/88)) ([d099c78](https://github.com/eslint/json/commit/d099c78318f8ca9a426d233717728304418425a1))
18 |
19 | ## [0.11.0](https://github.com/eslint/json/compare/json-v0.10.0...json-v0.11.0) (2025-03-14)
20 |
21 |
22 | ### Features
23 |
24 | * Add `types` to exports ([#84](https://github.com/eslint/json/issues/84)) ([d9eab9e](https://github.com/eslint/json/commit/d9eab9ec3a733b561f55235eb71a611c7d84ebbb))
25 |
26 |
27 | ### Bug Fixes
28 |
29 | * Update types and apply to all rules ([#86](https://github.com/eslint/json/issues/86)) ([10882ff](https://github.com/eslint/json/commit/10882ffe9c39cdd866be51801f9950f4a010cd87))
30 | * Use updated types from @eslint/core ([#66](https://github.com/eslint/json/issues/66)) ([460e7c7](https://github.com/eslint/json/commit/460e7c707ed30fc41df280e40f300bafd5a3cae2))
31 |
32 | ## [0.10.0](https://github.com/eslint/json/compare/json-v0.9.1...json-v0.10.0) (2025-01-19)
33 |
34 |
35 | ### Features
36 |
37 | * Add sort-keys rule ([#76](https://github.com/eslint/json/issues/76)) ([e68c247](https://github.com/eslint/json/commit/e68c247be11e1ea3fad20737f3e3672b855bc3ff))
38 |
39 | ## [0.9.1](https://github.com/eslint/json/compare/json-v0.9.0...json-v0.9.1) (2025-01-13)
40 |
41 |
42 | ### Bug Fixes
43 |
44 | * make types usable in CommonJS ([#77](https://github.com/eslint/json/issues/77)) ([41ef891](https://github.com/eslint/json/commit/41ef89142ae40c6b5a4ee1c69d4db406ca5ef529))
45 |
46 | ## [0.9.0](https://github.com/eslint/json/compare/json-v0.8.0...json-v0.9.0) (2024-12-09)
47 |
48 |
49 | ### Features
50 |
51 | * Add top-level-interop rule ([#69](https://github.com/eslint/json/issues/69)) ([af56d6c](https://github.com/eslint/json/commit/af56d6ce6bff9d073aedd7f07c3ec0248ec3b4e9))
52 | * Catch more unsafe numbers ([#71](https://github.com/eslint/json/issues/71)) ([5ffc7c0](https://github.com/eslint/json/commit/5ffc7c0ead359c60a0cb5b2b4fdb522846933853))
53 |
54 | ## [0.8.0](https://github.com/eslint/json/compare/json-v0.7.0...json-v0.8.0) (2024-11-23)
55 |
56 |
57 | ### Features
58 |
59 | * rule no-unnormalized-keys ([#63](https://github.com/eslint/json/issues/63)) ([c57882e](https://github.com/eslint/json/commit/c57882e1c3b51f00b94da4ed9b40d5cf2e4d6847))
60 |
61 |
62 | ### Bug Fixes
63 |
64 | * add type tests ([#65](https://github.com/eslint/json/issues/65)) ([a6c0bc9](https://github.com/eslint/json/commit/a6c0bc9db1e265484c275860fdb41fcfd8aefaf2))
65 | * expose `plugin.configs` in types ([#59](https://github.com/eslint/json/issues/59)) ([1fd0452](https://github.com/eslint/json/commit/1fd0452e97554ec4e696d2105f68df36fbe7f260))
66 |
67 | ## [0.7.0](https://github.com/eslint/json/compare/json-v0.6.0...json-v0.7.0) (2024-11-17)
68 |
69 |
70 | ### Features
71 |
72 | * enable `no-unsafe-values` and add it to recommended configuration ([#51](https://github.com/eslint/json/issues/51)) ([72273f5](https://github.com/eslint/json/commit/72273f5dc0461505989a278a1f16b88d64bc8d7d))
73 | * rule no-unsafe-values ([#30](https://github.com/eslint/json/issues/30)) ([6988e5c](https://github.com/eslint/json/commit/6988e5c1445bbca10b1988ca2d9949b4bc66378c))
74 |
75 |
76 | ### Bug Fixes
77 |
78 | * Upgrade Momoa to fix parsing errors ([#50](https://github.com/eslint/json/issues/50)) ([3723a07](https://github.com/eslint/json/commit/3723a071a3bae296d2dbe66684b9d62832f099ad))
79 |
80 | ## [0.6.0](https://github.com/eslint/json/compare/json-v0.5.0...json-v0.6.0) (2024-10-31)
81 |
82 |
83 | ### Features
84 |
85 | * Add allowTrailingCommas option for JSONC ([#42](https://github.com/eslint/json/issues/42)) ([c94953b](https://github.com/eslint/json/commit/c94953b702a1d9c0c48249f1bda727e2130841c8))
86 |
87 | ## [0.5.0](https://github.com/eslint/json/compare/json-v0.4.1...json-v0.5.0) (2024-10-02)
88 |
89 |
90 | ### Features
91 |
92 | * Add support for config comments ([#27](https://github.com/eslint/json/issues/27)) ([723ddf4](https://github.com/eslint/json/commit/723ddf4cc2593ce0469231a76f6dcf4dfb58c3e3))
93 |
94 | ## [0.4.1](https://github.com/eslint/json/compare/json-v0.4.0...json-v0.4.1) (2024-09-09)
95 |
96 |
97 | ### Bug Fixes
98 |
99 | * Don't require ESLint ([#25](https://github.com/eslint/json/issues/25)) ([dd112e1](https://github.com/eslint/json/commit/dd112e1ccf514a87a68d5068882ec7393aa6dd9b))
100 |
101 | ## [0.4.0](https://github.com/eslint/json/compare/json-v0.3.0...json-v0.4.0) (2024-08-20)
102 |
103 |
104 | ### Features
105 |
106 | * Export internal constructs for other plugin authors ([#17](https://github.com/eslint/json/issues/17)) ([ad729f0](https://github.com/eslint/json/commit/ad729f0c60d42a84b2c87da52a6d2456b5211b48))
107 |
108 | ## [0.3.0](https://github.com/eslint/json/compare/json-v0.2.0...json-v0.3.0) (2024-07-25)
109 |
110 |
111 | ### Features
112 |
113 | * Add JSON5 support ([#15](https://github.com/eslint/json/issues/15)) ([ea8dbb5](https://github.com/eslint/json/commit/ea8dbb53e1aa54dc9a6027393109c2988a3209f5))
114 |
115 | ## [0.2.0](https://github.com/eslint/json/compare/json-v0.1.0...json-v0.2.0) (2024-07-22)
116 |
117 |
118 | ### Features
119 |
120 | * Add getLoc and getRange to JSONSourceCode ([#13](https://github.com/eslint/json/issues/13)) ([2225f63](https://github.com/eslint/json/commit/2225f630284b601d4cfc4ecc19148121d6e11a3f))
121 | * Add meta info to plugin ([#12](https://github.com/eslint/json/issues/12)) ([f419757](https://github.com/eslint/json/commit/f419757b837fce5e37b29a2afe0b2885590ca8bd))
122 |
123 | ## [0.1.0](https://github.com/eslint/json/compare/json-v0.0.1...json-v0.1.0) (2024-07-06)
124 |
125 |
126 | ### Features
127 |
128 | * Add JSON plugin ([#1](https://github.com/eslint/json/issues/1)) ([1976f2c](https://github.com/eslint/json/commit/1976f2c48b1da0cfba2d5ad2553f76182c147621))
129 | * Add JSONLanguage#visitorKeys ([#4](https://github.com/eslint/json/issues/4)) ([d8afca4](https://github.com/eslint/json/commit/d8afca4fe72ae025c0acec523c0d6d9d9aaa5a49))
130 |
131 |
132 | ### Bug Fixes
133 |
134 | * release-please (again) ([#7](https://github.com/eslint/json/issues/7)) ([5ef7c6c](https://github.com/eslint/json/commit/5ef7c6c642f92912328e20bb2cb6b055c302f034))
135 | * Set initial release version in release-please-config.json ([#5](https://github.com/eslint/json/issues/5)) ([64f4db5](https://github.com/eslint/json/commit/64f4db5e68ab01be6acc9aad9b389bda256126a5))
136 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ESLint JSON Language Plugin
2 |
3 | ## Overview
4 |
5 | This package contains a plugin that allows you to natively lint JSON and JSONC files using ESLint.
6 |
7 | **Important:** This plugin requires ESLint v9.6.0 or higher and you must be using the [new configuration system](https://eslint.org/docs/latest/use/configure/configuration-files).
8 |
9 | ## Installation
10 |
11 | For Node.js and compatible runtimes:
12 |
13 | ```shell
14 | npm install @eslint/json -D
15 | # or
16 | yarn add @eslint/json -D
17 | # or
18 | pnpm install @eslint/json -D
19 | # or
20 | bun install @eslint/json -D
21 | ```
22 |
23 | For Deno:
24 |
25 | ```shell
26 | deno add @eslint/json
27 | ```
28 |
29 | ## Usage
30 |
31 | This package exports these languages:
32 |
33 | - `"json/json"` is for regular JSON files
34 | - `"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 configuration files
35 | - `"json/json5"` is for [JSON5](https://json5.org) files
36 |
37 | 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:
38 |
39 | ```js
40 | import { defineConfig } from "eslint/config";
41 | import json from "@eslint/json";
42 |
43 | export default defineConfig([
44 | {
45 | plugins: {
46 | json,
47 | },
48 | },
49 |
50 | // lint JSON files
51 | {
52 | files: ["**/*.json"],
53 | language: "json/json",
54 | rules: {
55 | "json/no-duplicate-keys": "error",
56 | },
57 | },
58 |
59 | // lint JSONC files
60 | {
61 | files: ["**/*.jsonc", ".vscode/*.json"],
62 | language: "json/jsonc",
63 | rules: {
64 | "json/no-duplicate-keys": "error",
65 | },
66 | },
67 |
68 | // lint JSON5 files
69 | {
70 | files: ["**/*.json5"],
71 | language: "json/json5",
72 | rules: {
73 | "json/no-duplicate-keys": "error",
74 | },
75 | },
76 | ]);
77 | ```
78 |
79 | In CommonJS format:
80 |
81 | ```js
82 | const { defineConfig } = require("eslint/config");
83 | const json = require("@eslint/json").default;
84 |
85 | module.exports = defineConfig([
86 | {
87 | plugins: {
88 | json,
89 | },
90 | },
91 |
92 | // lint JSON files
93 | {
94 | files: ["**/*.json"],
95 | language: "json/json",
96 | rules: {
97 | "json/no-duplicate-keys": "error",
98 | },
99 | },
100 |
101 | // lint JSONC files
102 | {
103 | files: ["**/*.jsonc", ".vscode/*.json"],
104 | language: "json/jsonc",
105 | rules: {
106 | "json/no-duplicate-keys": "error",
107 | },
108 | },
109 |
110 | // lint JSON5 files
111 | {
112 | files: ["**/*.json5"],
113 | language: "json/json5",
114 | rules: {
115 | "json/no-duplicate-keys": "error",
116 | },
117 | },
118 | ]);
119 | ```
120 |
121 | ## Recommended Configuration
122 |
123 | To use the recommended configuration for this plugin, specify your matching `files` and then use the `extends: ["json/recommended"]` property, like this:
124 |
125 | ```js
126 | import { defineConfig } from "eslint/config";
127 | import json from "@eslint/json";
128 |
129 | export default defineConfig([
130 | // lint JSON files
131 | {
132 | files: ["**/*.json"],
133 | ignores: ["package-lock.json"],
134 | plugins: { json },
135 | language: "json/json",
136 | extends: ["json/recommended"],
137 | },
138 |
139 | // lint JSONC files
140 | {
141 | files: ["**/*.jsonc"],
142 | plugins: { json },
143 | language: "json/jsonc",
144 | extends: ["json/recommended"],
145 | },
146 |
147 | // lint JSON5 files
148 | {
149 | files: ["**/*.json5"],
150 | plugins: { json },
151 | language: "json/json5",
152 | extends: ["json/recommended"],
153 | },
154 | ]);
155 | ```
156 |
157 | **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.
158 |
159 | ## Rules
160 |
161 | - `no-duplicate-keys` - warns when there are two keys in an object with the same text.
162 | - `no-empty-keys` - warns when there is a key in an object that is an empty string or contains only whitespace (note: `package-lock.json` uses empty keys intentionally)
163 | - `no-unsafe-values` - warns on values that are unsafe for interchange, such
164 | as strings with unmatched
165 | [surrogates](https://en.wikipedia.org/wiki/UTF-16), numbers that evaluate to
166 | Infinity, numbers that evaluate to zero unintentionally, numbers that look
167 | like integers but are too large, and
168 | [subnormal numbers](https://en.wikipedia.org/wiki/Subnormal_number).
169 | - `no-unnormalized-keys` - warns on keys containing [unnormalized characters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize#description). You can optionally specify the normalization form via `{ form: "form_name" }`, where `form_name` can be any of `"NFC"`, `"NFD"`, `"NFKC"`, or `"NFKD"`.
170 | - `sort-keys` - warns when keys are not in the specified order. Based on the ESLint [`sort-keys`](https://eslint.org/docs/latest/rules/sort-keys) rule.
171 | - `top-level-interop` - warns when the top-level item in the document is neither an array nor an object. This can be enabled to ensure maximal interoperability with the oldest JSON parsers.
172 |
173 | ## Configuration Comments
174 |
175 | 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).
176 |
177 | ```jsonc
178 | /* eslint json/no-empty-keys: "error" */
179 |
180 | {
181 | "foo": {
182 | "": 1, // eslint-disable-line json/no-empty-keys -- We want an empty key here
183 | },
184 | "bar": {
185 | // eslint-disable-next-line json/no-empty-keys -- We want an empty key here too
186 | "": 2,
187 | },
188 | /* eslint-disable json/no-empty-keys -- Empty keys are allowed in the following code as well */
189 | "baz": [
190 | {
191 | "": 3,
192 | },
193 | {
194 | "": 4,
195 | },
196 | ],
197 | /* eslint-enable json/no-empty-keys -- re-enable now */
198 | }
199 | ```
200 |
201 | Both line and block comments can be used for all kinds of configuration comments.
202 |
203 | ## Allowing trailing commas in JSONC
204 |
205 | 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:
206 |
207 | ```js
208 | import { defineConfig } from "eslint/config";
209 | import json from "@eslint/json";
210 |
211 | export default defineConfig([
212 | // lint JSONC files
213 | {
214 | files: ["**/*.jsonc"],
215 | plugins: { json },
216 | language: "json/jsonc",
217 | extends: ["json/recommended"],
218 | },
219 |
220 | // lint JSONC files and allow trailing commas
221 | {
222 | files: ["**/tsconfig.json", ".vscode/*.json"],
223 | plugins: { json },
224 | language: "json/jsonc",
225 | languageOptions: {
226 | allowTrailingCommas: true,
227 | },
228 | extends: ["json/recommended"],
229 | },
230 | ]);
231 | ```
232 |
233 | **Note:** The `allowTrailingCommas` option is only valid for the `json/jsonc` language.
234 |
235 | ## Frequently Asked Questions
236 |
237 | ### How does this relate to `eslint-plugin-json` and `eslint-plugin-jsonc`?
238 |
239 | 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:
240 |
241 | - [`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.
242 | - [`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.
243 |
244 | 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.
245 |
246 | ### What about missing rules that are available in `eslint-plugin-json` and `eslint-plugin-jsonc`?
247 |
248 | Most of the rules in `eslint-plugin-json` are actually syntax errors that are caught automatically by the parser used in this plugin.
249 |
250 | 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.
251 |
252 | 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.
253 |
254 | ## Editor and IDE Setup
255 |
256 | ### Visual Studio Code
257 |
258 | First, ensure you have the [ESLint plugin](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) installed.
259 |
260 | Then, edit `eslint.validate` in your `settings.json` file to include `json`, `jsonc`, and `json5`:
261 |
262 | ```json
263 | {
264 | "eslint.validate": ["json", "jsonc", "json5"]
265 | }
266 | ```
267 |
268 | ### JetBrains WebStorm
269 |
270 | 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:
271 |
272 | ```text
273 | **/*.{js,ts,jsx,tsx,cjs,cts,mjs,mts,html,vue,json,jsonc,json5}
274 | ```
275 |
276 | ## License
277 |
278 | Apache 2.0
279 |
280 |
281 |
282 |
283 | ## Sponsors
284 |
285 | The following companies, organizations, and individuals support ESLint's ongoing maintenance and development. [Become a Sponsor](https://eslint.org/donate)
286 | to get your logo on our READMEs and [website](https://eslint.org/sponsors).
287 |
288 | Diamond Sponsors
289 |
Platinum Sponsors
290 |
Gold Sponsors
291 |
Silver Sponsors
292 |
Bronze Sponsors
293 |
294 | Technology Sponsors
295 | 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.
296 |
297 |
298 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview ESLint configuration file.
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Imports
8 | //-----------------------------------------------------------------------------
9 |
10 | import eslintConfigESLint from "eslint-config-eslint";
11 | import eslintPlugin from "eslint-plugin-eslint-plugin";
12 | import json from "./src/index.js";
13 | import { defineConfig, globalIgnores } from "eslint/config";
14 |
15 | //-----------------------------------------------------------------------------
16 | // Helpers
17 | //-----------------------------------------------------------------------------
18 |
19 | const eslintPluginJSDoc = eslintConfigESLint.find(
20 | config => config.plugins?.jsdoc,
21 | ).plugins.jsdoc;
22 |
23 | const eslintPluginRulesRecommendedConfig =
24 | eslintPlugin.configs["flat/rules-recommended"];
25 | const eslintPluginTestsRecommendedConfig =
26 | eslintPlugin.configs["flat/tests-recommended"];
27 |
28 | //-----------------------------------------------------------------------------
29 | // Configuration
30 | //-----------------------------------------------------------------------------
31 |
32 | export default defineConfig([
33 | globalIgnores(["**/tests/fixtures/", "**/dist/"]),
34 |
35 | ...eslintConfigESLint.map(config => ({
36 | files: ["**/*.js"],
37 | ...config,
38 | })),
39 | {
40 | plugins: { json },
41 | files: ["**/*.json"],
42 | ignores: ["**/package-lock.json"],
43 | language: "json/json",
44 | extends: ["json/recommended"],
45 | },
46 | {
47 | files: ["**/*.js"],
48 | rules: {
49 | // disable rules we don't want to use from eslint-config-eslint
50 | "no-undefined": "off",
51 |
52 | // TODO: re-enable eslint-plugin-jsdoc rules
53 | ...Object.fromEntries(
54 | Object.keys(eslintPluginJSDoc.rules).map(name => [
55 | `jsdoc/${name}`,
56 | "off",
57 | ]),
58 | ),
59 | },
60 | },
61 | {
62 | files: ["**/tests/**"],
63 | languageOptions: {
64 | globals: {
65 | describe: "readonly",
66 | xdescribe: "readonly",
67 | it: "readonly",
68 | xit: "readonly",
69 | beforeEach: "readonly",
70 | afterEach: "readonly",
71 | before: "readonly",
72 | after: "readonly",
73 | },
74 | },
75 | },
76 | {
77 | files: ["src/rules/*.js"],
78 | extends: [eslintPluginRulesRecommendedConfig],
79 | rules: {
80 | "eslint-plugin/require-meta-schema": "off", // `schema` defaults to []
81 | "eslint-plugin/prefer-placeholders": "error",
82 | "eslint-plugin/prefer-replace-text": "error",
83 | "eslint-plugin/report-message-format": ["error", "[^a-z].*\\.$"],
84 | "eslint-plugin/require-meta-docs-description": [
85 | "error",
86 | { pattern: "^(Enforce|Require|Disallow) .+[^. ]$" },
87 | ],
88 | "eslint-plugin/require-meta-docs-url": [
89 | "error",
90 | {
91 | pattern: "https://github.com/eslint/json#rules",
92 | },
93 | ],
94 | },
95 | },
96 | {
97 | files: ["tests/rules/*.test.js"],
98 | extends: [eslintPluginTestsRecommendedConfig],
99 | rules: {
100 | "eslint-plugin/test-case-property-ordering": [
101 | "error",
102 | [
103 | "name",
104 | "filename",
105 | "code",
106 | "output",
107 | "language",
108 | "options",
109 | "languageOptions",
110 | "errors",
111 | ],
112 | ],
113 | "eslint-plugin/test-case-shorthand-strings": "error",
114 | },
115 | },
116 | ]);
117 |
--------------------------------------------------------------------------------
/jsr.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@eslint/json",
3 | "version": "0.12.0",
4 | "exports": "./dist/esm/index.js",
5 | "publish": {
6 | "include": [
7 | "dist/esm/index.js",
8 | "dist/esm/index.d.ts",
9 | "dist/esm/types.ts",
10 | "README.md",
11 | "jsr.json",
12 | "LICENSE"
13 | ]
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@eslint/json",
3 | "version": "0.12.0",
4 | "description": "JSON linting plugin for ESLint",
5 | "author": "Nicholas C. Zakas",
6 | "type": "module",
7 | "main": "dist/esm/index.js",
8 | "types": "dist/esm/index.d.ts",
9 | "exports": {
10 | ".": {
11 | "require": {
12 | "types": "./dist/cjs/index.d.cts",
13 | "default": "./dist/cjs/index.cjs"
14 | },
15 | "import": {
16 | "types": "./dist/esm/index.d.ts",
17 | "default": "./dist/esm/index.js"
18 | }
19 | },
20 | "./types": {
21 | "require": {
22 | "types": "./dist/cjs/types.cts"
23 | },
24 | "import": {
25 | "types": "./dist/esm/types.d.ts"
26 | }
27 | }
28 | },
29 | "files": [
30 | "dist"
31 | ],
32 | "publishConfig": {
33 | "access": "public"
34 | },
35 | "gitHooks": {
36 | "pre-commit": "lint-staged"
37 | },
38 | "lint-staged": {
39 | "*.js": [
40 | "eslint --fix",
41 | "prettier --write"
42 | ],
43 | "!(*.js)": "prettier --write --ignore-unknown"
44 | },
45 | "repository": {
46 | "type": "git",
47 | "url": "git+https://github.com/eslint/json.git"
48 | },
49 | "bugs": {
50 | "url": "https://github.com/eslint/json/issues"
51 | },
52 | "homepage": "https://github.com/eslint/json#readme",
53 | "scripts": {
54 | "build:dedupe-types": "node tools/dedupe-types.js dist/cjs/index.cjs dist/esm/index.js",
55 | "build:cts": "node tools/build-cts.js",
56 | "build": "rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json && npm run build:cts",
57 | "build:readme": "node tools/update-readme.js",
58 | "test:jsr": "npx jsr@latest publish --dry-run",
59 | "pretest": "npm run build",
60 | "lint": "eslint",
61 | "fmt": "prettier --write .",
62 | "fmt:check": "prettier --check .",
63 | "test": "mocha tests/**/*.js",
64 | "test:coverage": "c8 npm test",
65 | "test:types": "tsc -p tests/types/tsconfig.json"
66 | },
67 | "keywords": [
68 | "eslint",
69 | "eslint-plugin",
70 | "eslintplugin",
71 | "json",
72 | "linting"
73 | ],
74 | "license": "Apache-2.0",
75 | "dependencies": {
76 | "@eslint/core": "^0.14.0",
77 | "@eslint/plugin-kit": "^0.3.1",
78 | "@humanwhocodes/momoa": "^3.3.4",
79 | "natural-compare": "^1.4.0"
80 | },
81 | "devDependencies": {
82 | "c8": "^10.1.3",
83 | "dedent": "^1.5.3",
84 | "eslint": "^9.25.1",
85 | "eslint-config-eslint": "^11.0.0",
86 | "eslint-plugin-eslint-plugin": "^6.3.2",
87 | "got": "^14.4.2",
88 | "lint-staged": "^15.2.7",
89 | "mocha": "^11.3.0",
90 | "prettier": "^3.4.1",
91 | "rollup": "^4.41.0",
92 | "rollup-plugin-copy": "^3.5.0",
93 | "rollup-plugin-delete": "^3.0.1",
94 | "typescript": "^5.8.3",
95 | "yorkie": "^2.0.0"
96 | },
97 | "engines": {
98 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | useTabs: true,
3 | tabWidth: 4,
4 | arrowParens: "avoid",
5 |
6 | overrides: [
7 | {
8 | files: ["*.json"],
9 | options: {
10 | tabWidth: 2,
11 | useTabs: false,
12 | },
13 | },
14 | ],
15 | };
16 |
--------------------------------------------------------------------------------
/release-please-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "bootstrap-sha": "505795a1312e90f1ce3b59b530622929bc38b4fe",
3 | "bump-minor-pre-major": true,
4 | "packages": {
5 | ".": {
6 | "release-type": "node",
7 | "pull-request-title-pattern": "chore: release ${version} 🚀",
8 | "extra-files": [
9 | {
10 | "type": "json",
11 | "path": "jsr.json",
12 | "jsonpath": "$.version"
13 | },
14 | "src/index.js"
15 | ]
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import copy from "rollup-plugin-copy";
2 | import del from "rollup-plugin-delete";
3 |
4 | export default {
5 | input: "src/index.js",
6 | output: [
7 | {
8 | file: "dist/cjs/index.cjs",
9 | format: "cjs",
10 | },
11 | {
12 | file: "dist/esm/index.js",
13 | format: "esm",
14 | banner: '// @ts-self-types="./index.d.ts"',
15 | },
16 | ],
17 | plugins: [
18 | del({ targets: "dist/*" }),
19 | copy({
20 | targets: [
21 | { src: "src/types.ts", dest: "dist/cjs", rename: "types.cts" },
22 | { src: "src/types.ts", dest: "dist/esm" },
23 | ],
24 | }),
25 | ],
26 | };
27 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview JSON plugin.
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Imports
8 | //-----------------------------------------------------------------------------
9 |
10 | import { JSONLanguage } from "./languages/json-language.js";
11 | import { JSONSourceCode } from "./languages/json-source-code.js";
12 | import noDuplicateKeys from "./rules/no-duplicate-keys.js";
13 | import noEmptyKeys from "./rules/no-empty-keys.js";
14 | import noUnsafeValues from "./rules/no-unsafe-values.js";
15 | import noUnnormalizedKeys from "./rules/no-unnormalized-keys.js";
16 | import sortKeys from "./rules/sort-keys.js";
17 | import topLevelInterop from "./rules/top-level-interop.js";
18 |
19 | //-----------------------------------------------------------------------------
20 | // Plugin
21 | //-----------------------------------------------------------------------------
22 |
23 | const plugin = {
24 | meta: {
25 | name: "@eslint/json",
26 | version: "0.12.0", // x-release-please-version
27 | },
28 | languages: {
29 | json: new JSONLanguage({ mode: "json" }),
30 | jsonc: new JSONLanguage({ mode: "jsonc" }),
31 | json5: new JSONLanguage({ mode: "json5" }),
32 | },
33 | rules: {
34 | "no-duplicate-keys": noDuplicateKeys,
35 | "no-empty-keys": noEmptyKeys,
36 | "no-unsafe-values": noUnsafeValues,
37 | "no-unnormalized-keys": noUnnormalizedKeys,
38 | "sort-keys": sortKeys,
39 | "top-level-interop": topLevelInterop,
40 | },
41 | configs: {
42 | recommended: {
43 | plugins: {},
44 | rules: /** @type {const} */ ({
45 | "json/no-duplicate-keys": "error",
46 | "json/no-empty-keys": "error",
47 | "json/no-unsafe-values": "error",
48 | "json/no-unnormalized-keys": "error",
49 | }),
50 | },
51 | },
52 | };
53 |
54 | // eslint-disable-next-line no-lone-blocks -- The block syntax { ... } ensures that TypeScript does not get confused about the type of `plugin`.
55 | {
56 | plugin.configs.recommended.plugins.json = plugin;
57 | }
58 |
59 | export default plugin;
60 | export { JSONSourceCode };
61 | export * from "./languages/json-language.js";
62 |
--------------------------------------------------------------------------------
/src/languages/json-language.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @filedescription Functions to fix up rules to provide missing methods on the `context` object.
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //------------------------------------------------------------------------------
7 | // Imports
8 | //------------------------------------------------------------------------------
9 |
10 | import { parse } from "@humanwhocodes/momoa";
11 | import { JSONSourceCode } from "./json-source-code.js";
12 | import { visitorKeys } from "@humanwhocodes/momoa";
13 |
14 | //-----------------------------------------------------------------------------
15 | // Types
16 | //-----------------------------------------------------------------------------
17 |
18 | /**
19 | * @import { DocumentNode, AnyNode } from "@humanwhocodes/momoa";
20 | * @import { Language, OkParseResult, ParseResult, File } from "@eslint/core";
21 | *
22 | * @typedef {OkParseResult} JSONOkParseResult
23 | * @typedef {ParseResult} JSONParseResult
24 | *
25 | * @typedef {Object} JSONLanguageOptions
26 | * @property {boolean} [allowTrailingCommas] Whether to allow trailing commas in JSONC mode.
27 | */
28 |
29 | //-----------------------------------------------------------------------------
30 | // Exports
31 | //-----------------------------------------------------------------------------
32 |
33 | /**
34 | * JSON Language Object
35 | * @implements {Language<{ LangOptions: JSONLanguageOptions; Code: JSONSourceCode; RootNode: DocumentNode; Node: AnyNode }>}
36 | */
37 | export class JSONLanguage {
38 | /**
39 | * The type of file to read.
40 | * @type {"text"}
41 | */
42 | fileType = "text";
43 |
44 | /**
45 | * The line number at which the parser starts counting.
46 | * @type {0|1}
47 | */
48 | lineStart = 1;
49 |
50 | /**
51 | * The column number at which the parser starts counting.
52 | * @type {0|1}
53 | */
54 | columnStart = 1;
55 |
56 | /**
57 | * The name of the key that holds the type of the node.
58 | * @type {string}
59 | */
60 | nodeTypeKey = "type";
61 |
62 | /**
63 | * The parser mode.
64 | * @type {"json"|"jsonc"|"json5"}
65 | */
66 | #mode = "json";
67 |
68 | /**
69 | * The visitor keys.
70 | * @type {Record}
71 | */
72 | visitorKeys = Object.fromEntries([...visitorKeys]);
73 |
74 | /**
75 | * Creates a new instance.
76 | * @param {Object} options The options to use for this instance.
77 | * @param {"json"|"jsonc"|"json5"} options.mode The parser mode to use.
78 | */
79 | constructor({ mode }) {
80 | this.#mode = mode;
81 | }
82 |
83 | /**
84 | * Validates the language options.
85 | * @param {JSONLanguageOptions} languageOptions The language options to validate.
86 | * @returns {void}
87 | * @throws {Error} When the language options are invalid.
88 | */
89 | validateLanguageOptions(languageOptions) {
90 | if (languageOptions.allowTrailingCommas !== undefined) {
91 | if (typeof languageOptions.allowTrailingCommas !== "boolean") {
92 | throw new Error(
93 | "allowTrailingCommas must be a boolean if provided.",
94 | );
95 | }
96 |
97 | // we know that allowTrailingCommas is a boolean here
98 |
99 | // only allowed in JSONC mode
100 | if (this.#mode !== "jsonc") {
101 | throw new Error(
102 | "allowTrailingCommas option is only available in JSONC.",
103 | );
104 | }
105 | }
106 | }
107 |
108 | /**
109 | * Parses the given file into an AST.
110 | * @param {File} file The virtual file to parse.
111 | * @param {{languageOptions: JSONLanguageOptions}} context The options to use for parsing.
112 | * @returns {JSONParseResult} The result of parsing.
113 | */
114 | parse(file, context) {
115 | // Note: BOM already removed
116 | const text = /** @type {string} */ (file.body);
117 | const allowTrailingCommas =
118 | context?.languageOptions?.allowTrailingCommas;
119 |
120 | /*
121 | * Check for parsing errors first. If there's a parsing error, nothing
122 | * else can happen. However, a parsing error does not throw an error
123 | * from this method - it's just considered a fatal error message, a
124 | * problem that ESLint identified just like any other.
125 | */
126 | try {
127 | const root = parse(text, {
128 | mode: this.#mode,
129 | ranges: true,
130 | tokens: true,
131 | allowTrailingCommas,
132 | });
133 |
134 | return {
135 | ok: true,
136 | ast: root,
137 | };
138 | } catch (ex) {
139 | // error messages end with (line:column) so we strip that off for ESLint
140 | const message = ex.message
141 | .slice(0, ex.message.lastIndexOf("("))
142 | .trim();
143 |
144 | return {
145 | ok: false,
146 | errors: [
147 | {
148 | ...ex,
149 | message,
150 | },
151 | ],
152 | };
153 | }
154 | }
155 |
156 | /* eslint-disable class-methods-use-this -- Required to complete interface. */
157 | /**
158 | * Creates a new `JSONSourceCode` object from the given information.
159 | * @param {File} file The virtual file to create a `JSONSourceCode` object from.
160 | * @param {JSONOkParseResult} parseResult The result returned from `parse()`.
161 | * @returns {JSONSourceCode} The new `JSONSourceCode` object.
162 | */
163 | createSourceCode(file, parseResult) {
164 | return new JSONSourceCode({
165 | text: /** @type {string} */ (file.body),
166 | ast: parseResult.ast,
167 | });
168 | }
169 | /* eslint-enable class-methods-use-this -- Required to complete interface. */
170 | }
171 |
--------------------------------------------------------------------------------
/src/languages/json-source-code.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview The JSONSourceCode class.
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Imports
8 | //-----------------------------------------------------------------------------
9 |
10 | import { iterator } from "@humanwhocodes/momoa";
11 | import {
12 | VisitNodeStep,
13 | TextSourceCodeBase,
14 | ConfigCommentParser,
15 | Directive,
16 | } from "@eslint/plugin-kit";
17 |
18 | //-----------------------------------------------------------------------------
19 | // Types
20 | //-----------------------------------------------------------------------------
21 |
22 | /**
23 | * @import { DocumentNode, AnyNode, Token } from "@humanwhocodes/momoa";
24 | * @import { SourceLocation, FileProblem, DirectiveType, RulesConfig } from "@eslint/core";
25 | * @import { JSONSyntaxElement } from "../types.ts";
26 | * @import { JSONLanguageOptions } from "./json-language.js";
27 | */
28 |
29 | //-----------------------------------------------------------------------------
30 | // Helpers
31 | //-----------------------------------------------------------------------------
32 |
33 | const commentParser = new ConfigCommentParser();
34 |
35 | const INLINE_CONFIG =
36 | /^\s*(?:eslint(?:-enable|-disable(?:(?:-next)?-line)?)?)(?:\s|$)/u;
37 |
38 | /**
39 | * A class to represent a step in the traversal process.
40 | */
41 | class JSONTraversalStep extends VisitNodeStep {
42 | /**
43 | * The target of the step.
44 | * @type {AnyNode}
45 | */
46 | target = undefined;
47 |
48 | /**
49 | * Creates a new instance.
50 | * @param {Object} options The options for the step.
51 | * @param {AnyNode} options.target The target of the step.
52 | * @param {1|2} options.phase The phase of the step.
53 | * @param {Array} options.args The arguments of the step.
54 | */
55 | constructor({ target, phase, args }) {
56 | super({ target, phase, args });
57 |
58 | this.target = target;
59 | }
60 | }
61 |
62 | //-----------------------------------------------------------------------------
63 | // Exports
64 | //-----------------------------------------------------------------------------
65 |
66 | /**
67 | * JSON Source Code Object
68 | * @extends {TextSourceCodeBase<{LangOptions: JSONLanguageOptions, RootNode: DocumentNode, SyntaxElementWithLoc: JSONSyntaxElement, ConfigNode: Token}>}
69 | */
70 | export class JSONSourceCode extends TextSourceCodeBase {
71 | /**
72 | * Cached traversal steps.
73 | * @type {Array|undefined}
74 | */
75 | #steps;
76 |
77 | /**
78 | * Cache of parent nodes.
79 | * @type {WeakMap}
80 | */
81 | #parents = new WeakMap();
82 |
83 | /**
84 | * Collection of inline configuration comments.
85 | * @type {Array}
86 | */
87 | #inlineConfigComments;
88 |
89 | /**
90 | * The AST of the source code.
91 | * @type {DocumentNode}
92 | */
93 | ast = undefined;
94 |
95 | /**
96 | * The comment node in the source code.
97 | * @type {Array|undefined}
98 | */
99 | comments;
100 |
101 | /**
102 | * Creates a new instance.
103 | * @param {Object} options The options for the instance.
104 | * @param {string} options.text The source code text.
105 | * @param {DocumentNode} options.ast The root AST node.
106 | */
107 | constructor({ text, ast }) {
108 | super({ text, ast });
109 | this.ast = ast;
110 | this.comments = ast.tokens
111 | ? ast.tokens.filter(token => token.type.endsWith("Comment"))
112 | : [];
113 | }
114 |
115 | /**
116 | * Returns the value of the given comment.
117 | * @param {Token} comment The comment to get the value of.
118 | * @returns {string} The value of the comment.
119 | * @throws {Error} When an unexpected comment type is passed.
120 | */
121 | #getCommentValue(comment) {
122 | if (comment.type === "LineComment") {
123 | return this.getText(comment).slice(2); // strip leading `//`
124 | }
125 |
126 | if (comment.type === "BlockComment") {
127 | return this.getText(comment).slice(2, -2); // strip leading `/*` and trailing `*/`
128 | }
129 |
130 | throw new Error(`Unexpected comment type '${comment.type}'`);
131 | }
132 |
133 | /**
134 | * Returns an array of all inline configuration nodes found in the
135 | * source code.
136 | * @returns {Array} An array of all inline configuration nodes.
137 | */
138 | getInlineConfigNodes() {
139 | if (!this.#inlineConfigComments) {
140 | this.#inlineConfigComments = this.comments.filter(comment =>
141 | INLINE_CONFIG.test(this.#getCommentValue(comment)),
142 | );
143 | }
144 |
145 | return this.#inlineConfigComments ?? [];
146 | }
147 |
148 | /**
149 | * Returns directives that enable or disable rules along with any problems
150 | * encountered while parsing the directives.
151 | * @returns {{problems:Array,directives:Array}} Information
152 | * that ESLint needs to further process the directives.
153 | */
154 | getDisableDirectives() {
155 | const problems = [];
156 | const directives = [];
157 |
158 | this.getInlineConfigNodes().forEach(comment => {
159 | const { label, value, justification } =
160 | commentParser.parseDirective(this.#getCommentValue(comment));
161 |
162 | // `eslint-disable-line` directives are not allowed to span multiple lines as it would be confusing to which lines they apply
163 | if (
164 | label === "eslint-disable-line" &&
165 | comment.loc.start.line !== comment.loc.end.line
166 | ) {
167 | const message = `${label} comment should not span multiple lines.`;
168 |
169 | problems.push({
170 | ruleId: null,
171 | message,
172 | loc: comment.loc,
173 | });
174 | return;
175 | }
176 |
177 | switch (label) {
178 | case "eslint-disable":
179 | case "eslint-enable":
180 | case "eslint-disable-next-line":
181 | case "eslint-disable-line": {
182 | const directiveType = label.slice("eslint-".length);
183 |
184 | directives.push(
185 | new Directive({
186 | type: /** @type {DirectiveType} */ (directiveType),
187 | node: comment,
188 | value,
189 | justification,
190 | }),
191 | );
192 | }
193 |
194 | // no default
195 | }
196 | });
197 |
198 | return { problems, directives };
199 | }
200 |
201 | /**
202 | * Returns inline rule configurations along with any problems
203 | * encountered while parsing the configurations.
204 | * @returns {{problems:Array,configs:Array<{config:{rules:RulesConfig},loc:SourceLocation}>}} Information
205 | * that ESLint needs to further process the rule configurations.
206 | */
207 | applyInlineConfig() {
208 | const problems = [];
209 | const configs = [];
210 |
211 | this.getInlineConfigNodes().forEach(comment => {
212 | const { label, value } = commentParser.parseDirective(
213 | this.#getCommentValue(comment),
214 | );
215 |
216 | if (label === "eslint") {
217 | const parseResult = commentParser.parseJSONLikeConfig(value);
218 |
219 | if (parseResult.ok) {
220 | configs.push({
221 | config: {
222 | rules: parseResult.config,
223 | },
224 | loc: comment.loc,
225 | });
226 | } else {
227 | problems.push({
228 | ruleId: null,
229 | message:
230 | /** @type {{ok: false, error: { message: string }}} */ (
231 | parseResult
232 | ).error.message,
233 | loc: comment.loc,
234 | });
235 | }
236 | }
237 | });
238 |
239 | return {
240 | configs,
241 | problems,
242 | };
243 | }
244 |
245 | /**
246 | * Returns the parent of the given node.
247 | * @param {AnyNode} node The node to get the parent of.
248 | * @returns {AnyNode|undefined} The parent of the node.
249 | */
250 | getParent(node) {
251 | return this.#parents.get(node);
252 | }
253 |
254 | /**
255 | * Traverse the source code and return the steps that were taken.
256 | * @returns {Iterable} The steps that were taken while traversing the source code.
257 | */
258 | traverse() {
259 | // Because the AST doesn't mutate, we can cache the steps
260 | if (this.#steps) {
261 | return this.#steps.values();
262 | }
263 |
264 | /** @type {Array} */
265 | const steps = (this.#steps = []);
266 |
267 | for (const { node, parent, phase } of iterator(this.ast)) {
268 | if (parent) {
269 | this.#parents.set(
270 | /** @type {AnyNode} */ (node),
271 | /** @type {AnyNode} */ (parent),
272 | );
273 | }
274 |
275 | steps.push(
276 | new JSONTraversalStep({
277 | target: /** @type {AnyNode} */ (node),
278 | phase: phase === "enter" ? 1 : 2,
279 | args: [node, parent],
280 | }),
281 | );
282 | }
283 |
284 | return steps;
285 | }
286 | }
287 |
--------------------------------------------------------------------------------
/src/rules/no-duplicate-keys.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Rule to prevent duplicate keys in JSON.
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Type Definitions
8 | //-----------------------------------------------------------------------------
9 |
10 | /**
11 | * @import { MemberNode } from "@humanwhocodes/momoa";
12 | * @import { JSONRuleDefinition } from "../types.ts";
13 | *
14 | * @typedef {"duplicateKey"} NoDuplicateKeysMessageIds
15 | * @typedef {JSONRuleDefinition<{ MessageIds: NoDuplicateKeysMessageIds }>} NoDuplicateKeysRuleDefinition
16 | */
17 |
18 | //-----------------------------------------------------------------------------
19 | // Rule Definition
20 | //-----------------------------------------------------------------------------
21 |
22 | /** @type {NoDuplicateKeysRuleDefinition} */
23 | const rule = {
24 | meta: {
25 | type: "problem",
26 |
27 | docs: {
28 | recommended: true,
29 | description: "Disallow duplicate keys in JSON objects",
30 | url: "https://github.com/eslint/json#rules",
31 | },
32 |
33 | messages: {
34 | duplicateKey: 'Duplicate key "{{key}}" found.',
35 | },
36 | },
37 |
38 | create(context) {
39 | /** @type {Array|undefined>} */
40 | const objectKeys = [];
41 |
42 | /** @type {Map|undefined} */
43 | let keys;
44 |
45 | return {
46 | Object() {
47 | objectKeys.push(keys);
48 | keys = new Map();
49 | },
50 |
51 | Member(node) {
52 | const key =
53 | node.name.type === "String"
54 | ? node.name.value
55 | : node.name.name;
56 |
57 | if (keys.has(key)) {
58 | context.report({
59 | loc: node.name.loc,
60 | messageId: "duplicateKey",
61 | data: {
62 | key,
63 | },
64 | });
65 | } else {
66 | keys.set(key, node);
67 | }
68 | },
69 | "Object:exit"() {
70 | keys = objectKeys.pop();
71 | },
72 | };
73 | },
74 | };
75 |
76 | export default rule;
77 |
--------------------------------------------------------------------------------
/src/rules/no-empty-keys.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Rule to prevent empty keys in JSON.
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Type Definitions
8 | //-----------------------------------------------------------------------------
9 |
10 | /**
11 | * @import { JSONRuleDefinition } from "../types.ts";
12 | *
13 | * @typedef {"emptyKey"} NoEmptyKeysMessageIds
14 | * @typedef {JSONRuleDefinition<{ MessageIds: NoEmptyKeysMessageIds }>} NoEmptyKeysRuleDefinition
15 | */
16 |
17 | //-----------------------------------------------------------------------------
18 | // Rule Definition
19 | //-----------------------------------------------------------------------------
20 |
21 | /** @type {NoEmptyKeysRuleDefinition} */
22 | const rule = {
23 | meta: {
24 | type: "problem",
25 |
26 | docs: {
27 | recommended: true,
28 | description: "Disallow empty keys in JSON objects",
29 | url: "https://github.com/eslint/json#rules",
30 | },
31 |
32 | messages: {
33 | emptyKey: "Empty key found.",
34 | },
35 | },
36 |
37 | create(context) {
38 | return {
39 | Member(node) {
40 | const key =
41 | node.name.type === "String"
42 | ? node.name.value
43 | : node.name.name;
44 |
45 | if (key.trim() === "") {
46 | context.report({
47 | loc: node.name.loc,
48 | messageId: "emptyKey",
49 | });
50 | }
51 | },
52 | };
53 | },
54 | };
55 |
56 | export default rule;
57 |
--------------------------------------------------------------------------------
/src/rules/no-unnormalized-keys.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Rule to detect unnormalized keys in JSON.
3 | * @author Bradley Meck Farias
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Type Definitions
8 | //-----------------------------------------------------------------------------
9 |
10 | /**
11 | * @import { JSONRuleDefinition } from "../types.ts";
12 | *
13 | * @typedef {"unnormalizedKey"} NoUnnormalizedKeysMessageIds
14 | * @typedef {{ form: string }} NoUnnormalizedKeysOptions
15 | * @typedef {JSONRuleDefinition<{ RuleOptions: [NoUnnormalizedKeysOptions], MessageIds: NoUnnormalizedKeysMessageIds }>} NoUnnormalizedKeysRuleDefinition
16 | */
17 |
18 | //-----------------------------------------------------------------------------
19 | // Rule Definition
20 | //-----------------------------------------------------------------------------
21 |
22 | /** @type {NoUnnormalizedKeysRuleDefinition} */
23 | const rule = {
24 | meta: {
25 | type: "problem",
26 |
27 | docs: {
28 | recommended: true,
29 | description: "Disallow JSON keys that are not normalized",
30 | url: "https://github.com/eslint/json#rules",
31 | },
32 |
33 | messages: {
34 | unnormalizedKey: "Unnormalized key '{{key}}' found.",
35 | },
36 |
37 | schema: [
38 | {
39 | type: "object",
40 | properties: {
41 | form: {
42 | enum: ["NFC", "NFD", "NFKC", "NFKD"],
43 | },
44 | },
45 | additionalProperties: false,
46 | },
47 | ],
48 | },
49 |
50 | create(context) {
51 | const form = context.options.length
52 | ? context.options[0].form
53 | : undefined;
54 |
55 | return {
56 | Member(node) {
57 | const key =
58 | node.name.type === "String"
59 | ? node.name.value
60 | : node.name.name;
61 |
62 | if (key.normalize(form) !== key) {
63 | context.report({
64 | loc: node.name.loc,
65 | messageId: "unnormalizedKey",
66 | data: {
67 | key,
68 | },
69 | });
70 | }
71 | },
72 | };
73 | },
74 | };
75 |
76 | export default rule;
77 |
--------------------------------------------------------------------------------
/src/rules/no-unsafe-values.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Rule to detect unsafe values in JSON.
3 | * @author Bradley Meck Farias
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Type Definitions
8 | //-----------------------------------------------------------------------------
9 |
10 | /**
11 | * @import { JSONRuleDefinition } from "../types.ts";
12 | *
13 | * @typedef {"unsafeNumber"|"unsafeInteger"|"unsafeZero"|"subnormal"|"loneSurrogate"} NoUnsafeValuesMessageIds
14 | * @typedef {JSONRuleDefinition<{ MessageIds: NoUnsafeValuesMessageIds }>} NoUnsafeValuesRuleDefinition
15 | */
16 |
17 | //-----------------------------------------------------------------------------
18 | // Helpers
19 | //-----------------------------------------------------------------------------
20 |
21 | /*
22 | * This rule is based on the JSON grammar from RFC 8259, section 6.
23 | * https://tools.ietf.org/html/rfc8259#section-6
24 | *
25 | * We separately capture the integer and fractional parts of a number, so that
26 | * we can check for unsafe numbers that will evaluate to Infinity.
27 | */
28 | const NUMBER =
29 | /^-?(?0|([1-9][0-9]*))(?:\.(?[0-9]+))?(?:[eE][+-]?[0-9]+)?$/u;
30 | const NON_ZERO = /[1-9]/u;
31 |
32 | //-----------------------------------------------------------------------------
33 | // Rule Definition
34 | //-----------------------------------------------------------------------------
35 |
36 | /** @type {NoUnsafeValuesRuleDefinition} */
37 | const rule = {
38 | meta: {
39 | type: "problem",
40 |
41 | docs: {
42 | recommended: true,
43 | description: "Disallow JSON values that are unsafe for interchange",
44 | url: "https://github.com/eslint/json#rules",
45 | },
46 |
47 | messages: {
48 | unsafeNumber: "The number '{{ value }}' will evaluate to Infinity.",
49 | unsafeInteger:
50 | "The integer '{{ value }}' is outside the safe integer range.",
51 | unsafeZero: "The number '{{ value }}' will evaluate to zero.",
52 | subnormal:
53 | "Unexpected subnormal number '{{ value }}' found, which may cause interoperability issues.",
54 | loneSurrogate: "Lone surrogate '{{ surrogate }}' found.",
55 | },
56 | },
57 |
58 | create(context) {
59 | return {
60 | Number(node) {
61 | const value = context.sourceCode.getText(node);
62 |
63 | if (Number.isFinite(node.value) !== true) {
64 | context.report({
65 | loc: node.loc,
66 | messageId: "unsafeNumber",
67 | data: { value },
68 | });
69 | } else {
70 | // Also matches -0, intentionally
71 | if (node.value === 0) {
72 | // If the value has been rounded down to 0, but there was some
73 | // fraction or non-zero part before the e-, this is a very small
74 | // number that doesn't fit inside an f64.
75 | const match = value.match(NUMBER);
76 | // assert(match, "If the regex is right, match is always truthy")
77 |
78 | // If any part of the number other than the exponent has a
79 | // non-zero digit in it, this number was not intended to be
80 | // evaluated down to a zero.
81 | if (
82 | NON_ZERO.test(match.groups.int) ||
83 | NON_ZERO.test(match.groups.frac)
84 | ) {
85 | context.report({
86 | loc: node.loc,
87 | messageId: "unsafeZero",
88 | data: { value },
89 | });
90 | }
91 | } else if (!/[.e]/iu.test(value)) {
92 | // Intended to be an integer
93 | if (
94 | node.value > Number.MAX_SAFE_INTEGER ||
95 | node.value < Number.MIN_SAFE_INTEGER
96 | ) {
97 | context.report({
98 | loc: node.loc,
99 | messageId: "unsafeInteger",
100 | data: { value },
101 | });
102 | }
103 | } else {
104 | // Floating point. Check for subnormal.
105 | const buffer = new ArrayBuffer(8);
106 | const view = new DataView(buffer);
107 | view.setFloat64(0, node.value, false);
108 | const asBigInt = view.getBigUint64(0, false);
109 | // Subnormals have an 11-bit exponent of 0 and a non-zero mantissa.
110 | if ((asBigInt & 0x7ff0000000000000n) === 0n) {
111 | context.report({
112 | loc: node.loc,
113 | messageId: "subnormal",
114 | // Value included so that it's seen in scientific notation
115 | data: {
116 | value,
117 | },
118 | });
119 | }
120 | }
121 | }
122 | },
123 | String(node) {
124 | if (node.value.isWellFormed) {
125 | if (node.value.isWellFormed()) {
126 | return;
127 | }
128 | }
129 | // match any high surrogate and, if it exists, a paired low surrogate
130 | // match any low surrogate not already matched
131 | const surrogatePattern =
132 | /[\uD800-\uDBFF][\uDC00-\uDFFF]?|[\uDC00-\uDFFF]/gu;
133 | let match = surrogatePattern.exec(node.value);
134 | while (match) {
135 | // only need to report non-paired surrogates
136 | if (match[0].length < 2) {
137 | context.report({
138 | loc: node.loc,
139 | messageId: "loneSurrogate",
140 | data: {
141 | surrogate: JSON.stringify(match[0]).slice(
142 | 1,
143 | -1,
144 | ),
145 | },
146 | });
147 | }
148 | match = surrogatePattern.exec(node.value);
149 | }
150 | },
151 | };
152 | },
153 | };
154 |
155 | export default rule;
156 |
--------------------------------------------------------------------------------
/src/rules/sort-keys.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Rule to require JSON object keys to be sorted.
3 | * Copied largely from https://github.com/eslint/eslint/blob/main/lib/rules/sort-keys.js
4 | * @author Robin Thomas
5 | */
6 |
7 | //-----------------------------------------------------------------------------
8 | // Imports
9 | //-----------------------------------------------------------------------------
10 |
11 | import naturalCompare from "natural-compare";
12 |
13 | //-----------------------------------------------------------------------------
14 | // Type Definitions
15 | //-----------------------------------------------------------------------------
16 |
17 | /**
18 | * @import { JSONRuleDefinition } from "../types.ts";
19 | * @import { MemberNode } from "@humanwhocodes/momoa";
20 | *
21 | * @typedef {Object} SortOptions
22 | * @property {boolean} caseSensitive
23 | * @property {boolean} natural
24 | * @property {number} minKeys
25 | * @property {boolean} allowLineSeparatedGroups
26 | *
27 | * @typedef {"sortKeys"} SortKeysMessageIds
28 | * @typedef {"asc"|"desc"} SortDirection
29 | * @typedef {[SortDirection, SortOptions]} SortKeysRuleOptions
30 | * @typedef {JSONRuleDefinition<{ RuleOptions: SortKeysRuleOptions, MessageIds: SortKeysMessageIds }>} SortKeysRuleDefinition
31 | * @typedef {(a:string,b:string) => boolean} Comparator
32 | */
33 |
34 | //-----------------------------------------------------------------------------
35 | // Helpers
36 | //-----------------------------------------------------------------------------
37 |
38 | const hasNonWhitespace = /\S/u;
39 |
40 | const comparators = {
41 | ascending: {
42 | alphanumeric: {
43 | /** @type {Comparator} */
44 | sensitive: (a, b) => a <= b,
45 |
46 | /** @type {Comparator} */
47 | insensitive: (a, b) => a.toLowerCase() <= b.toLowerCase(),
48 | },
49 | natural: {
50 | /** @type {Comparator} */
51 | sensitive: (a, b) => naturalCompare(a, b) <= 0,
52 |
53 | /** @type {Comparator} */
54 | insensitive: (a, b) =>
55 | naturalCompare(a.toLowerCase(), b.toLowerCase()) <= 0,
56 | },
57 | },
58 | descending: {
59 | alphanumeric: {
60 | /** @type {Comparator} */
61 | sensitive: (a, b) =>
62 | comparators.ascending.alphanumeric.sensitive(b, a),
63 |
64 | /** @type {Comparator} */
65 | insensitive: (a, b) =>
66 | comparators.ascending.alphanumeric.insensitive(b, a),
67 | },
68 | natural: {
69 | /** @type {Comparator} */
70 | sensitive: (a, b) => comparators.ascending.natural.sensitive(b, a),
71 |
72 | /** @type {Comparator} */
73 | insensitive: (a, b) =>
74 | comparators.ascending.natural.insensitive(b, a),
75 | },
76 | },
77 | };
78 |
79 | /**
80 | * Gets the MemberNode's string key value.
81 | * @param {MemberNode} member
82 | * @return {string}
83 | */
84 | function getKey(member) {
85 | return member.name.type === "Identifier"
86 | ? member.name.name
87 | : member.name.value;
88 | }
89 |
90 | //-----------------------------------------------------------------------------
91 | // Rule Definition
92 | //-----------------------------------------------------------------------------
93 |
94 | /** @type {SortKeysRuleDefinition} */
95 | const rule = {
96 | meta: {
97 | type: "suggestion",
98 |
99 | defaultOptions: [
100 | "asc",
101 | {
102 | allowLineSeparatedGroups: false,
103 | caseSensitive: true,
104 | minKeys: 2,
105 | natural: false,
106 | },
107 | ],
108 |
109 | docs: {
110 | recommended: false,
111 | description: `Require JSON object keys to be sorted`,
112 | url: "https://github.com/eslint/json#rules",
113 | },
114 |
115 | messages: {
116 | sortKeys:
117 | "Expected object keys to be in {{sortName}} case-{{sensitivity}} {{direction}} order. '{{thisName}}' should be before '{{prevName}}'.",
118 | },
119 |
120 | schema: [
121 | {
122 | enum: ["asc", "desc"],
123 | },
124 | {
125 | type: "object",
126 | properties: {
127 | caseSensitive: {
128 | type: "boolean",
129 | },
130 | natural: {
131 | type: "boolean",
132 | },
133 | minKeys: {
134 | type: "integer",
135 | minimum: 2,
136 | },
137 | allowLineSeparatedGroups: {
138 | type: "boolean",
139 | },
140 | },
141 | additionalProperties: false,
142 | },
143 | ],
144 | },
145 |
146 | create(context) {
147 | const [
148 | directionShort,
149 | { allowLineSeparatedGroups, caseSensitive, natural, minKeys },
150 | ] = context.options;
151 |
152 | const direction = directionShort === "asc" ? "ascending" : "descending";
153 | const sortName = natural ? "natural" : "alphanumeric";
154 | const sensitivity = caseSensitive ? "sensitive" : "insensitive";
155 | const isValidOrder = comparators[direction][sortName][sensitivity];
156 |
157 | // 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
158 | const commentLineNums = new Set();
159 | for (const comment of context.sourceCode.comments) {
160 | for (
161 | let lineNum = comment.loc.start.line;
162 | lineNum <= comment.loc.end.line;
163 | lineNum += 1
164 | ) {
165 | commentLineNums.add(lineNum);
166 | }
167 | }
168 |
169 | /**
170 | * Checks if two members are line-separated.
171 | * @param {MemberNode} prevMember The previous member.
172 | * @param {MemberNode} member The current member.
173 | * @return {boolean}
174 | */
175 | function isLineSeparated(prevMember, member) {
176 | // Note that there can be comments *inside* members, e.g. `{"foo: /* comment *\/ "bar"}`, but these are ignored when calculating line-separated groups
177 | const prevMemberEndLine = prevMember.loc.end.line;
178 | const thisStartLine = member.loc.start.line;
179 | if (thisStartLine - prevMemberEndLine < 2) {
180 | return false;
181 | }
182 |
183 | for (
184 | let lineNum = prevMemberEndLine + 1;
185 | lineNum < thisStartLine;
186 | lineNum += 1
187 | ) {
188 | if (
189 | !commentLineNums.has(lineNum) &&
190 | !hasNonWhitespace.test(
191 | context.sourceCode.lines[lineNum - 1],
192 | )
193 | ) {
194 | return true;
195 | }
196 | }
197 |
198 | return false;
199 | }
200 |
201 | return {
202 | Object(node) {
203 | let prevMember;
204 | let prevName;
205 |
206 | if (node.members.length < minKeys) {
207 | return;
208 | }
209 |
210 | for (const member of node.members) {
211 | const thisName = getKey(member);
212 |
213 | if (
214 | prevMember &&
215 | !isValidOrder(prevName, thisName) &&
216 | (!allowLineSeparatedGroups ||
217 | !isLineSeparated(prevMember, member))
218 | ) {
219 | context.report({
220 | loc: member.name.loc,
221 | messageId: "sortKeys",
222 | data: {
223 | thisName,
224 | prevName,
225 | direction,
226 | sensitivity,
227 | sortName,
228 | },
229 | });
230 | }
231 |
232 | prevMember = member;
233 | prevName = thisName;
234 | }
235 | },
236 | };
237 | },
238 | };
239 |
240 | export default rule;
241 |
--------------------------------------------------------------------------------
/src/rules/top-level-interop.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Rule to ensure top-level items are either an array or object.
3 | * @author Joe Hildebrand
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Type Definitions
8 | //-----------------------------------------------------------------------------
9 |
10 | /**
11 | * @import { JSONRuleDefinition } from "../types.ts";
12 | *
13 | * @typedef {"topLevel"} TopLevelInteropMessageIds
14 | * @typedef {JSONRuleDefinition<{ MessageIds: TopLevelInteropMessageIds }>} TopLevelInteropRuleDefinition
15 | */
16 |
17 | //-----------------------------------------------------------------------------
18 | // Rule Definition
19 | //-----------------------------------------------------------------------------
20 |
21 | /** @type {TopLevelInteropRuleDefinition} */
22 | const rule = {
23 | meta: {
24 | type: "problem",
25 |
26 | docs: {
27 | recommended: false,
28 | description:
29 | "Require the JSON top-level value to be an array or object",
30 | url: "https://github.com/eslint/json#rules",
31 | },
32 |
33 | messages: {
34 | topLevel:
35 | "Top level item should be array or object, got '{{type}}'.",
36 | },
37 | },
38 |
39 | create(context) {
40 | return {
41 | Document(node) {
42 | const { type } = node.body;
43 | if (type !== "Object" && type !== "Array") {
44 | context.report({
45 | loc: node.loc,
46 | messageId: "topLevel",
47 | data: { type },
48 | });
49 | }
50 | },
51 | };
52 | },
53 | };
54 |
55 | export default rule;
56 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Additional types for this package.
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //------------------------------------------------------------------------------
7 | // Imports
8 | //------------------------------------------------------------------------------
9 |
10 | import type { RuleVisitor, RuleDefinition } from "@eslint/core";
11 | import type {
12 | DocumentNode,
13 | MemberNode,
14 | ElementNode,
15 | ObjectNode,
16 | ArrayNode,
17 | StringNode,
18 | NullNode,
19 | NumberNode,
20 | BooleanNode,
21 | NaNNode,
22 | InfinityNode,
23 | IdentifierNode,
24 | AnyNode,
25 | Token,
26 | } from "@humanwhocodes/momoa";
27 | import type { JSONLanguageOptions, JSONSourceCode } from "./index.js";
28 |
29 | //------------------------------------------------------------------------------
30 | // Types
31 | //------------------------------------------------------------------------------
32 |
33 | type ValueNodeParent = DocumentNode | MemberNode | ElementNode;
34 |
35 | /**
36 | * A JSON syntax element, including nodes and tokens.
37 | */
38 | export type JSONSyntaxElement = Token | AnyNode;
39 |
40 | /**
41 | * The visitor format returned from rules in this package.
42 | */
43 | export interface JSONRuleVisitor extends RuleVisitor {
44 | Document?(node: DocumentNode): void;
45 | Member?(node: MemberNode, parent?: ObjectNode): void;
46 | Element?(node: ElementNode, parent?: ArrayNode): void;
47 | Object?(node: ObjectNode, parent?: ValueNodeParent): void;
48 | Array?(node: ArrayNode, parent?: ValueNodeParent): void;
49 | String?(node: StringNode, parent?: ValueNodeParent): void;
50 | Null?(node: NullNode, parent?: ValueNodeParent): void;
51 | Number?(node: NumberNode, parent?: ValueNodeParent): void;
52 | Boolean?(node: BooleanNode, parent?: ValueNodeParent): void;
53 | NaN?(node: NaNNode, parent?: ValueNodeParent): void;
54 | Infinity?(node: InfinityNode, parent?: ValueNodeParent): void;
55 | Identifier?(node: IdentifierNode, parent?: ValueNodeParent): void;
56 |
57 | "Document:exit"?(node: DocumentNode): void;
58 | "Member:exit"?(node: MemberNode, parent?: ObjectNode): void;
59 | "Element:exit"?(node: ElementNode, parent?: ArrayNode): void;
60 | "Object:exit"?(node: ObjectNode, parent?: ValueNodeParent): void;
61 | "Array:exit"?(node: ArrayNode, parent?: ValueNodeParent): void;
62 | "String:exit"?(node: StringNode, parent?: ValueNodeParent): void;
63 | "Null:exit"?(node: NullNode, parent?: ValueNodeParent): void;
64 | "Number:exit"?(node: NumberNode, parent?: ValueNodeParent): void;
65 | "Boolean:exit"?(node: BooleanNode, parent?: ValueNodeParent): void;
66 | "NaN:exit"?(node: NaNNode, parent?: ValueNodeParent): void;
67 | "Infinity:exit"?(node: InfinityNode, parent?: ValueNodeParent): void;
68 | "Identifier:exit"?(node: IdentifierNode, parent?: ValueNodeParent): void;
69 | }
70 |
71 | export type JSONRuleDefinitionTypeOptions = {
72 | RuleOptions: unknown[];
73 | MessageIds: string;
74 | ExtRuleDocs: Record;
75 | };
76 |
77 | export type JSONRuleDefinition<
78 | Options extends Partial = {},
79 | > = RuleDefinition<
80 | // Language specific type options (non-configurable)
81 | {
82 | LangOptions: JSONLanguageOptions;
83 | Code: JSONSourceCode;
84 | Visitor: JSONRuleVisitor;
85 | Node: AnyNode;
86 | } & Required<
87 | // Rule specific type options (custom)
88 | Options &
89 | // Rule specific type options (defaults)
90 | Omit
91 | >
92 | >;
93 |
--------------------------------------------------------------------------------
/tests/languages/json-language.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Tests for JSONLanguage
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Imports
8 | //-----------------------------------------------------------------------------
9 |
10 | import { JSONLanguage } from "../../src/languages/json-language.js";
11 | import assert from "node:assert";
12 |
13 | //-----------------------------------------------------------------------------
14 | // Tests
15 | //-----------------------------------------------------------------------------
16 |
17 | describe("JSONLanguage", () => {
18 | describe("visitorKeys", () => {
19 | it("should have visitorKeys property", () => {
20 | const language = new JSONLanguage({ mode: "json" });
21 |
22 | assert.deepStrictEqual(language.visitorKeys.Document, ["body"]);
23 | });
24 | });
25 |
26 | describe("validateLanguageOptions()", () => {
27 | it("should throw an error when allowTrailingCommas is not a boolean", () => {
28 | const language = new JSONLanguage({
29 | mode: "jsonc",
30 | allowTrailingCommas: "true",
31 | });
32 | assert.throws(() => {
33 | language.validateLanguageOptions({
34 | allowTrailingCommas: "true",
35 | });
36 | }, /allowTrailingCommas/u);
37 | });
38 |
39 | it("should throw an error when allowTrailingCommas is a boolean in JSON mode", () => {
40 | const language = new JSONLanguage({ mode: "json" });
41 | assert.throws(() => {
42 | language.validateLanguageOptions({ allowTrailingCommas: true });
43 | }, /allowTrailingCommas/u);
44 | });
45 |
46 | it("should throw an error when allowTrailingCommas is a boolean in JSON5 mode", () => {
47 | const language = new JSONLanguage({ mode: "json5" });
48 | assert.throws(() => {
49 | language.validateLanguageOptions({ allowTrailingCommas: true });
50 | }, /allowTrailingCommas/u);
51 | });
52 |
53 | it("should not throw an error when allowTrailingCommas is a boolean in JSONC mode", () => {
54 | const language = new JSONLanguage({ mode: "jsonc" });
55 | assert.doesNotThrow(() => {
56 | language.validateLanguageOptions({ allowTrailingCommas: true });
57 | });
58 | });
59 |
60 | it("should not throw an error when allowTrailingCommas is not provided", () => {
61 | const language = new JSONLanguage({ mode: "jsonc" });
62 | assert.doesNotThrow(() => {
63 | language.validateLanguageOptions({});
64 | });
65 | });
66 |
67 | it("should not throw an error when allowTrailingCommas is not provided and other keys are present", () => {
68 | const language = new JSONLanguage({ mode: "jsonc" });
69 | assert.doesNotThrow(() => {
70 | language.validateLanguageOptions({ foo: "bar" });
71 | });
72 | });
73 | });
74 |
75 | describe("parse()", () => {
76 | it("should not parse jsonc by default", () => {
77 | const language = new JSONLanguage({ mode: "json" });
78 | const result = language.parse({
79 | body: "{\n//test\n}",
80 | path: "test.json",
81 | });
82 |
83 | assert.strictEqual(result.ok, false);
84 | assert.strictEqual(
85 | result.errors[0].message,
86 | "Unexpected character '/' found.",
87 | );
88 | });
89 |
90 | it("should not parse trailing commas by default in json mode", () => {
91 | const language = new JSONLanguage({ mode: "json" });
92 | const result = language.parse({
93 | body: '{\n"a": 1,\n}',
94 | path: "test.json",
95 | });
96 |
97 | assert.strictEqual(result.ok, false);
98 | assert.strictEqual(
99 | result.errors[0].message,
100 | "Unexpected token RBrace found.",
101 | );
102 | });
103 |
104 | it("should not parse trailing commas by default in jsonc mode", () => {
105 | const language = new JSONLanguage({ mode: "jsonc" });
106 | const result = language.parse({
107 | body: '{\n"a": 1,\n}',
108 | path: "test.jsonc",
109 | });
110 |
111 | assert.strictEqual(result.ok, false);
112 | assert.strictEqual(
113 | result.errors[0].message,
114 | "Unexpected token RBrace found.",
115 | );
116 | });
117 |
118 | it("should parse trailing commas when enabled in jsonc mode", () => {
119 | const language = new JSONLanguage({ mode: "jsonc" });
120 | const result = language.parse(
121 | {
122 | body: '{\n"a": 1,\n}',
123 | path: "test.jsonc",
124 | },
125 | { languageOptions: { allowTrailingCommas: true } },
126 | );
127 |
128 | assert.strictEqual(result.ok, true);
129 | assert.strictEqual(result.ast.type, "Document");
130 | assert.strictEqual(result.ast.body.type, "Object");
131 | });
132 |
133 | it("should parse json by default", () => {
134 | const language = new JSONLanguage({ mode: "json" });
135 | const result = language.parse({
136 | body: "{\n\n}",
137 | path: "test.json",
138 | });
139 |
140 | assert.strictEqual(result.ok, true);
141 | assert.strictEqual(result.ast.type, "Document");
142 | assert.strictEqual(result.ast.body.type, "Object");
143 | });
144 |
145 | it("should set the mode to jsonc", () => {
146 | const language = new JSONLanguage({ mode: "jsonc" });
147 | const result = language.parse({
148 | body: "{\n//test\n}",
149 | path: "test.jsonc",
150 | });
151 |
152 | assert.strictEqual(result.ok, true);
153 | assert.strictEqual(result.ast.type, "Document");
154 | assert.strictEqual(result.ast.body.type, "Object");
155 | });
156 | });
157 |
158 | describe("createSourceCode()", () => {
159 | it("should create a JSONSourceCode instance for JSON", () => {
160 | const language = new JSONLanguage({ mode: "json" });
161 | const file = { body: "{\n\n}", path: "test.json" };
162 | const parseResult = language.parse(file);
163 | const sourceCode = language.createSourceCode(file, parseResult);
164 | assert.strictEqual(sourceCode.constructor.name, "JSONSourceCode");
165 |
166 | assert.strictEqual(sourceCode.ast.type, "Document");
167 | assert.strictEqual(sourceCode.ast.body.type, "Object");
168 | assert.strictEqual(sourceCode.text, "{\n\n}");
169 | assert.strictEqual(sourceCode.comments.length, 0);
170 | });
171 |
172 | it("should create a JSONSourceCode instance for JSONC", () => {
173 | const language = new JSONLanguage({ mode: "jsonc" });
174 | const file = { body: "{\n//test\n}", path: "test.jsonc" };
175 | const parseResult = language.parse(file);
176 | const sourceCode = language.createSourceCode(
177 | file,
178 | parseResult,
179 | "test.jsonc",
180 | );
181 |
182 | assert.strictEqual(sourceCode.constructor.name, "JSONSourceCode");
183 |
184 | assert.strictEqual(sourceCode.ast.type, "Document");
185 | assert.strictEqual(sourceCode.ast.body.type, "Object");
186 | assert.strictEqual(sourceCode.text, "{\n//test\n}");
187 | assert.strictEqual(sourceCode.comments.length, 1);
188 | });
189 | });
190 | });
191 |
--------------------------------------------------------------------------------
/tests/languages/json-source-code.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Tests for JSONSourceCode
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Imports
8 | //-----------------------------------------------------------------------------
9 |
10 | import { JSONSourceCode } from "../../src/languages/json-source-code.js";
11 | import { JSONLanguage } from "../../src/languages/json-language.js";
12 | import assert from "node:assert";
13 | import dedent from "dedent";
14 |
15 | //-----------------------------------------------------------------------------
16 | // Tests
17 | //-----------------------------------------------------------------------------
18 |
19 | describe("JSONSourceCode", () => {
20 | describe("constructor", () => {
21 | it("should create a JSONSourceCode instance", () => {
22 | const ast = {
23 | type: "Document",
24 | body: {
25 | type: "Object",
26 | properties: [],
27 | },
28 | tokens: [],
29 | };
30 | const text = "{}";
31 | const sourceCode = new JSONSourceCode({
32 | text,
33 | ast,
34 | });
35 |
36 | assert.strictEqual(sourceCode.constructor.name, "JSONSourceCode");
37 | assert.strictEqual(sourceCode.ast, ast);
38 | assert.strictEqual(sourceCode.text, text);
39 | });
40 | });
41 |
42 | describe("getText()", () => {
43 | it("should return the text of the source code", () => {
44 | const file = { body: "{}", path: "test.json" };
45 | const language = new JSONLanguage({ mode: "json" });
46 | const parseResult = language.parse(file);
47 | const sourceCode = new JSONSourceCode({
48 | text: file.body,
49 | ast: parseResult.ast,
50 | });
51 |
52 | assert.strictEqual(sourceCode.getText(), file.body);
53 | });
54 | });
55 |
56 | describe("getLoc()", () => {
57 | it("should return the loc property of a node", () => {
58 | const loc = {
59 | start: {
60 | line: 1,
61 | column: 1,
62 | offset: 0,
63 | },
64 | end: {
65 | line: 1,
66 | column: 2,
67 | offset: 1,
68 | },
69 | };
70 | const ast = {
71 | type: "Document",
72 | body: {
73 | type: "Object",
74 | properties: [],
75 | },
76 | tokens: [],
77 | loc,
78 | };
79 | const text = "{}";
80 | const sourceCode = new JSONSourceCode({
81 | text,
82 | ast,
83 | });
84 |
85 | assert.strictEqual(sourceCode.getLoc(ast), loc);
86 | });
87 | });
88 |
89 | describe("getRange()", () => {
90 | it("should return the range property of a node", () => {
91 | const range = [0, 1];
92 | const ast = {
93 | type: "Document",
94 | body: {
95 | type: "Object",
96 | properties: [],
97 | },
98 | tokens: [],
99 | range,
100 | };
101 | const text = "{}";
102 | const sourceCode = new JSONSourceCode({
103 | text,
104 | ast,
105 | });
106 |
107 | assert.strictEqual(sourceCode.getRange(ast), range);
108 | });
109 | });
110 |
111 | describe("comments", () => {
112 | it("should contain an empty array when parsing JSON", () => {
113 | const file = { body: "{}", path: "test.json" };
114 | const language = new JSONLanguage({ mode: "json" });
115 | const parseResult = language.parse(file);
116 | const sourceCode = new JSONSourceCode({
117 | text: file.body,
118 | ast: parseResult.ast,
119 | });
120 |
121 | assert.deepStrictEqual(sourceCode.comments, []);
122 | });
123 |
124 | it("should contain an array of comments when parsing JSONC", () => {
125 | const file = { body: "{\n//test\n}", path: "test.jsonc" };
126 | const language = new JSONLanguage({ mode: "jsonc" });
127 | const parseResult = language.parse(file);
128 | const sourceCode = new JSONSourceCode({
129 | text: file.body,
130 | ast: parseResult.ast,
131 | });
132 |
133 | // should contain one comment
134 | assert.strictEqual(sourceCode.comments.length, 1);
135 |
136 | const comment = sourceCode.comments[0];
137 | assert.strictEqual(comment.type, "LineComment");
138 | assert.deepStrictEqual(comment.range, [2, 8]);
139 | assert.deepStrictEqual(comment.loc, {
140 | start: { line: 2, column: 1, offset: 2 },
141 | end: { line: 2, column: 7, offset: 8 },
142 | });
143 | });
144 | });
145 |
146 | describe("lines", () => {
147 | it("should return an array of lines", () => {
148 | const file = { body: "{\n//test\n}", path: "test.jsonc" };
149 | const language = new JSONLanguage({ mode: "jsonc" });
150 | const parseResult = language.parse(file);
151 | const sourceCode = new JSONSourceCode({
152 | text: file.body,
153 | ast: parseResult.ast,
154 | });
155 |
156 | assert.deepStrictEqual(sourceCode.lines, ["{", "//test", "}"]);
157 | });
158 | });
159 |
160 | describe("getParent()", () => {
161 | it("should return the parent node for a given node", () => {
162 | const ast = {
163 | type: "Document",
164 | body: {
165 | type: "Object",
166 | properties: [],
167 | },
168 | tokens: [],
169 | };
170 | const text = "{}";
171 | const sourceCode = new JSONSourceCode({
172 | text,
173 | ast,
174 | });
175 | const node = ast.body;
176 |
177 | // call traverse to initialize the parent map
178 | sourceCode.traverse();
179 |
180 | assert.strictEqual(sourceCode.getParent(node), ast);
181 | });
182 |
183 | it("should return the parent node for a deeply nested node", () => {
184 | const ast = {
185 | type: "Document",
186 | body: {
187 | type: "Object",
188 | members: [
189 | {
190 | type: "Member",
191 | name: {
192 | type: "Identifier",
193 | name: "foo",
194 | },
195 | value: {
196 | type: "Object",
197 | properties: [],
198 | },
199 | },
200 | ],
201 | },
202 | tokens: [],
203 | };
204 | const text = '{"foo":{}}';
205 | const sourceCode = new JSONSourceCode({
206 | text,
207 | ast,
208 | });
209 | const node = ast.body.members[0].value;
210 |
211 | // call traverse to initialize the parent map
212 | sourceCode.traverse();
213 |
214 | assert.strictEqual(sourceCode.getParent(node), ast.body.members[0]);
215 | });
216 | });
217 |
218 | describe("getAncestors()", () => {
219 | it("should return an array of ancestors for a given node", () => {
220 | const ast = {
221 | type: "Document",
222 | body: {
223 | type: "Object",
224 | members: [],
225 | },
226 | tokens: [],
227 | };
228 | const text = "{}";
229 | const sourceCode = new JSONSourceCode({
230 | text,
231 | ast,
232 | });
233 | const node = ast.body;
234 |
235 | // call traverse to initialize the parent map
236 | sourceCode.traverse();
237 |
238 | assert.deepStrictEqual(sourceCode.getAncestors(node), [ast]);
239 | });
240 |
241 | it("should return an array of ancestors for a deeply nested node", () => {
242 | const ast = {
243 | type: "Document",
244 | body: {
245 | type: "Object",
246 | members: [
247 | {
248 | type: "Member",
249 | name: {
250 | type: "Identifier",
251 | name: "foo",
252 | },
253 | value: {
254 | type: "Object",
255 | members: [],
256 | },
257 | },
258 | ],
259 | },
260 | tokens: [],
261 | };
262 | const text = '{"foo":{}}';
263 | const sourceCode = new JSONSourceCode({
264 | text,
265 | ast,
266 | });
267 | const node = ast.body.members[0].value;
268 |
269 | // call traverse to initialize the parent map
270 | sourceCode.traverse();
271 |
272 | assert.deepStrictEqual(sourceCode.getAncestors(node), [
273 | ast,
274 | ast.body,
275 | ast.body.members[0],
276 | ]);
277 | });
278 | });
279 |
280 | describe("config comments", () => {
281 | const text = dedent`
282 | {
283 | /* rule config comments */
284 | //eslint json/no-duplicate-keys: error
285 | // eslint json/no-duplicate-keys: [1] -- comment
286 | /*eslint json/no-duplicate-keys: [2, { allow: ["foo"] }]*/
287 | /*
288 | eslint
289 | json/no-empty-keys: warn,
290 | json/no-duplicate-keys: [2, "strict"]
291 | --
292 | comment
293 | */
294 |
295 | // invalid rule config comments
296 | // eslint json/no-duplicate-keys: [error
297 | /*eslint json/no-duplicate-keys: [1, { allow: ["foo"] ]*/
298 |
299 | // not rule config comments
300 | //eslintjson/no-duplicate-keys: error
301 | /*-eslint json/no-duplicate-keys: error*/
302 |
303 | /* disable directives */
304 | //eslint-disable
305 | /* eslint-disable json/no-duplicate-keys -- we want duplicate keys */
306 | // eslint-enable json/no-duplicate-keys, json/no-empty-keys
307 | /*eslint-enable*/
308 | "": 5, // eslint-disable-line json/no-empty-keys
309 | /*eslint-disable-line json/no-empty-keys -- special case*/ "": 6,
310 | //eslint-disable-next-line
311 | "": 7,
312 | /* eslint-disable-next-line json/no-duplicate-keys, json/no-empty-keys
313 | -- another special case
314 | */
315 | "": 8
316 |
317 | // invalid disable directives
318 | /* eslint-disable-line json/no-duplicate-keys
319 | */
320 |
321 | // not disable directives
322 | ///eslint-disable
323 | /*eslint-disable-*/
324 | }
325 | `;
326 |
327 | ["jsonc", "json5"].forEach(languageMode => {
328 | describe(`with ${languageMode} language`, () => {
329 | let sourceCode = null;
330 |
331 | beforeEach(() => {
332 | const file = { body: text, path: `test.${languageMode}` };
333 | const language = new JSONLanguage({ mode: languageMode });
334 | const parseResult = language.parse(file);
335 | sourceCode = new JSONSourceCode({
336 | text: file.body,
337 | ast: parseResult.ast,
338 | });
339 | });
340 |
341 | afterEach(() => {
342 | sourceCode = null;
343 | });
344 |
345 | describe("getInlineConfigNodes()", () => {
346 | it("should return inline config comments", () => {
347 | const allComments = sourceCode.comments;
348 | const configComments =
349 | sourceCode.getInlineConfigNodes();
350 |
351 | const configCommentsIndexes = [
352 | 1, 2, 3, 4, 6, 7, 12, 13, 14, 15, 16, 17, 18, 19,
353 | 21,
354 | ];
355 |
356 | assert.strictEqual(
357 | configComments.length,
358 | configCommentsIndexes.length,
359 | );
360 |
361 | configComments.forEach((configComment, i) => {
362 | assert.strictEqual(
363 | configComment,
364 | allComments[configCommentsIndexes[i]],
365 | );
366 | });
367 | });
368 | });
369 |
370 | describe("applyInlineConfig()", () => {
371 | it("should return rule configs and problems", () => {
372 | const allComments = sourceCode.comments;
373 | const { configs, problems } =
374 | sourceCode.applyInlineConfig();
375 |
376 | assert.deepStrictEqual(configs, [
377 | {
378 | config: {
379 | rules: {
380 | "json/no-duplicate-keys": "error",
381 | },
382 | },
383 | loc: allComments[1].loc,
384 | },
385 | {
386 | config: {
387 | rules: {
388 | "json/no-duplicate-keys": [1],
389 | },
390 | },
391 | loc: allComments[2].loc,
392 | },
393 | {
394 | config: {
395 | rules: {
396 | "json/no-duplicate-keys": [
397 | 2,
398 | { allow: ["foo"] },
399 | ],
400 | },
401 | },
402 | loc: allComments[3].loc,
403 | },
404 | {
405 | config: {
406 | rules: {
407 | "json/no-empty-keys": "warn",
408 | "json/no-duplicate-keys": [2, "strict"],
409 | },
410 | },
411 | loc: allComments[4].loc,
412 | },
413 | ]);
414 |
415 | assert.strictEqual(problems.length, 2);
416 | assert.strictEqual(problems[0].ruleId, null);
417 | assert.match(problems[0].message, /Failed to parse/u);
418 | assert.strictEqual(problems[0].loc, allComments[6].loc);
419 | assert.strictEqual(problems[1].ruleId, null);
420 | assert.match(problems[1].message, /Failed to parse/u);
421 | assert.strictEqual(problems[1].loc, allComments[7].loc);
422 | });
423 | });
424 |
425 | describe("getDisableDirectives()", () => {
426 | it("should return disable directives and problems", () => {
427 | const allComments = sourceCode.comments;
428 | const { directives, problems } =
429 | sourceCode.getDisableDirectives();
430 |
431 | assert.deepStrictEqual(
432 | directives.map(obj => ({ ...obj })),
433 | [
434 | {
435 | type: "disable",
436 | value: "",
437 | justification: "",
438 | node: allComments[12],
439 | },
440 | {
441 | type: "disable",
442 | value: "json/no-duplicate-keys",
443 | justification: "we want duplicate keys",
444 | node: allComments[13],
445 | },
446 | {
447 | type: "enable",
448 | value: "json/no-duplicate-keys, json/no-empty-keys",
449 | justification: "",
450 | node: allComments[14],
451 | },
452 | {
453 | type: "enable",
454 | value: "",
455 | justification: "",
456 | node: allComments[15],
457 | },
458 | {
459 | type: "disable-line",
460 | value: "json/no-empty-keys",
461 | justification: "",
462 | node: allComments[16],
463 | },
464 | {
465 | type: "disable-line",
466 | value: "json/no-empty-keys",
467 | justification: "special case",
468 | node: allComments[17],
469 | },
470 | {
471 | type: "disable-next-line",
472 | value: "",
473 | justification: "",
474 | node: allComments[18],
475 | },
476 | {
477 | type: "disable-next-line",
478 | value: "json/no-duplicate-keys, json/no-empty-keys",
479 | justification: "another special case",
480 | node: allComments[19],
481 | },
482 | ],
483 | );
484 |
485 | assert.strictEqual(problems.length, 1);
486 | assert.strictEqual(problems[0].ruleId, null);
487 | assert.strictEqual(
488 | problems[0].message,
489 | "eslint-disable-line comment should not span multiple lines.",
490 | );
491 | assert.strictEqual(
492 | problems[0].loc,
493 | allComments[21].loc,
494 | );
495 | });
496 | });
497 | });
498 | });
499 | });
500 | });
501 |
--------------------------------------------------------------------------------
/tests/package/exports.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Tests for the package index's exports.
3 | * @author Steve Dodier-Lazaro
4 | */
5 |
6 | //------------------------------------------------------------------------------
7 | // Imports
8 | //------------------------------------------------------------------------------
9 |
10 | import * as exports from "../../src/index.js";
11 | import assert from "node:assert";
12 | import path from "node:path";
13 | import fs from "node:fs/promises";
14 | import { fileURLToPath, pathToFileURL } from "node:url";
15 |
16 | //-----------------------------------------------------------------------------
17 | // Helpers
18 | //-----------------------------------------------------------------------------
19 |
20 | const filename = fileURLToPath(import.meta.url);
21 | const dirname = path.dirname(filename);
22 | const rulesDir = path.resolve(dirname, "../../src/rules");
23 |
24 | //------------------------------------------------------------------------------
25 | // Tests
26 | //------------------------------------------------------------------------------
27 |
28 | describe("Package exports", () => {
29 | it("has the ESLint plugin as a default export", () => {
30 | assert.deepStrictEqual(Object.keys(exports.default), [
31 | "meta",
32 | "languages",
33 | "rules",
34 | "configs",
35 | ]);
36 | });
37 |
38 | it("has all available rules exported in the ESLint plugin", async () => {
39 | const allRules = (await fs.readdir(rulesDir))
40 | .filter(name => name.endsWith(".js"))
41 | .map(name => name.slice(0, -".js".length))
42 | .sort();
43 | const exportedRules = exports.default.rules;
44 |
45 | assert.deepStrictEqual(
46 | Object.keys(exportedRules).sort(),
47 | allRules,
48 | "Expected all rules to be exported in the ESLint plugin (`plugin.rules` in `src/index.js`)",
49 | );
50 |
51 | for (const [ruleName, rule] of Object.entries(exportedRules)) {
52 | assert.strictEqual(
53 | rule,
54 | (
55 | await import(
56 | pathToFileURL(path.resolve(rulesDir, `${ruleName}.js`))
57 | )
58 | ).default,
59 | `Expected ${ruleName}.js to be exported under key "${ruleName}" in the ESLint plugin (\`plugin.rules\` in \`src/index.js\`)`,
60 | );
61 | }
62 | });
63 |
64 | it("has a JSONLanguage export", () => {
65 | assert.ok(exports.JSONLanguage);
66 | });
67 |
68 | it("has a JSONSourceCode export", () => {
69 | assert.ok(exports.JSONSourceCode);
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/tests/plugin/eslint.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Integration tests with ESLint.
3 | * @author Milos Djermanovic
4 | */
5 |
6 | //------------------------------------------------------------------------------
7 | // Imports
8 | //------------------------------------------------------------------------------
9 |
10 | import json from "../../src/index.js";
11 | import ESLintAPI from "eslint";
12 | const { ESLint } = ESLintAPI;
13 |
14 | import assert from "node:assert";
15 | import dedent from "dedent";
16 |
17 | //-----------------------------------------------------------------------------
18 | // Tests
19 | //-----------------------------------------------------------------------------
20 |
21 | describe("when the plugin is used with ESLint", () => {
22 | describe("plugin configs", () => {
23 | Object.keys(json.configs).forEach(configName => {
24 | it(`Using "${configName}" config should not throw`, async () => {
25 | const config = {
26 | files: ["**/*.json"],
27 | language: "json/json",
28 | ...json.configs[configName],
29 | };
30 |
31 | const eslint = new ESLint({
32 | overrideConfigFile: true,
33 | overrideConfig: config,
34 | });
35 |
36 | await eslint.lintText("{}", { filePath: "test.json" });
37 | });
38 | });
39 | });
40 |
41 | describe("config comments", () => {
42 | ["jsonc", "json5"].forEach(language => {
43 | describe(`with ${language} language`, () => {
44 | const config = {
45 | files: [`**/*.${language}`],
46 | plugins: {
47 | json,
48 | },
49 | language: `json/${language}`,
50 | rules: {
51 | "json/no-empty-keys": "error",
52 | },
53 | };
54 | const filePath = `test.${language}`;
55 |
56 | let eslint = null;
57 |
58 | beforeEach(() => {
59 | eslint = new ESLint({
60 | overrideConfigFile: true,
61 | overrideConfig: config,
62 | });
63 | });
64 |
65 | afterEach(() => {
66 | eslint = null;
67 | });
68 |
69 | describe("rule configuration comments", () => {
70 | it("should be able to turn off rule", async () => {
71 | const [result] = await eslint.lintText(
72 | dedent`
73 | /* eslint json/no-empty-keys: off */
74 | {
75 | "": 42
76 | }
77 | `,
78 | {
79 | filePath,
80 | },
81 | );
82 |
83 | assert.strictEqual(result.messages.length, 0);
84 | assert.strictEqual(result.suppressedMessages.length, 0);
85 | });
86 |
87 | it("should be able to enable rule", async () => {
88 | const [result] = await eslint.lintText(
89 | dedent`
90 | /* eslint json/no-duplicate-keys: error */
91 | {
92 | "foo": 42,
93 | "foo": 43
94 | }
95 | `,
96 | {
97 | filePath,
98 | },
99 | );
100 |
101 | assert.strictEqual(result.messages.length, 1);
102 |
103 | assert.strictEqual(
104 | result.messages[0].ruleId,
105 | "json/no-duplicate-keys",
106 | );
107 | assert.strictEqual(
108 | result.messages[0].messageId,
109 | "duplicateKey",
110 | );
111 | assert.strictEqual(result.messages[0].severity, 2);
112 | assert.strictEqual(result.messages[0].line, 4);
113 |
114 | assert.strictEqual(result.suppressedMessages.length, 0);
115 | });
116 |
117 | it("should be able to enable/reconfigure multiple rules", async () => {
118 | const [result] = await eslint.lintText(
119 | dedent`
120 | /* eslint json/no-duplicate-keys: [2], json/no-empty-keys: [1] */
121 | {
122 | "": 42,
123 | "foo": 43,
124 | "foo": 44
125 | }
126 | `,
127 | {
128 | filePath,
129 | },
130 | );
131 |
132 | assert.strictEqual(result.messages.length, 2);
133 |
134 | assert.strictEqual(
135 | result.messages[0].ruleId,
136 | "json/no-empty-keys",
137 | );
138 | assert.strictEqual(
139 | result.messages[0].messageId,
140 | "emptyKey",
141 | );
142 | assert.strictEqual(result.messages[0].severity, 1);
143 | assert.strictEqual(result.messages[0].line, 3);
144 |
145 | assert.strictEqual(
146 | result.messages[1].ruleId,
147 | "json/no-duplicate-keys",
148 | );
149 | assert.strictEqual(
150 | result.messages[1].messageId,
151 | "duplicateKey",
152 | );
153 | assert.strictEqual(result.messages[1].severity, 2);
154 | assert.strictEqual(result.messages[1].line, 5);
155 |
156 | assert.strictEqual(result.suppressedMessages.length, 0);
157 | });
158 |
159 | it("should be reported when invalid", async () => {
160 | const [result] = await eslint.lintText(
161 | dedent`
162 | // foo
163 | /* eslint json/no-duplicate-keys: [2 */
164 | {
165 | }
166 | `,
167 | {
168 | filePath,
169 | },
170 | );
171 |
172 | assert.strictEqual(result.messages.length, 1);
173 |
174 | assert.strictEqual(result.messages[0].ruleId, null);
175 | assert.match(
176 | result.messages[0].message,
177 | /Failed to parse/u,
178 | );
179 | assert.strictEqual(result.messages[0].severity, 2);
180 | assert.strictEqual(result.messages[0].line, 2);
181 | assert.strictEqual(result.messages[0].column, 1);
182 | assert.strictEqual(result.messages[0].endLine, 2);
183 | assert.strictEqual(result.messages[0].endColumn, 40);
184 |
185 | assert.strictEqual(result.suppressedMessages.length, 0);
186 | });
187 | });
188 |
189 | describe("disable directives", () => {
190 | it("eslint-disable should suppress rule errors, eslint-enable should re-enable rule errors", async () => {
191 | const [result] = await eslint.lintText(
192 | dedent`
193 | [
194 | /* eslint-disable json/no-empty-keys -- allowed in first two elements */
195 | {
196 | "": 42
197 | },
198 | {
199 | "": 43
200 | },
201 | /* eslint-enable json/no-empty-keys */
202 | {
203 | "": 44
204 | }
205 | ]
206 | `,
207 | {
208 | filePath,
209 | },
210 | );
211 |
212 | assert.strictEqual(result.messages.length, 1);
213 |
214 | assert.strictEqual(
215 | result.messages[0].ruleId,
216 | "json/no-empty-keys",
217 | );
218 | assert.strictEqual(
219 | result.messages[0].messageId,
220 | "emptyKey",
221 | );
222 | assert.strictEqual(result.messages[0].severity, 2);
223 | assert.strictEqual(result.messages[0].line, 11);
224 |
225 | assert.strictEqual(result.suppressedMessages.length, 2);
226 |
227 | assert.strictEqual(
228 | result.suppressedMessages[0].ruleId,
229 | "json/no-empty-keys",
230 | );
231 | assert.strictEqual(
232 | result.suppressedMessages[0].messageId,
233 | "emptyKey",
234 | );
235 | assert.strictEqual(
236 | result.suppressedMessages[0].severity,
237 | 2,
238 | );
239 | assert.strictEqual(
240 | result.suppressedMessages[0].line,
241 | 4,
242 | );
243 | assert.strictEqual(
244 | result.suppressedMessages[0].suppressions.length,
245 | 1,
246 | );
247 | assert.strictEqual(
248 | result.suppressedMessages[0].suppressions[0].kind,
249 | "directive",
250 | );
251 | assert.strictEqual(
252 | result.suppressedMessages[0].suppressions[0]
253 | .justification,
254 | "allowed in first two elements",
255 | );
256 |
257 | assert.strictEqual(
258 | result.suppressedMessages[1].ruleId,
259 | "json/no-empty-keys",
260 | );
261 | assert.strictEqual(
262 | result.suppressedMessages[1].messageId,
263 | "emptyKey",
264 | );
265 | assert.strictEqual(
266 | result.suppressedMessages[1].severity,
267 | 2,
268 | );
269 | assert.strictEqual(
270 | result.suppressedMessages[1].line,
271 | 7,
272 | );
273 | assert.strictEqual(
274 | result.suppressedMessages[1].suppressions.length,
275 | 1,
276 | );
277 | assert.strictEqual(
278 | result.suppressedMessages[1].suppressions[0].kind,
279 | "directive",
280 | );
281 | assert.strictEqual(
282 | result.suppressedMessages[1].suppressions[0]
283 | .justification,
284 | "allowed in first two elements",
285 | );
286 | });
287 |
288 | it("eslint-disable should suppress errors from multiple rules", async () => {
289 | const [result] = await eslint.lintText(
290 | dedent`
291 | /* eslint json/no-duplicate-keys: warn */
292 | /* eslint-disable json/no-empty-keys, json/no-duplicate-keys */
293 | {
294 | "": 42,
295 | "foo": 5,
296 | "foo": 6
297 | }
298 | `,
299 | {
300 | filePath,
301 | },
302 | );
303 |
304 | assert.strictEqual(result.suppressedMessages.length, 2);
305 |
306 | assert.strictEqual(
307 | result.suppressedMessages[0].ruleId,
308 | "json/no-empty-keys",
309 | );
310 | assert.strictEqual(
311 | result.suppressedMessages[0].messageId,
312 | "emptyKey",
313 | );
314 | assert.strictEqual(
315 | result.suppressedMessages[0].severity,
316 | 2,
317 | );
318 | assert.strictEqual(
319 | result.suppressedMessages[0].line,
320 | 4,
321 | );
322 | assert.strictEqual(
323 | result.suppressedMessages[0].suppressions.length,
324 | 1,
325 | );
326 | assert.strictEqual(
327 | result.suppressedMessages[0].suppressions[0].kind,
328 | "directive",
329 | );
330 | assert.strictEqual(
331 | result.suppressedMessages[0].suppressions[0]
332 | .justification,
333 | "",
334 | );
335 |
336 | assert.strictEqual(
337 | result.suppressedMessages[1].ruleId,
338 | "json/no-duplicate-keys",
339 | );
340 | assert.strictEqual(
341 | result.suppressedMessages[1].messageId,
342 | "duplicateKey",
343 | );
344 | assert.strictEqual(
345 | result.suppressedMessages[1].severity,
346 | 1,
347 | );
348 | assert.strictEqual(
349 | result.suppressedMessages[1].line,
350 | 6,
351 | );
352 | assert.strictEqual(
353 | result.suppressedMessages[1].suppressions.length,
354 | 1,
355 | );
356 | assert.strictEqual(
357 | result.suppressedMessages[1].suppressions[0].kind,
358 | "directive",
359 | );
360 | assert.strictEqual(
361 | result.suppressedMessages[1].suppressions[0]
362 | .justification,
363 | "",
364 | );
365 | });
366 |
367 | it("eslint-disable-line should suppress rule errors on the same line", async () => {
368 | const [result] = await eslint.lintText(
369 | dedent`
370 | {
371 | "": 42, // eslint-disable-line json/no-empty-keys -- allowed here
372 | "": 43
373 | }
374 | `,
375 | {
376 | filePath,
377 | },
378 | );
379 |
380 | assert.strictEqual(result.messages.length, 1);
381 |
382 | assert.strictEqual(
383 | result.messages[0].ruleId,
384 | "json/no-empty-keys",
385 | );
386 | assert.strictEqual(
387 | result.messages[0].messageId,
388 | "emptyKey",
389 | );
390 | assert.strictEqual(result.messages[0].severity, 2);
391 | assert.strictEqual(result.messages[0].line, 3);
392 |
393 | assert.strictEqual(result.suppressedMessages.length, 1);
394 |
395 | assert.strictEqual(
396 | result.suppressedMessages[0].ruleId,
397 | "json/no-empty-keys",
398 | );
399 | assert.strictEqual(
400 | result.suppressedMessages[0].messageId,
401 | "emptyKey",
402 | );
403 | assert.strictEqual(
404 | result.suppressedMessages[0].severity,
405 | 2,
406 | );
407 | assert.strictEqual(
408 | result.suppressedMessages[0].line,
409 | 2,
410 | );
411 | assert.strictEqual(
412 | result.suppressedMessages[0].suppressions.length,
413 | 1,
414 | );
415 | assert.strictEqual(
416 | result.suppressedMessages[0].suppressions[0].kind,
417 | "directive",
418 | );
419 | assert.strictEqual(
420 | result.suppressedMessages[0].suppressions[0]
421 | .justification,
422 | "allowed here",
423 | );
424 | });
425 |
426 | it("eslint-disable-next-line should suppress rule errors on the next line", async () => {
427 | const [result] = await eslint.lintText(
428 | dedent`
429 | {
430 | "": 42, // eslint-disable-next-line json/no-empty-keys -- allowed here
431 | "": 43
432 | }
433 | `,
434 | {
435 | filePath,
436 | },
437 | );
438 |
439 | assert.strictEqual(result.messages.length, 1);
440 |
441 | assert.strictEqual(
442 | result.messages[0].ruleId,
443 | "json/no-empty-keys",
444 | );
445 | assert.strictEqual(
446 | result.messages[0].messageId,
447 | "emptyKey",
448 | );
449 | assert.strictEqual(result.messages[0].severity, 2);
450 | assert.strictEqual(result.messages[0].line, 2);
451 |
452 | assert.strictEqual(result.suppressedMessages.length, 1);
453 |
454 | assert.strictEqual(
455 | result.suppressedMessages[0].ruleId,
456 | "json/no-empty-keys",
457 | );
458 | assert.strictEqual(
459 | result.suppressedMessages[0].messageId,
460 | "emptyKey",
461 | );
462 | assert.strictEqual(
463 | result.suppressedMessages[0].severity,
464 | 2,
465 | );
466 | assert.strictEqual(
467 | result.suppressedMessages[0].line,
468 | 3,
469 | );
470 | assert.strictEqual(
471 | result.suppressedMessages[0].suppressions.length,
472 | 1,
473 | );
474 | assert.strictEqual(
475 | result.suppressedMessages[0].suppressions[0].kind,
476 | "directive",
477 | );
478 | assert.strictEqual(
479 | result.suppressedMessages[0].suppressions[0]
480 | .justification,
481 | "allowed here",
482 | );
483 | });
484 |
485 | it("multiline eslint-disable-next-line should suppress rule errors on the next line", async () => {
486 | const [result] = await eslint.lintText(
487 | dedent`
488 | {
489 | /* eslint-disable-next-line
490 | json/no-empty-keys
491 | */
492 | "": 42
493 | }
494 | `,
495 | {
496 | filePath,
497 | },
498 | );
499 |
500 | assert.strictEqual(result.messages.length, 0);
501 |
502 | assert.strictEqual(result.suppressedMessages.length, 1);
503 |
504 | assert.strictEqual(
505 | result.suppressedMessages[0].ruleId,
506 | "json/no-empty-keys",
507 | );
508 | assert.strictEqual(
509 | result.suppressedMessages[0].messageId,
510 | "emptyKey",
511 | );
512 | assert.strictEqual(
513 | result.suppressedMessages[0].severity,
514 | 2,
515 | );
516 | assert.strictEqual(
517 | result.suppressedMessages[0].line,
518 | 5,
519 | );
520 | assert.strictEqual(
521 | result.suppressedMessages[0].suppressions.length,
522 | 1,
523 | );
524 | assert.strictEqual(
525 | result.suppressedMessages[0].suppressions[0].kind,
526 | "directive",
527 | );
528 | assert.strictEqual(
529 | result.suppressedMessages[0].suppressions[0]
530 | .justification,
531 | "",
532 | );
533 | });
534 |
535 | it("multiline eslint-disable-line should be reported as error and not suppress any rule errors", async () => {
536 | const [result] = await eslint.lintText(
537 | dedent`
538 | {
539 | "": 42, /* eslint-disable-line
540 | json/no-empty-keys
541 | */ "": 43
542 | }
543 | `,
544 | {
545 | filePath,
546 | },
547 | );
548 |
549 | assert.strictEqual(result.messages.length, 3);
550 |
551 | assert.strictEqual(
552 | result.messages[0].ruleId,
553 | "json/no-empty-keys",
554 | );
555 | assert.strictEqual(
556 | result.messages[0].messageId,
557 | "emptyKey",
558 | );
559 | assert.strictEqual(result.messages[0].severity, 2);
560 | assert.strictEqual(result.messages[0].line, 2);
561 |
562 | assert.strictEqual(result.messages[1].ruleId, null);
563 | assert.strictEqual(
564 | result.messages[1].message,
565 | "eslint-disable-line comment should not span multiple lines.",
566 | );
567 | assert.strictEqual(result.messages[1].severity, 2);
568 | assert.strictEqual(result.messages[1].line, 2);
569 | assert.strictEqual(result.messages[1].column, 10);
570 | assert.strictEqual(result.messages[1].endLine, 4);
571 | assert.strictEqual(result.messages[1].endColumn, 5);
572 |
573 | assert.strictEqual(
574 | result.messages[2].ruleId,
575 | "json/no-empty-keys",
576 | );
577 | assert.strictEqual(
578 | result.messages[2].messageId,
579 | "emptyKey",
580 | );
581 | assert.strictEqual(result.messages[2].severity, 2);
582 | assert.strictEqual(result.messages[2].line, 4);
583 |
584 | assert.strictEqual(result.suppressedMessages.length, 0);
585 | });
586 | });
587 | });
588 | });
589 | });
590 | });
591 |
--------------------------------------------------------------------------------
/tests/rules/no-duplicate-keys.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Tests for no-duplicate-keys rule.
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //------------------------------------------------------------------------------
7 | // Imports
8 | //------------------------------------------------------------------------------
9 |
10 | import rule from "../../src/rules/no-duplicate-keys.js";
11 | import json from "../../src/index.js";
12 | import { RuleTester } from "eslint";
13 |
14 | //------------------------------------------------------------------------------
15 | // Tests
16 | //------------------------------------------------------------------------------
17 |
18 | const ruleTester = new RuleTester({
19 | plugins: {
20 | json,
21 | },
22 | language: "json/json",
23 | });
24 |
25 | ruleTester.run("no-duplicate-keys", rule, {
26 | valid: [
27 | '{"foo": 1, "bar": 2}',
28 | '{"foo": 1, "bar": 2, "baz": 3}',
29 | "[]",
30 | "{}",
31 | '{"foo": 1, "bar": {"bar": 2}}',
32 | '{"foo": { "bar": 5 }, "bar": 6 }',
33 | {
34 | code: "{foo: 1, bar: {bar: 2}}",
35 | language: "json/json5",
36 | },
37 | ],
38 | invalid: [
39 | {
40 | code: '{"foo": 1, "foo": 2}',
41 | errors: [
42 | {
43 | messageId: "duplicateKey",
44 | data: { key: "foo" },
45 | line: 1,
46 | column: 12,
47 | endLine: 1,
48 | endColumn: 17,
49 | },
50 | ],
51 | },
52 | {
53 | code: `{
54 | "foo": {
55 | "bar": 5
56 | },
57 | "foo": 6
58 | }`,
59 | errors: [
60 | {
61 | messageId: "duplicateKey",
62 | data: { key: "foo" },
63 | line: 5,
64 | column: 5,
65 | endLine: 5,
66 | endColumn: 10,
67 | },
68 | ],
69 | },
70 | {
71 | code: "{foo: 1, foo: 2}",
72 | language: "json/json5",
73 | errors: [
74 | {
75 | messageId: "duplicateKey",
76 | data: { key: "foo" },
77 | line: 1,
78 | column: 10,
79 | endLine: 1,
80 | endColumn: 13,
81 | },
82 | ],
83 | },
84 | {
85 | code: `{
86 | foo: {
87 | "bar": 5
88 | },
89 | foo: 6
90 | }`,
91 | language: "json/json5",
92 | errors: [
93 | {
94 | messageId: "duplicateKey",
95 | data: { key: "foo" },
96 | line: 5,
97 | column: 5,
98 | endLine: 5,
99 | endColumn: 8,
100 | },
101 | ],
102 | },
103 | {
104 | code: '{"foo": 1, foo: 2}',
105 | language: "json/json5",
106 | errors: [
107 | {
108 | messageId: "duplicateKey",
109 | data: { key: "foo" },
110 | line: 1,
111 | column: 12,
112 | endLine: 1,
113 | endColumn: 15,
114 | },
115 | ],
116 | },
117 | {
118 | code: `{
119 | foo: {
120 | "bar": 5
121 | },
122 | "foo": 6
123 | }`,
124 | language: "json/json5",
125 | errors: [
126 | {
127 | messageId: "duplicateKey",
128 | data: { key: "foo" },
129 | line: 5,
130 | column: 5,
131 | endLine: 5,
132 | endColumn: 10,
133 | },
134 | ],
135 | },
136 | {
137 | code: '{"f\\u006fot": 1, "fo\\u006ft": 2}',
138 | errors: [
139 | {
140 | messageId: "duplicateKey",
141 | data: { key: "foot" },
142 | line: 1,
143 | column: 18,
144 | endLine: 1,
145 | endColumn: 29,
146 | },
147 | ],
148 | },
149 | {
150 | code: '{"f\\u006fot": 1, "fo\\u006ft": 2}',
151 | language: "json/jsonc",
152 | errors: [
153 | {
154 | messageId: "duplicateKey",
155 | data: { key: "foot" },
156 | line: 1,
157 | column: 18,
158 | endLine: 1,
159 | endColumn: 29,
160 | },
161 | ],
162 | },
163 | {
164 | code: '{"f\\u006fot": 1, "fo\\u006ft": 2}',
165 | language: "json/json5",
166 | errors: [
167 | {
168 | messageId: "duplicateKey",
169 | data: { key: "foot" },
170 | line: 1,
171 | column: 18,
172 | endLine: 1,
173 | endColumn: 29,
174 | },
175 | ],
176 | },
177 | {
178 | code: "{f\\u006fot: 1, fo\\u006ft: 2}",
179 | language: "json/json5",
180 | errors: [
181 | {
182 | messageId: "duplicateKey",
183 | data: { key: "foot" },
184 | line: 1,
185 | column: 16,
186 | endLine: 1,
187 | endColumn: 25,
188 | },
189 | ],
190 | },
191 | ],
192 | });
193 |
--------------------------------------------------------------------------------
/tests/rules/no-empty-keys.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Tests for no-empty-keys rule.
3 | * @author Nicholas C. Zakas
4 | */
5 |
6 | //------------------------------------------------------------------------------
7 | // Imports
8 | //------------------------------------------------------------------------------
9 |
10 | import rule from "../../src/rules/no-empty-keys.js";
11 | import json from "../../src/index.js";
12 | import { RuleTester } from "eslint";
13 |
14 | //------------------------------------------------------------------------------
15 | // Tests
16 | //------------------------------------------------------------------------------
17 |
18 | const ruleTester = new RuleTester({
19 | plugins: {
20 | json,
21 | },
22 | language: "json/json",
23 | });
24 |
25 | ruleTester.run("no-empty-keys", rule, {
26 | valid: [
27 | '{"foo": 1, "bar": 2}',
28 | {
29 | code: '{"foo": 1, "bar": 2, "baz": 3}',
30 | language: "json/json5",
31 | },
32 | {
33 | code: '{foo: 1, bar: 2, "baz": 3}',
34 | language: "json/json5",
35 | },
36 | ],
37 | invalid: [
38 | {
39 | code: '{"": 1}',
40 | errors: [
41 | {
42 | messageId: "emptyKey",
43 | line: 1,
44 | column: 2,
45 | endLine: 1,
46 | endColumn: 4,
47 | },
48 | ],
49 | },
50 | {
51 | code: '{" ": 1}',
52 | errors: [
53 | {
54 | messageId: "emptyKey",
55 | line: 1,
56 | column: 2,
57 | endLine: 1,
58 | endColumn: 6,
59 | },
60 | ],
61 | },
62 | {
63 | code: "{'': 1}",
64 | language: "json/json5",
65 | errors: [
66 | {
67 | messageId: "emptyKey",
68 | line: 1,
69 | column: 2,
70 | endLine: 1,
71 | endColumn: 4,
72 | },
73 | ],
74 | },
75 | {
76 | code: "{' ': 1}",
77 | language: "json/json5",
78 | errors: [
79 | {
80 | messageId: "emptyKey",
81 | line: 1,
82 | column: 2,
83 | endLine: 1,
84 | endColumn: 6,
85 | },
86 | ],
87 | },
88 | ],
89 | });
90 |
--------------------------------------------------------------------------------
/tests/rules/no-unnormalized-keys.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Tests for no-unnormalized-keys rule.
3 | * @author Bradley Meck Farias
4 | */
5 |
6 | //------------------------------------------------------------------------------
7 | // Imports
8 | //------------------------------------------------------------------------------
9 |
10 | import rule from "../../src/rules/no-unnormalized-keys.js";
11 | import json from "../../src/index.js";
12 | import { RuleTester } from "eslint";
13 |
14 | //------------------------------------------------------------------------------
15 | // Tests
16 | //------------------------------------------------------------------------------
17 |
18 | const ruleTester = new RuleTester({
19 | plugins: {
20 | json,
21 | },
22 | language: "json/json",
23 | });
24 |
25 | const o = "\u1E9B\u0323";
26 |
27 | ruleTester.run("no-unnormalized-keys", rule, {
28 | valid: [
29 | `{"${o}":"NFC"}`,
30 | {
31 | code: `{"${o}":"NFC"}`,
32 | options: [{ form: "NFC" }],
33 | },
34 | {
35 | code: `{"${o.normalize("NFD")}":"NFD"}`,
36 | options: [{ form: "NFD" }],
37 | },
38 | {
39 | code: `{"${o.normalize("NFKC")}":"NFKC"}`,
40 | options: [{ form: "NFKC" }],
41 | },
42 | {
43 | code: `{"${o.normalize("NFKD")}":"NFKD"}`,
44 | options: [{ form: "NFKD" }],
45 | },
46 | ],
47 | invalid: [
48 | {
49 | code: `{"${o.normalize("NFD")}":"NFD"}`,
50 | errors: [
51 | {
52 | messageId: "unnormalizedKey",
53 | data: { key: o.normalize("NFD") },
54 | line: 1,
55 | column: 2,
56 | endLine: 1,
57 | endColumn: 7,
58 | },
59 | ],
60 | },
61 | {
62 | code: `{${o.normalize("NFD")}:"NFD"}`,
63 | language: "json/json5",
64 | errors: [
65 | {
66 | messageId: "unnormalizedKey",
67 | data: { key: o.normalize("NFD") },
68 | line: 1,
69 | column: 2,
70 | endLine: 1,
71 | endColumn: 5,
72 | },
73 | ],
74 | },
75 | {
76 | code: `{"${o.normalize("NFKC")}":"NFKC"}`,
77 | options: [{ form: "NFKD" }],
78 | errors: [
79 | {
80 | messageId: "unnormalizedKey",
81 | data: { key: o.normalize("NFKC") },
82 | line: 1,
83 | column: 2,
84 | endLine: 1,
85 | endColumn: 5,
86 | },
87 | ],
88 | },
89 | ],
90 | });
91 |
--------------------------------------------------------------------------------
/tests/rules/no-unsafe-values.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Tests for no-unsafe-values rule.
3 | * @author Bradley Meck Farias
4 | */
5 |
6 | //------------------------------------------------------------------------------
7 | // Imports
8 | //------------------------------------------------------------------------------
9 |
10 | import rule from "../../src/rules/no-unsafe-values.js";
11 | import json from "../../src/index.js";
12 | import { RuleTester } from "eslint";
13 |
14 | //------------------------------------------------------------------------------
15 | // Tests
16 | //------------------------------------------------------------------------------
17 |
18 | const ruleTester = new RuleTester({
19 | plugins: {
20 | json,
21 | },
22 | language: "json/json",
23 | });
24 |
25 | ruleTester.run("no-unsafe-values", rule, {
26 | valid: [
27 | "123",
28 | {
29 | code: "1234",
30 | language: "json/json5",
31 | },
32 | {
33 | code: "12345",
34 | language: "json/json5",
35 | },
36 | '"🔥"',
37 | '"\\ud83d\\udd25"',
38 | "0.00000",
39 | "0e0000000",
40 | "0.00000e0000",
41 | ],
42 | invalid: [
43 | {
44 | code: "2e308",
45 | errors: [
46 | {
47 | messageId: "unsafeNumber",
48 | data: {
49 | value: "2e308",
50 | },
51 | line: 1,
52 | column: 1,
53 | endLine: 1,
54 | endColumn: 6,
55 | },
56 | ],
57 | },
58 | {
59 | code: "-2e308",
60 | errors: [
61 | {
62 | messageId: "unsafeNumber",
63 | data: {
64 | value: "-2e308",
65 | },
66 | line: 1,
67 | column: 1,
68 | endLine: 1,
69 | endColumn: 7,
70 | },
71 | ],
72 | },
73 | {
74 | code: '"\ud83d"',
75 | errors: [
76 | {
77 | messageId: "loneSurrogate",
78 | line: 1,
79 | column: 1,
80 | endLine: 1,
81 | endColumn: 4,
82 | },
83 | ],
84 | },
85 | {
86 | code: '"\\ud83d"',
87 | errors: [
88 | {
89 | messageId: "loneSurrogate",
90 | data: { surrogate: "\\ud83d" },
91 | line: 1,
92 | column: 1,
93 | endLine: 1,
94 | endColumn: 9,
95 | },
96 | ],
97 | },
98 | {
99 | code: '"\udd25"',
100 | errors: [
101 | {
102 | messageId: "loneSurrogate",
103 | data: { surrogate: "\\udd25" },
104 | line: 1,
105 | column: 1,
106 | endLine: 1,
107 | endColumn: 4,
108 | },
109 | ],
110 | },
111 | {
112 | code: '"\\udd25"',
113 | errors: [
114 | {
115 | messageId: "loneSurrogate",
116 | data: { surrogate: "\\udd25" },
117 | line: 1,
118 | column: 1,
119 | endLine: 1,
120 | endColumn: 9,
121 | },
122 | ],
123 | },
124 | {
125 | code: '"\ud83d\ud83d"',
126 | errors: [
127 | {
128 | message: "Lone surrogate '\\ud83d' found.",
129 | line: 1,
130 | column: 1,
131 | endLine: 1,
132 | endColumn: 5,
133 | },
134 | {
135 | message: "Lone surrogate '\\ud83d' found.",
136 | line: 1,
137 | column: 1,
138 | endLine: 1,
139 | endColumn: 5,
140 | },
141 | ],
142 | },
143 | {
144 | code: "1e-400",
145 | errors: [
146 | {
147 | messageId: "unsafeZero",
148 | data: {
149 | value: "1e-400",
150 | },
151 | line: 1,
152 | column: 1,
153 | endLine: 1,
154 | endColumn: 7,
155 | },
156 | ],
157 | },
158 | {
159 | code: "-1e-400",
160 | errors: [
161 | {
162 | messageId: "unsafeZero",
163 | data: {
164 | value: "-1e-400",
165 | },
166 | line: 1,
167 | column: 1,
168 | endLine: 1,
169 | endColumn: 8,
170 | },
171 | ],
172 | },
173 | {
174 | code: "0.01e-400",
175 | errors: [
176 | {
177 | messageId: "unsafeZero",
178 | data: {
179 | value: "0.01e-400",
180 | },
181 | line: 1,
182 | column: 1,
183 | endLine: 1,
184 | endColumn: 10,
185 | },
186 | ],
187 | },
188 | {
189 | code: "-10.2e-402",
190 | errors: [
191 | {
192 | messageId: "unsafeZero",
193 | data: {
194 | value: "-10.2e-402",
195 | },
196 | line: 1,
197 | column: 1,
198 | endLine: 1,
199 | endColumn: 11,
200 | },
201 | ],
202 | },
203 | {
204 | code: `0.${"0".repeat(400)}1`,
205 | errors: [
206 | {
207 | messageId: "unsafeZero",
208 | data: {
209 | value: `0.${"0".repeat(400)}1`,
210 | },
211 | line: 1,
212 | column: 1,
213 | endLine: 1,
214 | endColumn: 404,
215 | },
216 | ],
217 | },
218 | {
219 | code: "9007199254740992",
220 | errors: [
221 | {
222 | messageId: "unsafeInteger",
223 | data: {
224 | value: "9007199254740992",
225 | },
226 | line: 1,
227 | column: 1,
228 | endLine: 1,
229 | endColumn: 17,
230 | },
231 | ],
232 | },
233 | {
234 | code: "-9007199254740992",
235 | errors: [
236 | {
237 | messageId: "unsafeInteger",
238 | data: {
239 | value: "-9007199254740992",
240 | },
241 | line: 1,
242 | column: 1,
243 | endLine: 1,
244 | endColumn: 18,
245 | },
246 | ],
247 | },
248 | {
249 | code: "2.2250738585072009e-308",
250 | errors: [
251 | {
252 | messageId: "subnormal",
253 | data: {
254 | value: "2.2250738585072009e-308",
255 | },
256 | line: 1,
257 | column: 1,
258 | endLine: 1,
259 | endColumn: 24,
260 | },
261 | ],
262 | },
263 | {
264 | code: "-2.2250738585072009e-308",
265 | errors: [
266 | {
267 | messageId: "subnormal",
268 | data: {
269 | value: "-2.2250738585072009e-308",
270 | },
271 | line: 1,
272 | column: 1,
273 | endLine: 1,
274 | endColumn: 25,
275 | },
276 | ],
277 | },
278 | ],
279 | });
280 |
--------------------------------------------------------------------------------
/tests/rules/sort-keys.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Tests for sort-keys rule. Cribbed from https://github.com/eslint/eslint/blob/main/tests/lib/rules/sort-keys.js. TODO: How to maintain parity with eslint/sort-keys?
3 | * @author Robin Thomas
4 | */
5 |
6 | import rule from "../../src/rules/sort-keys.js";
7 | import json from "../../src/index.js";
8 | import { RuleTester } from "eslint";
9 |
10 | const ruleTester = new RuleTester({
11 | plugins: {
12 | json,
13 | },
14 | language: "json/json",
15 | });
16 |
17 | ruleTester.run("sort-keys", rule, {
18 | valid: [
19 | // default (asc)
20 | { code: '{"":1, "a":2}', options: [] },
21 | { code: '{"_":2, "a":1, "b":3}', options: [] },
22 | { code: '{"a":1, "b":3, "c":2}', options: [] },
23 | { code: '{"a":2, "b":3, "b_":1}', options: [] },
24 | { code: '{"C":3, "b_":1, "c":2}', options: [] },
25 | { code: '{"$":1, "A":3, "_":2, "a":4}', options: [] },
26 | {
27 | code: `{$:1, 'A':3, "_":2, a:4}`,
28 | language: `json/json5`,
29 | options: [],
30 | },
31 | { code: '{"1":1, "11":2, "2":4, "A":3}', options: [] },
32 | { code: '{"#":1, "Z":2, "À":3, "è":4}', options: [] },
33 | { code: '{"#":1, Z:2, À:3, è:4}', language: `json/json5`, options: [] },
34 |
35 | // nested
36 | { code: '{"a":1, "b":{"x":1, "y":1}, "c":1}', options: [] },
37 | {
38 | code: `
39 | {
40 | "a":1,
41 | "b": {
42 | "x":1,
43 | "y":1
44 | },
45 | "c":1
46 | }
47 | `,
48 | options: [],
49 | },
50 | {
51 | code: `
52 | [
53 | {
54 | "a":1,
55 | "b": {
56 | "x":1,
57 | "y":1
58 | }
59 | },
60 | {
61 | "c":1,
62 | "d":1
63 | }
64 | ]
65 | `,
66 | options: [],
67 | },
68 | {
69 | code: `
70 | [
71 | {
72 | "a":1,
73 | b: {
74 | "x":1,
75 | y:1
76 | }
77 | },
78 | {
79 | "c":1,
80 | d:1
81 | }
82 | ]
83 | `,
84 | language: "json/json5",
85 | options: [],
86 | errors: [
87 | {
88 | messageId: "sortKeys",
89 | },
90 | ],
91 | },
92 |
93 | // asc
94 | {
95 | code: '{"_":2, "a":1, "b":3} // asc"',
96 | language: "json/jsonc",
97 | options: ["asc"],
98 | },
99 | { code: '{"a":1, "b":3, "c":2}', options: ["asc"] },
100 | { code: '{"a":2, "b":3, "b_":1}', options: ["asc"] },
101 | { code: '{"C":3, "b_":1, "c":2}', options: ["asc"] },
102 | { code: '{"$":1, "A":3, "_":2, "a":4}', options: ["asc"] },
103 | { code: '{"1":1, "11":2, "2":4, "A":3}', options: ["asc"] },
104 | { code: '{"#":1, "Z":2, "À":3, "è":4}', options: ["asc"] },
105 |
106 | // asc, minKeys should ignore unsorted keys when number of keys is less than minKeys
107 | { code: '{"a":1, "c":2, "b":3}', options: ["asc", { minKeys: 4 }] },
108 |
109 | // asc, insensitive
110 | {
111 | code: '{"_":2, "a":1, "b":3} // asc, insensitive',
112 | language: "json/jsonc",
113 | options: ["asc", { caseSensitive: false }],
114 | },
115 | {
116 | code: '{"a":1, "b":3, "c":2}',
117 | options: ["asc", { caseSensitive: false }],
118 | },
119 | {
120 | code: '{"a":2, "b":3, "b_":1}',
121 | options: ["asc", { caseSensitive: false }],
122 | },
123 | {
124 | code: '{"b_":1, "C":3, "c":2}',
125 | options: ["asc", { caseSensitive: false }],
126 | },
127 | {
128 | code: '{"b_":1, "c":3, "C":2}',
129 | options: ["asc", { caseSensitive: false }],
130 | },
131 | {
132 | code: '{"$":1, "_":2, "A":3, "a":4}',
133 | options: ["asc", { caseSensitive: false }],
134 | },
135 | {
136 | code: '{"1":1, "11":2, "2":4, "A":3}',
137 | options: ["asc", { caseSensitive: false }],
138 | },
139 | {
140 | code: '{"#":1, "Z":2, "À":3, "è":4}',
141 | options: ["asc", { caseSensitive: false }],
142 | },
143 |
144 | // asc, insensitive, minKeys should ignore unsorted keys when number of keys is less than minKeys
145 | {
146 | code: '{"$":1, "A":3, "_":2, "a":4}',
147 | options: ["asc", { caseSensitive: false, minKeys: 5 }],
148 | },
149 |
150 | // asc, natural
151 | {
152 | code: '{"_":2, "a":1, "b":3} // asc, natural',
153 | language: "json/jsonc",
154 | options: ["asc", { natural: true }],
155 | },
156 | {
157 | code: '{"a":1, "b":3, "c":2}',
158 | options: ["asc", { natural: true }],
159 | },
160 | {
161 | code: '{"a":2, "b":3, "b_":1}',
162 | options: ["asc", { natural: true }],
163 | },
164 | {
165 | code: '{"C":3, "b_":1, "c":2}',
166 | options: ["asc", { natural: true }],
167 | },
168 | {
169 | code: '{"$":1, "_":2, "A":3, "a":4}',
170 | options: ["asc", { natural: true }],
171 | },
172 | {
173 | code: '{"1":1, "2":4, "11":2, "A":3}',
174 | options: ["asc", { natural: true }],
175 | },
176 | {
177 | code: '{"#":1, "Z":2, "À":3, "è":4}',
178 | options: ["asc", { natural: true }],
179 | },
180 |
181 | // asc, natural, minKeys should ignore unsorted keys when number of keys is less than minKeys
182 | {
183 | code: '{"b_":1, "a":2, "b":3}',
184 | options: ["asc", { natural: true, minKeys: 4 }],
185 | },
186 |
187 | // asc, natural, insensitive
188 | {
189 | code: '{"_":2, "a":1, "b":3} // asc, natural, insensitive',
190 | language: "json/jsonc",
191 | options: ["asc", { natural: true, caseSensitive: false }],
192 | },
193 | {
194 | code: '{"a":1, "b":3, "c":2}',
195 | options: ["asc", { natural: true, caseSensitive: false }],
196 | },
197 | {
198 | code: '{"a":2, "b":3, "b_":1}',
199 | options: ["asc", { natural: true, caseSensitive: false }],
200 | },
201 | {
202 | code: '{"b_":1, "C":3, "c":2}',
203 | options: ["asc", { natural: true, caseSensitive: false }],
204 | },
205 | {
206 | code: '{"b_":1, "c":3, "C":2}',
207 | options: ["asc", { natural: true, caseSensitive: false }],
208 | },
209 | {
210 | code: '{"$":1, "_":2, "A":3, "a":4}',
211 | options: ["asc", { natural: true, caseSensitive: false }],
212 | },
213 | {
214 | code: '{"1":1, "2":4, "11":2, "A":3}',
215 | options: ["asc", { natural: true, caseSensitive: false }],
216 | },
217 | {
218 | code: '{"#":1, "Z":2, "À":3, "è":4}',
219 | options: ["asc", { natural: true, caseSensitive: false }],
220 | },
221 |
222 | // asc, natural, insensitive, minKeys should ignore unsorted keys when number of keys is less than minKeys
223 | {
224 | code: '{"a":1, "_":2, "b":3}',
225 | options: [
226 | "asc",
227 | { natural: true, caseSensitive: false, minKeys: 4 },
228 | ],
229 | },
230 |
231 | // desc
232 | {
233 | code: '{"b":3, "a":1, "_":2} // desc',
234 | language: "json/jsonc",
235 | options: ["desc"],
236 | },
237 | {
238 | code: `{b:3, "a":1, '_':2} // desc`,
239 | language: "json/json5",
240 | options: ["desc"],
241 | },
242 | { code: '{"c":2, "b":3, "a":1}', options: ["desc"] },
243 | { code: '{"b_":1, "b":3, "a":2}', options: ["desc"] },
244 | { code: '{"c":2, "b_":1, "C":3}', options: ["desc"] },
245 | { code: '{"a":4, "_":2, "A":3, "$":1}', options: ["desc"] },
246 | { code: '{"A":3, "2":4, "11":2, "1":1}', options: ["desc"] },
247 | { code: '{"è":4, "À":3, "Z":2, "#":1}', options: ["desc"] },
248 |
249 | // desc, minKeys should ignore unsorted keys when number of keys is less than minKeys
250 | {
251 | code: '{"a":1, "c":2, "b":3}',
252 | options: ["desc", { minKeys: 4 }],
253 | },
254 |
255 | // desc, insensitive
256 | {
257 | code: '{"b":3, "a":1, "_":2} // desc, insensitive',
258 | language: "json/jsonc",
259 | options: ["desc", { caseSensitive: false }],
260 | },
261 | {
262 | code: '{"c":2, "b":3, "a":1}',
263 | options: ["desc", { caseSensitive: false }],
264 | },
265 | {
266 | code: '{"b_":1, "b":3, "a":2}',
267 | options: ["desc", { caseSensitive: false }],
268 | },
269 | {
270 | code: '{"c":2, "C":3, "b_":1}',
271 | options: ["desc", { caseSensitive: false }],
272 | },
273 | {
274 | code: '{"C":2, "c":3, "b_":1}',
275 | options: ["desc", { caseSensitive: false }],
276 | },
277 | {
278 | code: '{"a":4, "A":3, "_":2, "$":1}',
279 | options: ["desc", { caseSensitive: false }],
280 | },
281 | {
282 | code: '{"A":3, "2":4, "11":2, "1":1}',
283 | options: ["desc", { caseSensitive: false }],
284 | },
285 | {
286 | code: '{"è":4, "À":3, "Z":2, "#":1}',
287 | options: ["desc", { caseSensitive: false }],
288 | },
289 |
290 | // desc, insensitive, minKeys should ignore unsorted keys when number of keys is less than minKeys
291 | {
292 | code: '{"$":1, "_":2, "A":3, "a":4}',
293 | options: ["desc", { caseSensitive: false, minKeys: 5 }],
294 | },
295 |
296 | // desc, natural
297 | {
298 | code: '{"b":3, "a":1, "_":2} // desc, natural',
299 | language: "json/jsonc",
300 | options: ["desc", { natural: true }],
301 | },
302 | {
303 | code: '{"c":2, "b":3, "a":1}',
304 | options: ["desc", { natural: true }],
305 | },
306 | {
307 | code: '{"b_":1, "b":3, "a":2}',
308 | options: ["desc", { natural: true }],
309 | },
310 | {
311 | code: '{"c":2, "b_":1, "C":3}',
312 | options: ["desc", { natural: true }],
313 | },
314 | {
315 | code: '{"a":4, "A":3, "_":2, "$":1}',
316 | options: ["desc", { natural: true }],
317 | },
318 | {
319 | code: '{"A":3, "11":2, "2":4, "1":1}',
320 | options: ["desc", { natural: true }],
321 | },
322 | {
323 | code: '{"è":4, "À":3, "Z":2, "#":1}',
324 | options: ["desc", { natural: true }],
325 | },
326 |
327 | // desc, natural, minKeys should ignore unsorted keys when number of keys is less than minKeys
328 | {
329 | code: '{"b_":1, "a":2, "b":3}',
330 | options: ["desc", { natural: true, minKeys: 4 }],
331 | },
332 |
333 | // desc, natural, insensitive
334 | {
335 | code: '{"b":3, "a":1, "_":2} // desc, natural, insensitive',
336 | language: "json/jsonc",
337 | options: ["desc", { natural: true, caseSensitive: false }],
338 | },
339 | {
340 | code: '{"c":2, "b":3, "a":1}',
341 | options: ["desc", { natural: true, caseSensitive: false }],
342 | },
343 | {
344 | code: '{"b_":1, "b":3, "a":2}',
345 | options: ["desc", { natural: true, caseSensitive: false }],
346 | },
347 | {
348 | code: '{"c":2, "C":3, "b_":1}',
349 | options: ["desc", { natural: true, caseSensitive: false }],
350 | },
351 | {
352 | code: '{"C":2, "c":3, "b_":1}',
353 | options: ["desc", { natural: true, caseSensitive: false }],
354 | },
355 | {
356 | code: '{"a":4, "A":3, "_":2, "$":1}',
357 | options: ["desc", { natural: true, caseSensitive: false }],
358 | },
359 | {
360 | code: '{"A":3, "11":2, "2":4, "1":1}',
361 | options: ["desc", { natural: true, caseSensitive: false }],
362 | },
363 | {
364 | code: '{"è":4, "À":3, "Z":2, "#":1}',
365 | options: ["desc", { natural: true, caseSensitive: false }],
366 | },
367 |
368 | // desc, natural, insensitive, minKeys should ignore unsorted keys when number of keys is less than minKeys
369 | {
370 | code: '{"a":1, "_":2, "b":3}',
371 | options: [
372 | "desc",
373 | { natural: true, caseSensitive: false, minKeys: 4 },
374 | ],
375 | },
376 |
377 | // allowLineSeparatedGroups option
378 | {
379 | code: `
380 | {
381 | "a": 1,
382 | "b": 2,
383 | "c": 3,
384 | "e": 4,
385 | "f": 5,
386 | "g": 6
387 | }
388 | `,
389 | options: ["asc", { allowLineSeparatedGroups: false }],
390 | },
391 | {
392 | code: `
393 | {
394 | "e": 1,
395 | "f": 2,
396 | "g": 3,
397 |
398 | "a": 4,
399 | "b": 5,
400 | "c": 6
401 | }
402 | `,
403 | options: ["asc", { allowLineSeparatedGroups: true }],
404 | },
405 | {
406 | code: `
407 | {
408 | "b": 1,
409 |
410 | // comment
411 | "a": 2,
412 | "c": 3
413 | }
414 | `,
415 | language: "json/jsonc",
416 | options: ["asc", { allowLineSeparatedGroups: true }],
417 | },
418 | {
419 | code: `
420 | {
421 | "b": 1
422 |
423 | ,
424 |
425 | // comment
426 | "a": 2,
427 | "c": 3
428 | }
429 | `,
430 | language: "json/jsonc",
431 | options: ["asc", { allowLineSeparatedGroups: true }],
432 | },
433 | {
434 | code: `
435 | {
436 | "b": "/*",
437 |
438 | "a": "*/"
439 | }
440 | `,
441 | options: ["asc", { allowLineSeparatedGroups: true }],
442 | },
443 | {
444 | code: `
445 | {
446 | "b":1,
447 | "c": {
448 | "y":1,
449 | "z":1,
450 |
451 | "x":1
452 | },
453 |
454 | "a":1
455 | }
456 | `,
457 | options: ["asc", { allowLineSeparatedGroups: true }],
458 | },
459 |
460 | {
461 | code: `
462 | {
463 | "b":1,
464 | a: {
465 | "y":1,
466 | x:1,
467 |
468 | "z":1
469 | },
470 |
471 | c:1
472 | }
473 | `,
474 | language: `json/json5`,
475 | options: ["desc", { allowLineSeparatedGroups: true }],
476 | },
477 |
478 | // Commas are not considered separating lines
479 | {
480 | code: `
481 | {
482 | "b": 1
483 |
484 | ,
485 |
486 | "a": 2
487 | }
488 | `,
489 | options: ["asc", { allowLineSeparatedGroups: true }],
490 | },
491 | {
492 | code: `
493 | {
494 | "a": 1
495 |
496 |
497 | ,
498 | "b": 2
499 | }
500 | `,
501 | options: ["asc", { allowLineSeparatedGroups: false }],
502 | },
503 | {
504 | code: `
505 | {
506 | "b": 1
507 | // comment before comma
508 |
509 | ,
510 | "a": 2
511 | }
512 | `,
513 | language: "json/jsonc",
514 | options: ["asc", { allowLineSeparatedGroups: true }],
515 | },
516 | ],
517 | invalid: [
518 | // default (asc)
519 | {
520 | code: '{"a":1, "":2} // default',
521 | language: "json/jsonc",
522 | errors: [
523 | {
524 | messageId: "sortKeys",
525 | data: {
526 | sortName: "alphanumeric",
527 | sensitivity: "sensitive",
528 | direction: "ascending",
529 | thisName: "",
530 | prevName: "a",
531 | },
532 | },
533 | ],
534 | },
535 | {
536 | code: '{"a":1, "_":2, "b":3} // default',
537 | language: "json/jsonc",
538 | errors: [
539 | {
540 | messageId: "sortKeys",
541 | data: {
542 | sortName: "alphanumeric",
543 | sensitivity: "sensitive",
544 | direction: "ascending",
545 | thisName: "_",
546 | prevName: "a",
547 | },
548 | },
549 | ],
550 | },
551 | {
552 | code: '{"a":1, "c":2, "b":3}',
553 | errors: [
554 | {
555 | messageId: "sortKeys",
556 | data: {
557 | sortName: "alphanumeric",
558 | sensitivity: "sensitive",
559 | direction: "ascending",
560 | thisName: "b",
561 | prevName: "c",
562 | },
563 | },
564 | ],
565 | },
566 | {
567 | code: '{"b_":1, "a":2, "b":3}',
568 | errors: [
569 | {
570 | messageId: "sortKeys",
571 | data: {
572 | sortName: "alphanumeric",
573 | sensitivity: "sensitive",
574 | direction: "ascending",
575 | thisName: "a",
576 | prevName: "b_",
577 | },
578 | },
579 | ],
580 | },
581 | {
582 | code: '{"b_":1, "c":2, "C":3}',
583 | errors: [
584 | {
585 | messageId: "sortKeys",
586 | data: {
587 | sortName: "alphanumeric",
588 | sensitivity: "sensitive",
589 | direction: "ascending",
590 | thisName: "C",
591 | prevName: "c",
592 | },
593 | },
594 | ],
595 | },
596 | {
597 | code: '{"$":1, "_":2, "A":3, "a":4}',
598 | errors: [
599 | {
600 | messageId: "sortKeys",
601 | data: {
602 | sortName: "alphanumeric",
603 | sensitivity: "sensitive",
604 | direction: "ascending",
605 | thisName: "A",
606 | prevName: "_",
607 | },
608 | },
609 | ],
610 | },
611 | {
612 | code: '{"1":1, "2":4, "A":3, "11":2}',
613 | errors: [
614 | {
615 | messageId: "sortKeys",
616 | data: {
617 | sortName: "alphanumeric",
618 | sensitivity: "sensitive",
619 | direction: "ascending",
620 | thisName: "11",
621 | prevName: "A",
622 | },
623 | },
624 | ],
625 | },
626 | {
627 | code: '{"#":1, "À":3, "Z":2, "è":4}',
628 | errors: [
629 | {
630 | messageId: "sortKeys",
631 | data: {
632 | sortName: "alphanumeric",
633 | sensitivity: "sensitive",
634 | direction: "ascending",
635 | thisName: "Z",
636 | prevName: "À",
637 | },
638 | },
639 | ],
640 | },
641 |
642 | // asc
643 | {
644 | code: '{"a":1, "_":2, "b":3} // asc',
645 | language: "json/jsonc",
646 | options: ["asc"],
647 | errors: [
648 | {
649 | messageId: "sortKeys",
650 | data: {
651 | sortName: "alphanumeric",
652 | sensitivity: "sensitive",
653 | direction: "ascending",
654 | thisName: "_",
655 | prevName: "a",
656 | },
657 | },
658 | ],
659 | },
660 | {
661 | code: '{"a":1, "c":2, "b":3}',
662 | options: ["asc"],
663 | errors: [
664 | {
665 | messageId: "sortKeys",
666 | data: {
667 | sortName: "alphanumeric",
668 | sensitivity: "sensitive",
669 | direction: "ascending",
670 | thisName: "b",
671 | prevName: "c",
672 | },
673 | },
674 | ],
675 | },
676 | {
677 | code: '{"b_":1, "a":2, "b":3}',
678 | options: ["asc"],
679 | errors: [
680 | {
681 | messageId: "sortKeys",
682 | data: {
683 | sortName: "alphanumeric",
684 | sensitivity: "sensitive",
685 | direction: "ascending",
686 | thisName: "a",
687 | prevName: "b_",
688 | },
689 | },
690 | ],
691 | },
692 | {
693 | code: '{"b_":1, "c":2, "C":3}',
694 | options: ["asc"],
695 | errors: [
696 | {
697 | messageId: "sortKeys",
698 | data: {
699 | sortName: "alphanumeric",
700 | sensitivity: "sensitive",
701 | direction: "ascending",
702 | thisName: "C",
703 | prevName: "c",
704 | },
705 | },
706 | ],
707 | },
708 | {
709 | code: '{"$":1, "_":2, "A":3, "a":4}',
710 | options: ["asc"],
711 | errors: [
712 | {
713 | messageId: "sortKeys",
714 | data: {
715 | sortName: "alphanumeric",
716 | sensitivity: "sensitive",
717 | direction: "ascending",
718 | thisName: "A",
719 | prevName: "_",
720 | },
721 | },
722 | ],
723 | },
724 | {
725 | code: '{"1":1, "2":4, "A":3, "11":2}',
726 | options: ["asc"],
727 | errors: [
728 | {
729 | messageId: "sortKeys",
730 | data: {
731 | sortName: "alphanumeric",
732 | sensitivity: "sensitive",
733 | direction: "ascending",
734 | thisName: "11",
735 | prevName: "A",
736 | },
737 | },
738 | ],
739 | },
740 | {
741 | code: '{"#":1, "À":3, "Z":2, "è":4}',
742 | options: ["asc"],
743 | errors: [
744 | {
745 | messageId: "sortKeys",
746 | data: {
747 | sortName: "alphanumeric",
748 | sensitivity: "sensitive",
749 | direction: "ascending",
750 | thisName: "Z",
751 | prevName: "À",
752 | },
753 | },
754 | ],
755 | },
756 |
757 | // asc, minKeys should error when number of keys is greater than or equal to minKeys
758 | {
759 | code: '{"a":1, "_":2, "b":3}',
760 | options: ["asc", { minKeys: 3 }],
761 | errors: [
762 | {
763 | messageId: "sortKeys",
764 | data: {
765 | sortName: "alphanumeric",
766 | sensitivity: "sensitive",
767 | direction: "ascending",
768 | thisName: "_",
769 | prevName: "a",
770 | },
771 | },
772 | ],
773 | },
774 |
775 | // asc, insensitive
776 | {
777 | code: '{"a":1, "_":2, "b":3} // asc, insensitive',
778 | language: "json/jsonc",
779 | options: ["asc", { caseSensitive: false }],
780 | errors: [
781 | {
782 | messageId: "sortKeys",
783 | data: {
784 | sortName: "alphanumeric",
785 | sensitivity: "insensitive",
786 | direction: "ascending",
787 | thisName: "_",
788 | prevName: "a",
789 | },
790 | },
791 | ],
792 | },
793 | {
794 | code: '{"a":1, "c":2, "b":3}',
795 | options: ["asc", { caseSensitive: false }],
796 | errors: [
797 | {
798 | messageId: "sortKeys",
799 | data: {
800 | sortName: "alphanumeric",
801 | sensitivity: "insensitive",
802 | direction: "ascending",
803 | thisName: "b",
804 | prevName: "c",
805 | },
806 | },
807 | ],
808 | },
809 | {
810 | code: '{"b_":1, "a":2, "b":3}',
811 | options: ["asc", { caseSensitive: false }],
812 | errors: [
813 | {
814 | messageId: "sortKeys",
815 | data: {
816 | sortName: "alphanumeric",
817 | sensitivity: "insensitive",
818 | direction: "ascending",
819 | thisName: "a",
820 | prevName: "b_",
821 | },
822 | },
823 | ],
824 | },
825 | {
826 | code: '{"$":1, "A":3, "_":2, "a":4}',
827 | options: ["asc", { caseSensitive: false }],
828 | errors: [
829 | {
830 | messageId: "sortKeys",
831 | data: {
832 | sortName: "alphanumeric",
833 | sensitivity: "insensitive",
834 | direction: "ascending",
835 | thisName: "_",
836 | prevName: "A",
837 | },
838 | },
839 | ],
840 | },
841 | {
842 | code: '{"1":1, "2":4, "A":3, "11":2}',
843 | options: ["asc", { caseSensitive: false }],
844 | errors: [
845 | {
846 | messageId: "sortKeys",
847 | data: {
848 | sortName: "alphanumeric",
849 | sensitivity: "insensitive",
850 | direction: "ascending",
851 | thisName: "11",
852 | prevName: "A",
853 | },
854 | },
855 | ],
856 | },
857 | {
858 | code: '{"#":1, "À":3, "Z":2, "è":4}',
859 | options: ["asc", { caseSensitive: false }],
860 | errors: [
861 | {
862 | messageId: "sortKeys",
863 | data: {
864 | sortName: "alphanumeric",
865 | sensitivity: "insensitive",
866 | direction: "ascending",
867 | thisName: "Z",
868 | prevName: "À",
869 | },
870 | },
871 | ],
872 | },
873 |
874 | // asc, insensitive, minKeys should error when number of keys is greater than or equal to minKeys
875 | {
876 | code: '{"a":1, "_":2, "b":3}',
877 | options: ["asc", { caseSensitive: false, minKeys: 3 }],
878 | errors: [
879 | {
880 | messageId: "sortKeys",
881 | data: {
882 | sortName: "alphanumeric",
883 | sensitivity: "insensitive",
884 | direction: "ascending",
885 | thisName: "_",
886 | prevName: "a",
887 | },
888 | },
889 | ],
890 | },
891 |
892 | // asc, natural
893 | {
894 | code: '{"a":1, "_":2, "b":3} // asc, natural',
895 | language: "json/jsonc",
896 | options: ["asc", { natural: true }],
897 | errors: [
898 | {
899 | messageId: "sortKeys",
900 | data: {
901 | sortName: "natural",
902 | sensitivity: "sensitive",
903 | direction: "ascending",
904 | thisName: "_",
905 | prevName: "a",
906 | },
907 | },
908 | ],
909 | },
910 | {
911 | code: '{"a":1, "c":2, "b":3}',
912 | options: ["asc", { natural: true }],
913 | errors: [
914 | {
915 | messageId: "sortKeys",
916 | data: {
917 | sortName: "natural",
918 | sensitivity: "sensitive",
919 | direction: "ascending",
920 | thisName: "b",
921 | prevName: "c",
922 | },
923 | },
924 | ],
925 | },
926 | {
927 | code: '{"b_":1, "a":2, "b":3}',
928 | options: ["asc", { natural: true }],
929 | errors: [
930 | {
931 | messageId: "sortKeys",
932 | data: {
933 | sortName: "natural",
934 | sensitivity: "sensitive",
935 | direction: "ascending",
936 | thisName: "a",
937 | prevName: "b_",
938 | },
939 | },
940 | ],
941 | },
942 | {
943 | code: '{"b_":1, "c":2, "C":3}',
944 | options: ["asc", { natural: true }],
945 | errors: [
946 | {
947 | messageId: "sortKeys",
948 | data: {
949 | sortName: "natural",
950 | sensitivity: "sensitive",
951 | direction: "ascending",
952 | thisName: "C",
953 | prevName: "c",
954 | },
955 | },
956 | ],
957 | },
958 | {
959 | code: '{"$":1, "A":3, "_":2, "a":4}',
960 | options: ["asc", { natural: true }],
961 | errors: [
962 | {
963 | messageId: "sortKeys",
964 | data: {
965 | sortName: "natural",
966 | sensitivity: "sensitive",
967 | direction: "ascending",
968 | thisName: "_",
969 | prevName: "A",
970 | },
971 | },
972 | ],
973 | },
974 | {
975 | code: '{"1":1, "2":4, "A":3, "11":2}',
976 | options: ["asc", { natural: true }],
977 | errors: [
978 | {
979 | messageId: "sortKeys",
980 | data: {
981 | sortName: "natural",
982 | sensitivity: "sensitive",
983 | direction: "ascending",
984 | thisName: "11",
985 | prevName: "A",
986 | },
987 | },
988 | ],
989 | },
990 | {
991 | code: '{"#":1, "À":3, "Z":2, "è":4}',
992 | options: ["asc", { natural: true }],
993 | errors: [
994 | {
995 | messageId: "sortKeys",
996 | data: {
997 | sortName: "natural",
998 | sensitivity: "sensitive",
999 | direction: "ascending",
1000 | thisName: "Z",
1001 | prevName: "À",
1002 | },
1003 | },
1004 | ],
1005 | },
1006 |
1007 | // asc, natural, minKeys should error when number of keys is greater than or equal to minKeys
1008 | {
1009 | code: '{"a":1, "_":2, "b":3}',
1010 | options: ["asc", { natural: true, minKeys: 2 }],
1011 | errors: [
1012 | {
1013 | messageId: "sortKeys",
1014 | data: {
1015 | sortName: "natural",
1016 | sensitivity: "sensitive",
1017 | direction: "ascending",
1018 | thisName: "_",
1019 | prevName: "a",
1020 | },
1021 | },
1022 | ],
1023 | },
1024 |
1025 | // asc, natural, insensitive
1026 | {
1027 | code: '{"a":1, "_":2, "b":3} // asc, natural, insensitive',
1028 | language: "json/jsonc",
1029 | options: ["asc", { natural: true, caseSensitive: false }],
1030 | errors: [
1031 | {
1032 | messageId: "sortKeys",
1033 | data: {
1034 | sortName: "natural",
1035 | sensitivity: "insensitive",
1036 | direction: "ascending",
1037 | thisName: "_",
1038 | prevName: "a",
1039 | },
1040 | },
1041 | ],
1042 | },
1043 | {
1044 | code: '{"a":1, "c":2, "b":3}',
1045 | options: ["asc", { natural: true, caseSensitive: false }],
1046 | errors: [
1047 | {
1048 | messageId: "sortKeys",
1049 | data: {
1050 | sortName: "natural",
1051 | sensitivity: "insensitive",
1052 | direction: "ascending",
1053 | thisName: "b",
1054 | prevName: "c",
1055 | },
1056 | },
1057 | ],
1058 | },
1059 | {
1060 | code: '{"b_":1, "a":2, "b":3}',
1061 | options: ["asc", { natural: true, caseSensitive: false }],
1062 | errors: [
1063 | {
1064 | messageId: "sortKeys",
1065 | data: {
1066 | sortName: "natural",
1067 | sensitivity: "insensitive",
1068 | direction: "ascending",
1069 | thisName: "a",
1070 | prevName: "b_",
1071 | },
1072 | },
1073 | ],
1074 | },
1075 | {
1076 | code: '{"$":1, "A":3, "_":2, "a":4}',
1077 | options: ["asc", { natural: true, caseSensitive: false }],
1078 | errors: [
1079 | {
1080 | messageId: "sortKeys",
1081 | data: {
1082 | sortName: "natural",
1083 | sensitivity: "insensitive",
1084 | direction: "ascending",
1085 | thisName: "_",
1086 | prevName: "A",
1087 | },
1088 | },
1089 | ],
1090 | },
1091 | {
1092 | code: '{"1":1, "11":2, "2":4, "A":3}',
1093 | options: ["asc", { natural: true, caseSensitive: false }],
1094 | errors: [
1095 | {
1096 | messageId: "sortKeys",
1097 | data: {
1098 | sortName: "natural",
1099 | sensitivity: "insensitive",
1100 | direction: "ascending",
1101 | thisName: "2",
1102 | prevName: "11",
1103 | },
1104 | },
1105 | ],
1106 | },
1107 | {
1108 | code: '{"#":1, "À":3, "Z":2, "è":4}',
1109 | options: ["asc", { natural: true, caseSensitive: false }],
1110 | errors: [
1111 | {
1112 | messageId: "sortKeys",
1113 | data: {
1114 | sortName: "natural",
1115 | sensitivity: "insensitive",
1116 | direction: "ascending",
1117 | thisName: "Z",
1118 | prevName: "À",
1119 | },
1120 | },
1121 | ],
1122 | },
1123 |
1124 | // asc, natural, insensitive, minKeys should error when number of keys is greater than or equal to minKeys
1125 | {
1126 | code: '{"a":1, "_":2, "b":3}',
1127 | options: [
1128 | "asc",
1129 | { natural: true, caseSensitive: false, minKeys: 3 },
1130 | ],
1131 | errors: [
1132 | {
1133 | messageId: "sortKeys",
1134 | data: {
1135 | sortName: "natural",
1136 | sensitivity: "insensitive",
1137 | direction: "ascending",
1138 | thisName: "_",
1139 | prevName: "a",
1140 | },
1141 | },
1142 | ],
1143 | },
1144 |
1145 | // desc
1146 | {
1147 | code: '{"":1, "a":2} // desc',
1148 | language: "json/jsonc",
1149 | options: ["desc"],
1150 | errors: [
1151 | {
1152 | messageId: "sortKeys",
1153 | data: {
1154 | sortName: "alphanumeric",
1155 | sensitivity: "sensitive",
1156 | direction: "descending",
1157 | thisName: "a",
1158 | prevName: "",
1159 | },
1160 | },
1161 | ],
1162 | },
1163 | {
1164 | code: '{"a":1, "_":2, "b":3} // desc',
1165 | language: "json/jsonc",
1166 | options: ["desc"],
1167 | errors: [
1168 | {
1169 | messageId: "sortKeys",
1170 | data: {
1171 | sortName: "alphanumeric",
1172 | sensitivity: "sensitive",
1173 | direction: "descending",
1174 | thisName: "b",
1175 | prevName: "_",
1176 | },
1177 | },
1178 | ],
1179 | },
1180 | {
1181 | code: '{"a":1, "c":2, "b":3}',
1182 | options: ["desc"],
1183 | errors: [
1184 | {
1185 | messageId: "sortKeys",
1186 | data: {
1187 | sortName: "alphanumeric",
1188 | sensitivity: "sensitive",
1189 | direction: "descending",
1190 | thisName: "c",
1191 | prevName: "a",
1192 | },
1193 | },
1194 | ],
1195 | },
1196 | {
1197 | code: '{"b_":1, "a":2, "b":3}',
1198 | options: ["desc"],
1199 | errors: [
1200 | {
1201 | messageId: "sortKeys",
1202 | data: {
1203 | sortName: "alphanumeric",
1204 | sensitivity: "sensitive",
1205 | direction: "descending",
1206 | thisName: "b",
1207 | prevName: "a",
1208 | },
1209 | },
1210 | ],
1211 | },
1212 | {
1213 | code: '{"b_":1, "c":2, "C":3}',
1214 | options: ["desc"],
1215 | errors: [
1216 | {
1217 | messageId: "sortKeys",
1218 | data: {
1219 | sortName: "alphanumeric",
1220 | sensitivity: "sensitive",
1221 | direction: "descending",
1222 | thisName: "c",
1223 | prevName: "b_",
1224 | },
1225 | },
1226 | ],
1227 | },
1228 | {
1229 | code: '{"$":1, "_":2, "A":3, "a":4}',
1230 | options: ["desc"],
1231 | errors: [
1232 | {
1233 | messageId: "sortKeys",
1234 | data: {
1235 | sortName: "alphanumeric",
1236 | sensitivity: "sensitive",
1237 | direction: "descending",
1238 | thisName: "_",
1239 | prevName: "$",
1240 | },
1241 | },
1242 | {
1243 | messageId: "sortKeys",
1244 | data: {
1245 | sortName: "alphanumeric",
1246 | sensitivity: "sensitive",
1247 | direction: "descending",
1248 | thisName: "a",
1249 | prevName: "A",
1250 | },
1251 | },
1252 | ],
1253 | },
1254 | {
1255 | code: '{"1":1, "2":4, "A":3, "11":2}',
1256 | options: ["desc"],
1257 | errors: [
1258 | {
1259 | messageId: "sortKeys",
1260 | data: {
1261 | sortName: "alphanumeric",
1262 | sensitivity: "sensitive",
1263 | direction: "descending",
1264 | thisName: "2",
1265 | prevName: "1",
1266 | },
1267 | },
1268 | {
1269 | messageId: "sortKeys",
1270 | data: {
1271 | sortName: "alphanumeric",
1272 | sensitivity: "sensitive",
1273 | direction: "descending",
1274 | thisName: "A",
1275 | prevName: "2",
1276 | },
1277 | },
1278 | ],
1279 | },
1280 | {
1281 | code: '{"#":1, "À":3, "Z":2, "è":4}',
1282 | options: ["desc"],
1283 | errors: [
1284 | {
1285 | messageId: "sortKeys",
1286 | data: {
1287 | sortName: "alphanumeric",
1288 | sensitivity: "sensitive",
1289 | direction: "descending",
1290 | thisName: "À",
1291 | prevName: "#",
1292 | },
1293 | },
1294 | {
1295 | messageId: "sortKeys",
1296 | data: {
1297 | sortName: "alphanumeric",
1298 | sensitivity: "sensitive",
1299 | direction: "descending",
1300 | thisName: "è",
1301 | prevName: "Z",
1302 | },
1303 | },
1304 | ],
1305 | },
1306 |
1307 | // desc, minKeys should error when number of keys is greater than or equal to minKeys
1308 | {
1309 | code: '{"a":1, "_":2, "b":3}',
1310 | options: ["desc", { minKeys: 3 }],
1311 | errors: [
1312 | {
1313 | messageId: "sortKeys",
1314 | data: {
1315 | sortName: "alphanumeric",
1316 | sensitivity: "sensitive",
1317 | direction: "descending",
1318 | thisName: "b",
1319 | prevName: "_",
1320 | },
1321 | },
1322 | ],
1323 | },
1324 |
1325 | // desc, insensitive
1326 | {
1327 | code: '{"a":1, "_":2, "b":3} // desc, insensitive',
1328 | language: "json/jsonc",
1329 | options: ["desc", { caseSensitive: false }],
1330 | errors: [
1331 | {
1332 | messageId: "sortKeys",
1333 | data: {
1334 | sortName: "alphanumeric",
1335 | sensitivity: "insensitive",
1336 | direction: "descending",
1337 | thisName: "b",
1338 | prevName: "_",
1339 | },
1340 | },
1341 | ],
1342 | },
1343 | {
1344 | code: '{"a":1, "c":2, "b":3}',
1345 | options: ["desc", { caseSensitive: false }],
1346 | errors: [
1347 | {
1348 | messageId: "sortKeys",
1349 | data: {
1350 | sortName: "alphanumeric",
1351 | sensitivity: "insensitive",
1352 | direction: "descending",
1353 | thisName: "c",
1354 | prevName: "a",
1355 | },
1356 | },
1357 | ],
1358 | },
1359 | {
1360 | code: '{"b_":1, "a":2, "b":3}',
1361 | options: ["desc", { caseSensitive: false }],
1362 | errors: [
1363 | {
1364 | messageId: "sortKeys",
1365 | data: {
1366 | sortName: "alphanumeric",
1367 | sensitivity: "insensitive",
1368 | direction: "descending",
1369 | thisName: "b",
1370 | prevName: "a",
1371 | },
1372 | },
1373 | ],
1374 | },
1375 | {
1376 | code: '{"b_":1, "c":2, "C":3}',
1377 | options: ["desc", { caseSensitive: false }],
1378 | errors: [
1379 | {
1380 | messageId: "sortKeys",
1381 | data: {
1382 | sortName: "alphanumeric",
1383 | sensitivity: "insensitive",
1384 | direction: "descending",
1385 | thisName: "c",
1386 | prevName: "b_",
1387 | },
1388 | },
1389 | ],
1390 | },
1391 | {
1392 | code: '{"$":1, "_":2, "A":3, "a":4}',
1393 | options: ["desc", { caseSensitive: false }],
1394 | errors: [
1395 | {
1396 | messageId: "sortKeys",
1397 | data: {
1398 | sortName: "alphanumeric",
1399 | sensitivity: "insensitive",
1400 | direction: "descending",
1401 | thisName: "_",
1402 | prevName: "$",
1403 | },
1404 | },
1405 | {
1406 | messageId: "sortKeys",
1407 | data: {
1408 | sortName: "alphanumeric",
1409 | sensitivity: "insensitive",
1410 | direction: "descending",
1411 | thisName: "A",
1412 | prevName: "_",
1413 | },
1414 | },
1415 | ],
1416 | },
1417 | {
1418 | code: '{"1":1, "2":4, "A":3, "11":2}',
1419 | options: ["desc", { caseSensitive: false }],
1420 | errors: [
1421 | {
1422 | messageId: "sortKeys",
1423 | data: {
1424 | sortName: "alphanumeric",
1425 | sensitivity: "insensitive",
1426 | direction: "descending",
1427 | thisName: "2",
1428 | prevName: "1",
1429 | },
1430 | },
1431 | {
1432 | messageId: "sortKeys",
1433 | data: {
1434 | sortName: "alphanumeric",
1435 | sensitivity: "insensitive",
1436 | direction: "descending",
1437 | thisName: "A",
1438 | prevName: "2",
1439 | },
1440 | },
1441 | ],
1442 | },
1443 | {
1444 | code: '{"#":1, "À":3, "Z":2, "è":4}',
1445 | options: ["desc", { caseSensitive: false }],
1446 | errors: [
1447 | {
1448 | messageId: "sortKeys",
1449 | data: {
1450 | sortName: "alphanumeric",
1451 | sensitivity: "insensitive",
1452 | direction: "descending",
1453 | thisName: "À",
1454 | prevName: "#",
1455 | },
1456 | },
1457 | {
1458 | messageId: "sortKeys",
1459 | data: {
1460 | sortName: "alphanumeric",
1461 | sensitivity: "insensitive",
1462 | direction: "descending",
1463 | thisName: "è",
1464 | prevName: "Z",
1465 | },
1466 | },
1467 | ],
1468 | },
1469 |
1470 | // desc, insensitive should error when number of keys is greater than or equal to minKeys
1471 | {
1472 | code: '{"a":1, "_":2, "b":3}',
1473 | options: ["desc", { caseSensitive: false, minKeys: 2 }],
1474 | errors: [
1475 | {
1476 | messageId: "sortKeys",
1477 | data: {
1478 | sortName: "alphanumeric",
1479 | sensitivity: "insensitive",
1480 | direction: "descending",
1481 | thisName: "b",
1482 | prevName: "_",
1483 | },
1484 | },
1485 | ],
1486 | },
1487 |
1488 | // desc, natural
1489 | {
1490 | code: '{"a":1, "_":2, "b":3} // desc, natural',
1491 | language: "json/jsonc",
1492 | options: ["desc", { natural: true }],
1493 | errors: [
1494 | {
1495 | messageId: "sortKeys",
1496 | data: {
1497 | sortName: "natural",
1498 | sensitivity: "sensitive",
1499 | direction: "descending",
1500 | thisName: "b",
1501 | prevName: "_",
1502 | },
1503 | },
1504 | ],
1505 | },
1506 | {
1507 | code: '{"a":1, "c":2, "b":3}',
1508 | options: ["desc", { natural: true }],
1509 | errors: [
1510 | {
1511 | messageId: "sortKeys",
1512 | data: {
1513 | sortName: "natural",
1514 | sensitivity: "sensitive",
1515 | direction: "descending",
1516 | thisName: "c",
1517 | prevName: "a",
1518 | },
1519 | },
1520 | ],
1521 | },
1522 | {
1523 | code: '{"b_":1, "a":2, "b":3}',
1524 | options: ["desc", { natural: true }],
1525 | errors: [
1526 | {
1527 | messageId: "sortKeys",
1528 | data: {
1529 | sortName: "natural",
1530 | sensitivity: "sensitive",
1531 | direction: "descending",
1532 | thisName: "b",
1533 | prevName: "a",
1534 | },
1535 | },
1536 | ],
1537 | },
1538 | {
1539 | code: '{"b_":1, "c":2, "C":3}',
1540 | options: ["desc", { natural: true }],
1541 | errors: [
1542 | {
1543 | messageId: "sortKeys",
1544 | data: {
1545 | sortName: "natural",
1546 | sensitivity: "sensitive",
1547 | direction: "descending",
1548 | thisName: "c",
1549 | prevName: "b_",
1550 | },
1551 | },
1552 | ],
1553 | },
1554 | {
1555 | code: '{"$":1, "_":2, "A":3, "a":4}',
1556 | options: ["desc", { natural: true }],
1557 | errors: [
1558 | {
1559 | messageId: "sortKeys",
1560 | data: {
1561 | sortName: "natural",
1562 | sensitivity: "sensitive",
1563 | direction: "descending",
1564 | thisName: "_",
1565 | prevName: "$",
1566 | },
1567 | },
1568 | {
1569 | messageId: "sortKeys",
1570 | data: {
1571 | sortName: "natural",
1572 | sensitivity: "sensitive",
1573 | direction: "descending",
1574 | thisName: "A",
1575 | prevName: "_",
1576 | },
1577 | },
1578 | {
1579 | messageId: "sortKeys",
1580 | data: {
1581 | sortName: "natural",
1582 | sensitivity: "sensitive",
1583 | direction: "descending",
1584 | thisName: "a",
1585 | prevName: "A",
1586 | },
1587 | },
1588 | ],
1589 | },
1590 | {
1591 | code: '{"1":1, "2":4, "A":3, "11":2}',
1592 | options: ["desc", { natural: true }],
1593 | errors: [
1594 | {
1595 | messageId: "sortKeys",
1596 | data: {
1597 | sortName: "natural",
1598 | sensitivity: "sensitive",
1599 | direction: "descending",
1600 | thisName: "2",
1601 | prevName: "1",
1602 | },
1603 | },
1604 | {
1605 | messageId: "sortKeys",
1606 | data: {
1607 | sortName: "natural",
1608 | sensitivity: "sensitive",
1609 | direction: "descending",
1610 | thisName: "A",
1611 | prevName: "2",
1612 | },
1613 | },
1614 | ],
1615 | },
1616 | {
1617 | code: '{"#":1, "À":3, "Z":2, "è":4}',
1618 | options: ["desc", { natural: true }],
1619 | errors: [
1620 | {
1621 | messageId: "sortKeys",
1622 | data: {
1623 | sortName: "natural",
1624 | sensitivity: "sensitive",
1625 | direction: "descending",
1626 | thisName: "À",
1627 | prevName: "#",
1628 | },
1629 | },
1630 | {
1631 | messageId: "sortKeys",
1632 | data: {
1633 | sortName: "natural",
1634 | sensitivity: "sensitive",
1635 | direction: "descending",
1636 | thisName: "è",
1637 | prevName: "Z",
1638 | },
1639 | },
1640 | ],
1641 | },
1642 |
1643 | // desc, natural should error when number of keys is greater than or equal to minKeys
1644 | {
1645 | code: '{"a":1, "_":2, "b":3}',
1646 | options: ["desc", { natural: true, minKeys: 3 }],
1647 | errors: [
1648 | {
1649 | messageId: "sortKeys",
1650 | data: {
1651 | sortName: "natural",
1652 | sensitivity: "sensitive",
1653 | direction: "descending",
1654 | thisName: "b",
1655 | prevName: "_",
1656 | },
1657 | },
1658 | ],
1659 | },
1660 |
1661 | // desc, natural, insensitive
1662 | {
1663 | code: '{"a":1, "_":2, "b":3} // desc, natural, insensitive',
1664 | language: "json/jsonc",
1665 | options: ["desc", { natural: true, caseSensitive: false }],
1666 | errors: [
1667 | {
1668 | messageId: "sortKeys",
1669 | data: {
1670 | sortName: "natural",
1671 | sensitivity: "insensitive",
1672 | direction: "descending",
1673 | thisName: "b",
1674 | prevName: "_",
1675 | },
1676 | },
1677 | ],
1678 | },
1679 | {
1680 | code: '{"a":1, "c":2, "b":3}',
1681 | options: ["desc", { natural: true, caseSensitive: false }],
1682 | errors: [
1683 | {
1684 | messageId: "sortKeys",
1685 | data: {
1686 | sortName: "natural",
1687 | sensitivity: "insensitive",
1688 | direction: "descending",
1689 | thisName: "c",
1690 | prevName: "a",
1691 | },
1692 | },
1693 | ],
1694 | },
1695 | {
1696 | code: '{"b_":1, "a":2, "b":3}',
1697 | options: ["desc", { natural: true, caseSensitive: false }],
1698 | errors: [
1699 | {
1700 | messageId: "sortKeys",
1701 | data: {
1702 | sortName: "natural",
1703 | sensitivity: "insensitive",
1704 | direction: "descending",
1705 | thisName: "b",
1706 | prevName: "a",
1707 | },
1708 | },
1709 | ],
1710 | },
1711 | {
1712 | code: '{"b_":1, "c":2, "C":3}',
1713 | options: ["desc", { natural: true, caseSensitive: false }],
1714 | errors: [
1715 | {
1716 | messageId: "sortKeys",
1717 | data: {
1718 | sortName: "natural",
1719 | sensitivity: "insensitive",
1720 | direction: "descending",
1721 | thisName: "c",
1722 | prevName: "b_",
1723 | },
1724 | },
1725 | ],
1726 | },
1727 | {
1728 | code: '{"$":1, "_":2, "A":3, "a":4}',
1729 | options: ["desc", { natural: true, caseSensitive: false }],
1730 | errors: [
1731 | {
1732 | messageId: "sortKeys",
1733 | data: {
1734 | sortName: "natural",
1735 | sensitivity: "insensitive",
1736 | direction: "descending",
1737 | thisName: "_",
1738 | prevName: "$",
1739 | },
1740 | },
1741 | {
1742 | messageId: "sortKeys",
1743 | data: {
1744 | sortName: "natural",
1745 | sensitivity: "insensitive",
1746 | direction: "descending",
1747 | thisName: "A",
1748 | prevName: "_",
1749 | },
1750 | },
1751 | ],
1752 | },
1753 | {
1754 | code: '{"1":1, "2":4, "11":2, "A":3}',
1755 | options: ["desc", { natural: true, caseSensitive: false }],
1756 | errors: [
1757 | {
1758 | messageId: "sortKeys",
1759 | data: {
1760 | sortName: "natural",
1761 | sensitivity: "insensitive",
1762 | direction: "descending",
1763 | thisName: "2",
1764 | prevName: "1",
1765 | },
1766 | },
1767 | {
1768 | messageId: "sortKeys",
1769 | data: {
1770 | sortName: "natural",
1771 | sensitivity: "insensitive",
1772 | direction: "descending",
1773 | thisName: "11",
1774 | prevName: "2",
1775 | },
1776 | },
1777 | {
1778 | messageId: "sortKeys",
1779 | data: {
1780 | sortName: "natural",
1781 | sensitivity: "insensitive",
1782 | direction: "descending",
1783 | thisName: "A",
1784 | prevName: "11",
1785 | },
1786 | },
1787 | ],
1788 | },
1789 | {
1790 | code: '{"#":1, "À":3, "Z":2, "è":4}',
1791 | options: ["desc", { natural: true, caseSensitive: false }],
1792 | errors: [
1793 | {
1794 | messageId: "sortKeys",
1795 | data: {
1796 | sortName: "natural",
1797 | sensitivity: "insensitive",
1798 | direction: "descending",
1799 | thisName: "À",
1800 | prevName: "#",
1801 | },
1802 | },
1803 | {
1804 | messageId: "sortKeys",
1805 | data: {
1806 | sortName: "natural",
1807 | sensitivity: "insensitive",
1808 | direction: "descending",
1809 | thisName: "è",
1810 | prevName: "Z",
1811 | },
1812 | },
1813 | ],
1814 | },
1815 |
1816 | // desc, natural, insensitive should error when number of keys is greater than or equal to minKeys
1817 | {
1818 | code: '{"a":1, "_":2, "b":3}',
1819 | options: [
1820 | "desc",
1821 | { natural: true, caseSensitive: false, minKeys: 2 },
1822 | ],
1823 | errors: [
1824 | {
1825 | messageId: "sortKeys",
1826 | data: {
1827 | sortName: "natural",
1828 | sensitivity: "insensitive",
1829 | direction: "descending",
1830 | thisName: "b",
1831 | prevName: "_",
1832 | },
1833 | },
1834 | ],
1835 | },
1836 |
1837 | // When allowLineSeparatedGroups option is false
1838 | {
1839 | code: `
1840 | {
1841 | "b": 1,
1842 | "c": 2,
1843 | "a": 3
1844 | }
1845 | `,
1846 | options: ["asc", { allowLineSeparatedGroups: false }],
1847 | errors: [
1848 | {
1849 | messageId: "sortKeys",
1850 | data: {
1851 | sortName: "alphanumeric",
1852 | sensitivity: "sensitive",
1853 | direction: "ascending",
1854 | thisName: "a",
1855 | prevName: "c",
1856 | },
1857 | },
1858 | ],
1859 | },
1860 | {
1861 | code: `
1862 | {
1863 | "b": 1,
1864 |
1865 | "c": 2,
1866 |
1867 | "a": 3
1868 | }
1869 | `,
1870 | options: ["asc", { allowLineSeparatedGroups: false }],
1871 | errors: [
1872 | {
1873 | messageId: "sortKeys",
1874 | data: {
1875 | sortName: "alphanumeric",
1876 | sensitivity: "sensitive",
1877 | direction: "ascending",
1878 | thisName: "a",
1879 | prevName: "c",
1880 | },
1881 | },
1882 | ],
1883 | },
1884 |
1885 | // When allowLineSeparatedGroups option is true
1886 | {
1887 | code: `
1888 | {
1889 | "b": "/*",
1890 | "a": "*/"
1891 | }
1892 | `,
1893 | options: ["asc", { allowLineSeparatedGroups: true }],
1894 | errors: [
1895 | {
1896 | messageId: "sortKeys",
1897 | data: {
1898 | sortName: "alphanumeric",
1899 | sensitivity: "sensitive",
1900 | direction: "ascending",
1901 | thisName: "a",
1902 | prevName: "b",
1903 | },
1904 | },
1905 | ],
1906 | },
1907 | {
1908 | code: `
1909 | {
1910 | "b": 1
1911 | // comment before comma
1912 | , "a": 2
1913 | }
1914 | `,
1915 | language: "json/jsonc",
1916 | options: ["asc", { allowLineSeparatedGroups: true }],
1917 | errors: [
1918 | {
1919 | messageId: "sortKeys",
1920 | data: {
1921 | sortName: "alphanumeric",
1922 | sensitivity: "sensitive",
1923 | direction: "ascending",
1924 | thisName: "a",
1925 | prevName: "b",
1926 | },
1927 | },
1928 | ],
1929 | },
1930 | {
1931 | code: `
1932 | [
1933 | {
1934 | "b":1,
1935 | "a": {
1936 | "x":1,
1937 | "y":1
1938 | },
1939 |
1940 | "d":1,
1941 | "c":1
1942 | }
1943 | ]
1944 | `,
1945 | options: ["desc", { allowLineSeparatedGroups: true }],
1946 | errors: [
1947 | {
1948 | messageId: "sortKeys",
1949 | data: {
1950 | sortName: "alphanumeric",
1951 | sensitivity: "sensitive",
1952 | direction: "descending",
1953 | thisName: "y",
1954 | prevName: "x",
1955 | },
1956 | },
1957 | ],
1958 | },
1959 | {
1960 | code: `
1961 | {
1962 | "b": /*foo */ 1,
1963 | // some multiline comment
1964 | // using line comment style
1965 | "a": 2 // "a" and "b" are not line separated
1966 | }
1967 | `,
1968 | language: "json/jsonc",
1969 | options: ["asc", { allowLineSeparatedGroups: true }],
1970 | errors: [
1971 | {
1972 | messageId: "sortKeys",
1973 | data: {
1974 | sortName: "alphanumeric",
1975 | sensitivity: "sensitive",
1976 | direction: "ascending",
1977 | thisName: "a",
1978 | prevName: "b",
1979 | },
1980 | },
1981 | ],
1982 | },
1983 | {
1984 | code: `
1985 | {
1986 | "b": 1,
1987 | /* some multiline comment
1988 | using block comment style */
1989 | /* the empty line...
1990 |
1991 | ...in this one doesn't count */
1992 | "a": 2 // "a" and "b" are not line separated
1993 | }
1994 | `,
1995 | language: "json/jsonc",
1996 | options: ["asc", { allowLineSeparatedGroups: true }],
1997 | errors: [
1998 | {
1999 | messageId: "sortKeys",
2000 | data: {
2001 | sortName: "alphanumeric",
2002 | sensitivity: "sensitive",
2003 | direction: "ascending",
2004 | thisName: "a",
2005 | prevName: "b",
2006 | },
2007 | },
2008 | ],
2009 | },
2010 | {
2011 | code: `
2012 | {
2013 | "b": 1
2014 | ,
2015 | "a": 2
2016 | }
2017 | `,
2018 | options: ["asc", { allowLineSeparatedGroups: true }],
2019 | errors: [
2020 | {
2021 | messageId: "sortKeys",
2022 | data: {
2023 | sortName: "alphanumeric",
2024 | sensitivity: "sensitive",
2025 | direction: "ascending",
2026 | thisName: "a",
2027 | prevName: "b",
2028 | },
2029 | },
2030 | ],
2031 | },
2032 | ],
2033 | });
2034 |
--------------------------------------------------------------------------------
/tests/rules/top-level-interop.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Tests for top-level-interop rule.
3 | * @author Joe Hildebrand
4 | */
5 |
6 | //------------------------------------------------------------------------------
7 | // Imports
8 | //------------------------------------------------------------------------------
9 |
10 | import rule from "../../src/rules/top-level-interop.js";
11 | import json from "../../src/index.js";
12 | import { RuleTester } from "eslint";
13 |
14 | //------------------------------------------------------------------------------
15 | // Tests
16 | //------------------------------------------------------------------------------
17 |
18 | const ruleTester = new RuleTester({
19 | plugins: {
20 | json,
21 | },
22 | language: "json/json",
23 | });
24 |
25 | ruleTester.run("top-level-interop", rule, {
26 | valid: [
27 | "[]",
28 | {
29 | code: "[1]",
30 | language: "json/json5",
31 | },
32 | {
33 | code: "[1, 2]",
34 | language: "json/json5",
35 | },
36 | "{}",
37 | {
38 | code: '{"foo": 1}',
39 | language: "json/json5",
40 | },
41 | {
42 | code: '{"foo": 1, "foo": 2}',
43 | language: "json/json5",
44 | },
45 | ],
46 | invalid: [
47 | {
48 | code: "1",
49 | errors: [
50 | {
51 | messageId: "topLevel",
52 | data: {
53 | type: "Number",
54 | },
55 | line: 1,
56 | column: 1,
57 | endLine: 1,
58 | endColumn: 2,
59 | },
60 | ],
61 | },
62 | {
63 | code: "true",
64 | errors: [
65 | {
66 | messageId: "topLevel",
67 | data: {
68 | type: "Boolean",
69 | },
70 | line: 1,
71 | column: 1,
72 | endLine: 1,
73 | endColumn: 5,
74 | },
75 | ],
76 | },
77 | {
78 | code: "null",
79 | errors: [
80 | {
81 | messageId: "topLevel",
82 | data: {
83 | type: "Null",
84 | },
85 | line: 1,
86 | column: 1,
87 | endLine: 1,
88 | endColumn: 5,
89 | },
90 | ],
91 | },
92 | {
93 | code: '"foo"',
94 | errors: [
95 | {
96 | messageId: "topLevel",
97 | data: {
98 | type: "String",
99 | },
100 | line: 1,
101 | column: 1,
102 | endLine: 1,
103 | endColumn: 6,
104 | },
105 | ],
106 | },
107 | ],
108 | });
109 |
--------------------------------------------------------------------------------
/tests/types/cjs-import.test.cts:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview CommonJS type import test for ESLint JSON Language Plugin.
3 | * @author Francesco Trotta
4 | */
5 |
6 | //-----------------------------------------------------------------------------
7 | // Imports
8 | //-----------------------------------------------------------------------------
9 |
10 | import "@eslint/json";
11 |
--------------------------------------------------------------------------------
/tests/types/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "noEmit": true,
5 | "rootDir": "../..",
6 | "strict": true,
7 | "verbatimModuleSyntax": true,
8 | "erasableSyntaxOnly": true
9 | },
10 | "files": [],
11 | "include": [".", "../../dist"]
12 | }
13 |
--------------------------------------------------------------------------------
/tests/types/types.test.ts:
--------------------------------------------------------------------------------
1 | import json, { JSONSourceCode } from "@eslint/json";
2 | import { ESLint } from "eslint";
3 | import type {
4 | JSONSyntaxElement,
5 | JSONRuleDefinition,
6 | JSONRuleVisitor,
7 | } from "@eslint/json/types";
8 | import type {
9 | AnyNode,
10 | ArrayNode,
11 | BooleanNode,
12 | DocumentNode,
13 | ElementNode,
14 | IdentifierNode,
15 | InfinityNode,
16 | MemberNode,
17 | NaNNode,
18 | NullNode,
19 | NumberNode,
20 | ObjectNode,
21 | StringNode,
22 | } from "@humanwhocodes/momoa";
23 | import type { SourceLocation, SourceRange } from "@eslint/core";
24 |
25 | json satisfies ESLint.Plugin;
26 | json.meta.name satisfies string;
27 | json.meta.version satisfies string;
28 |
29 | // Check that these languages are defined:
30 | json.languages.json satisfies object;
31 | json.languages.json5 satisfies object;
32 | json.languages.jsonc satisfies object;
33 |
34 | // Check that `plugins` in the recommended config is defined:
35 | json.configs.recommended.plugins satisfies object;
36 |
37 | {
38 | type RecommendedRuleName = keyof typeof json.configs.recommended.rules;
39 | type RuleName = `json/${keyof typeof json.rules}`;
40 | type AssertAllNamesIn = never;
41 |
42 | // Check that all recommended rule names match the names of existing rules in this plugin.
43 | null as AssertAllNamesIn;
44 | }
45 |
46 | // Check that types are imported correctly from `@humanwhocodes/momoa`.
47 | ({
48 | start: { line: 1, column: 1, offset: 1 },
49 | end: { line: 1, column: 1, offset: 1 },
50 | }) satisfies JSONSyntaxElement["loc"];
51 | ({
52 | // @ts-expect-error -- This is not a valid Location.
53 | start: 100,
54 | end: { line: 1, column: 1, offset: 1 },
55 | }) satisfies JSONSyntaxElement["loc"];
56 |
57 | (): JSONRuleDefinition => ({
58 | create({ sourceCode }): JSONRuleVisitor {
59 | sourceCode satisfies JSONSourceCode;
60 | sourceCode.ast satisfies DocumentNode;
61 | sourceCode.lines satisfies string[];
62 | sourceCode.text satisfies string;
63 |
64 | function testVisitor(
65 | node: NodeType,
66 | parent?:
67 | | DocumentNode
68 | | MemberNode
69 | | ElementNode
70 | | ArrayNode
71 | | ObjectNode,
72 | ) {
73 | sourceCode.getLoc(node) satisfies SourceLocation;
74 | sourceCode.getRange(node) satisfies SourceRange;
75 | sourceCode.getParent(node) satisfies AnyNode | undefined;
76 | sourceCode.getAncestors(node) satisfies JSONSyntaxElement[];
77 | sourceCode.getText(node) satisfies string;
78 | }
79 |
80 | return {
81 | Array: (...args) => testVisitor(...args),
82 | "Array:exit": (...args) => testVisitor(...args),
83 | Boolean: (...args) => testVisitor(...args),
84 | "Boolean:exit": (...args) => testVisitor(...args),
85 | Document: (...args) => testVisitor(...args),
86 | "Document:exit": (...args) => testVisitor(...args),
87 | Element: (...args) => testVisitor(...args),
88 | "Element:exit": (...args) => testVisitor(...args),
89 | Identifier: (...args) => testVisitor(...args),
90 | "Identifier:exit": (...args) =>
91 | testVisitor(...args),
92 | Infinity: (...args) => testVisitor(...args),
93 | "Infinity:exit": (...args) => testVisitor(...args),
94 | Member: (...args) => testVisitor(...args),
95 | "Member:exit": (...args) => testVisitor(...args),
96 | NaN: (...args) => testVisitor(...args),
97 | "NaN:exit": (...args) => testVisitor(...args),
98 | Null: (...args) => testVisitor(...args),
99 | "Null:exit": (...args) => testVisitor(...args),
100 | Number: (...args) => testVisitor(...args),
101 | "Number:exit": (...args) => testVisitor(...args),
102 | Object: (...args) => testVisitor(...args),
103 | "Object:exit": (...args) => testVisitor(...args),
104 | String: (...args) => testVisitor(...args),
105 | "String:exit": (...args) => testVisitor(...args),
106 | };
107 | },
108 | });
109 |
110 | // All options optional - JSONRuleDefinition and JSONRuleDefinition<{}>
111 | // should be the same type.
112 | (rule1: JSONRuleDefinition, rule2: JSONRuleDefinition<{}>) => {
113 | rule1 satisfies typeof rule2;
114 | rule2 satisfies typeof rule1;
115 | };
116 |
117 | // Type restrictions should be enforced
118 | (): JSONRuleDefinition<{
119 | RuleOptions: [string, number];
120 | MessageIds: "foo" | "bar";
121 | ExtRuleDocs: { foo: string; bar: number };
122 | }> => ({
123 | meta: {
124 | messages: {
125 | foo: "FOO",
126 |
127 | // @ts-expect-error Wrong type for message ID
128 | bar: 42,
129 | },
130 | docs: {
131 | foo: "FOO",
132 |
133 | // @ts-expect-error Wrong type for declared property
134 | bar: "BAR",
135 |
136 | // @ts-expect-error Wrong type for predefined property
137 | description: 42,
138 | },
139 | },
140 | create({ options }) {
141 | // Types for rule options
142 | options[0] satisfies string;
143 | options[1] satisfies number;
144 |
145 | return {};
146 | },
147 | });
148 |
149 | // Undeclared properties should produce an error
150 | (): JSONRuleDefinition<{
151 | MessageIds: "foo" | "bar";
152 | ExtRuleDocs: { foo: number; bar: string };
153 | }> => ({
154 | meta: {
155 | messages: {
156 | foo: "FOO",
157 |
158 | // Declared message ID is not required
159 | // bar: "BAR",
160 |
161 | // @ts-expect-error Undeclared message ID is not allowed
162 | baz: "BAZ",
163 | },
164 | docs: {
165 | foo: 42,
166 |
167 | // Declared property is not required
168 | // bar: "BAR",
169 |
170 | // @ts-expect-error Undeclared property key is not allowed
171 | baz: "BAZ",
172 |
173 | // Predefined property is allowed
174 | description: "Lorem ipsum",
175 | },
176 | },
177 | create() {
178 | return {};
179 | },
180 | });
181 |
--------------------------------------------------------------------------------
/tools/build-cts.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Rewrites import expressions for CommonJS compatibility.
3 | * This script creates "dist/cjs/index.d.cts" from "dist/esm/index.d.ts" by modifying imports
4 | * from `"./types.ts"` to `"./types.cts"`.
5 | *
6 | * Also updates "types.cts" to reference "index.cts"
7 | *
8 | * @author Francesco Trotta
9 | */
10 |
11 | import { readFile, writeFile } from "node:fs/promises";
12 |
13 | const oldSourceText = await readFile("dist/esm/index.d.ts", "utf-8");
14 | const newSourceText = oldSourceText.replaceAll('"./types.ts"', '"./types.cts"');
15 | await writeFile("dist/cjs/index.d.cts", newSourceText);
16 |
17 | // Now update the types.cts to reference index.cts
18 | const typesText = await readFile("dist/cjs/types.cts", "utf-8");
19 | const updatedTypesText = typesText.replaceAll('"./index.js"', '"./index.cjs"');
20 |
21 | await writeFile("dist/cjs/types.cts", updatedTypesText);
22 |
--------------------------------------------------------------------------------
/tools/commit-readme.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #------------------------------------------------------------------------------
4 | # Commits the data files if any have changed
5 | #------------------------------------------------------------------------------
6 |
7 | if [ -z "$(git status --porcelain)" ]; then
8 | echo "Data did not change."
9 | else
10 | echo "Data changed!"
11 |
12 | # commit the result
13 | git add README.md
14 | git commit -m "docs: Update README sponsors"
15 |
16 | # push back to source control
17 | git push origin HEAD
18 | fi
19 |
--------------------------------------------------------------------------------
/tools/dedupe-types.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Strips typedef aliases from the rolled-up file. This
3 | * is necessary because the TypeScript compiler throws an error when
4 | * it encounters a duplicate typedef.
5 | *
6 | * Usage:
7 | * node tools/dedupe-types.js filename1.js filename2.js ...
8 | *
9 | * @author Nicholas C. Zakas
10 | */
11 |
12 | //-----------------------------------------------------------------------------
13 | // Imports
14 | //-----------------------------------------------------------------------------
15 |
16 | import fs from "node:fs";
17 |
18 | //-----------------------------------------------------------------------------
19 | // Main
20 | //-----------------------------------------------------------------------------
21 |
22 | const importRegExp =
23 | /^\s*\*\s*@import\s*\{\s*(?[^,}]+(?:\s*,\s*[^,}]+)*)\s*\}\s*from\s*"(?[^"]+)"/u;
24 |
25 | // read files from the command line
26 | const files = process.argv.slice(2);
27 |
28 | files.forEach(filePath => {
29 | const lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/gu);
30 | const imports = new Map();
31 |
32 | // find all imports and remove them
33 | const remainingLines = lines.filter(line => {
34 | if (!line.startsWith(" * @import")) {
35 | return true;
36 | }
37 |
38 | const match = importRegExp.exec(line);
39 |
40 | if (!match) {
41 | throw Error("Something is very wrong");
42 | }
43 |
44 | const source = match.groups.source;
45 | const ids = match.groups.ids.split(/,/gu).map(id => id.trim());
46 |
47 | // save the import data
48 |
49 | if (!imports.has(source)) {
50 | imports.set(source, new Set());
51 | }
52 |
53 | const existingIds = imports.get(source);
54 | ids.forEach(id => existingIds.add(id));
55 |
56 | return false;
57 | });
58 |
59 | // create a new import statement for each unique import
60 | const jsdocBlock = ["/**"];
61 |
62 | imports.forEach((ids, source) => {
63 | // if it's a local file, we don't need it
64 | if (source.startsWith("./")) {
65 | return;
66 | }
67 |
68 | const idList = Array.from(ids).join(", ");
69 | jsdocBlock.push(` * @import { ${idList} } from "${source}"`);
70 | });
71 |
72 | // add the new import statements to the top of the file
73 | jsdocBlock.push(" */");
74 | remainingLines.unshift(...jsdocBlock);
75 | remainingLines.unshift(""); // add a blank line before the block
76 |
77 | // replace references to ../types.ts with ./types.ts
78 | const text = remainingLines
79 | .join("\n")
80 | .replace(/\.\.\/types\.ts/gu, "./types.ts");
81 |
82 | fs.writeFileSync(filePath, text, "utf8");
83 | });
84 |
--------------------------------------------------------------------------------
/tools/update-readme.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Script to update the README with sponsors details in all packages.
3 | *
4 | * node tools/update-readme.js
5 | *
6 | * @author Milos Djermanovic
7 | */
8 |
9 | //-----------------------------------------------------------------------------
10 | // Requirements
11 | //-----------------------------------------------------------------------------
12 |
13 | import { readFileSync, writeFileSync } from "node:fs";
14 | import got from "got";
15 |
16 | //-----------------------------------------------------------------------------
17 | // Data
18 | //-----------------------------------------------------------------------------
19 |
20 | const SPONSORS_URL =
21 | "https://raw.githubusercontent.com/eslint/eslint.org/main/includes/sponsors.md";
22 |
23 | const README_FILE_PATH = "./README.md";
24 |
25 | //-----------------------------------------------------------------------------
26 | // Helpers
27 | //-----------------------------------------------------------------------------
28 |
29 | /**
30 | * Fetches the latest sponsors from the website.
31 | * @returns {Promise}} Prerendered sponsors markdown.
32 | */
33 | async function fetchSponsorsMarkdown() {
34 | return got(SPONSORS_URL).text();
35 | }
36 |
37 | //-----------------------------------------------------------------------------
38 | // Main
39 | //-----------------------------------------------------------------------------
40 |
41 | const allSponsors = await fetchSponsorsMarkdown();
42 |
43 | // read readme file
44 | const readme = readFileSync(README_FILE_PATH, "utf8");
45 |
46 | let newReadme = readme.replace(
47 | /[\w\W]*?/u,
48 | `\n\n${allSponsors}\n`,
49 | );
50 |
51 | // replace multiple consecutive blank lines with just one blank line
52 | newReadme = newReadme.replace(/(?<=^|\n)\n{2,}/gu, "\n");
53 |
54 | // output to the files
55 | writeFileSync(README_FILE_PATH, newReadme, "utf8");
56 |
--------------------------------------------------------------------------------
/tsconfig.esm.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "files": ["dist/esm/index.js"]
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": ["src/index.js"],
3 | "compilerOptions": {
4 | "declaration": true,
5 | "emitDeclarationOnly": true,
6 | "allowJs": true,
7 | "checkJs": true,
8 | "outDir": "dist/esm",
9 | "target": "ESNext",
10 | "moduleResolution": "NodeNext",
11 | "module": "NodeNext",
12 | "allowImportingTsExtensions": true
13 | }
14 | }
15 |
--------------------------------------------------------------------------------