├── .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 |

AG Grid

Platinum Sponsors

290 |

Automattic Airbnb

Gold Sponsors

291 |

Qlty Software trunk.io Shopify

Silver Sponsors

292 |

Vite Liftoff American Express StackBlitz

Bronze Sponsors

293 |

Cybozu Anagram Solver Icons8 Discord GitBook Neko Nx Mercedes-Benz Group HeroCoders LambdaTest

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 |

Netlify Algolia 1Password

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 | --------------------------------------------------------------------------------