├── explorer ├── .npmrc ├── shim │ ├── module.js │ └── path.js ├── babel.config.js ├── src │ ├── main.js │ ├── App.vue │ └── components │ │ ├── SnsBar.vue │ │ ├── AstOptions.vue │ │ └── MonacoEditor.vue ├── vue.config.js ├── eslint.config.mjs ├── package.json └── public │ └── index.html ├── .npmrc ├── tests ├── fixtures │ └── parser │ │ └── ast │ │ ├── nan-03-input.json5 │ │ ├── binary-02-input.jsonx │ │ ├── null-01-input.json5 │ │ ├── paren-01-input.jsonx │ │ ├── bingint-01-input.jsonx │ │ ├── infinity-03-input.json5 │ │ ├── regexp-01-input.jsonx │ │ ├── template-01-input.json6 │ │ ├── template-03-input.json6 │ │ ├── array-01-input.json5 │ │ ├── array-02-input.json5 │ │ ├── nan-01-input.json5 │ │ ├── number-01-input.json5 │ │ ├── number-02-input.json5 │ │ ├── number-key-01-input.json6 │ │ ├── object-01-input.json5 │ │ ├── string-01-input.json5 │ │ ├── template-02-input.json6 │ │ ├── undefined-01-input.json6 │ │ ├── array-empty-slots-01-input.json6 │ │ ├── array-empty-slots-02-input.json6 │ │ ├── undefined-02-input.json6 │ │ ├── infinity-01-input.json5 │ │ ├── numeric-separator-01-input.json6 │ │ ├── unicode-codepoint-escape01-input.json6 │ │ ├── unicode-codepoint-escape02-input.jsonx │ │ ├── unicode-codepoint-escape03-input.json │ │ ├── comments-01-input.json5 │ │ ├── binary-01-input.jsonx │ │ ├── comments-02-input.json5 │ │ ├── numeric-separator-01-requirements.json │ │ ├── nan-02-input.json5 │ │ ├── comments-03-input.json5 │ │ ├── infinity-02-input.json5 │ │ ├── object-02-input.json5 │ │ ├── object-03-input.json5 │ │ ├── unary-ops-01-input.json6 │ │ ├── nan-03-output.json │ │ ├── infinity-03-output.json │ │ ├── undefined-01-output.json │ │ ├── null-01-output.json │ │ ├── template-01-output.json │ │ ├── paren-01-output.json │ │ ├── comments-02-output.json │ │ ├── comments-01-output.json │ │ ├── comments-03-output.json │ │ ├── array-empty-slots-01-output.json │ │ ├── binary-02-output.json │ │ ├── template-03-output.json │ │ ├── string-01-output.json │ │ ├── array-empty-slots-02-output.json │ │ ├── unicode-codepoint-escape02-output.json │ │ ├── object-01-output.json │ │ ├── bingint-01-output.json │ │ ├── unicode-codepoint-escape01-output.json │ │ ├── unicode-codepoint-escape03-output.json │ │ ├── regexp-01-output.json │ │ ├── template-02-output.json │ │ ├── number-01-output.json │ │ └── number-02-output.json └── src │ ├── meta.ts │ ├── parser │ ├── parser-options.ts │ ├── utils.ts │ ├── parser.ts │ └── syntaxes │ │ ├── json.ts │ │ ├── jsonc.ts │ │ └── json5.ts │ └── utils │ └── ast.ts ├── .env-cmdrc ├── typings ├── espree │ └── index.d.ts └── eslint-utils │ └── index.d.ts ├── tools ├── update.ts ├── lib │ └── changesets-util.ts ├── update-meta.ts ├── update-fixtures.ts └── pkg.pr.new-comment.mjs ├── src ├── meta.ts ├── parser │ ├── utils.ts │ ├── syntax-context.ts │ ├── visitor-keys.ts │ ├── modules │ │ ├── acorn.ts │ │ ├── espree.ts │ │ └── require-utils.ts │ ├── token-store.ts │ ├── traverse.ts │ ├── ast.ts │ ├── errors.ts │ └── extend-parser.ts ├── types.ts └── index.ts ├── tsconfig.build.json ├── .github ├── ISSUE_TEMPLATE │ ├── other.yml │ ├── feature_request.yml │ └── bug_report.yml ├── FUNDING.yml └── workflows │ ├── GHPages.yml │ ├── format.yml │ ├── pkg.pr.new-comment.yml │ ├── stale.yml │ ├── pkg.pr.new.yml │ ├── NodeCI.yml │ └── Release.yml ├── .changeset ├── config.json └── README.md ├── .vscode └── settings.json ├── renovate.json ├── tsconfig.json ├── LICENSE ├── CHANGELOG.md ├── docs └── Plugins.md ├── eslint.config.mjs ├── .gitignore ├── benchmark └── index.ts ├── try-json5and6.html ├── package.json └── README.md /explorer/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | force=true 3 | -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/nan-03-input.json5: -------------------------------------------------------------------------------- 1 | NaN -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/binary-02-input.jsonx: -------------------------------------------------------------------------------- 1 | 42 + 1 -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/null-01-input.json5: -------------------------------------------------------------------------------- 1 | null -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/paren-01-input.jsonx: -------------------------------------------------------------------------------- 1 | (42) -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/bingint-01-input.jsonx: -------------------------------------------------------------------------------- 1 | {"42":42n} -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/infinity-03-input.json5: -------------------------------------------------------------------------------- 1 | Infinity -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/regexp-01-input.jsonx: -------------------------------------------------------------------------------- 1 | {a:/reg/} -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/template-01-input.json6: -------------------------------------------------------------------------------- 1 | `abc` -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/template-03-input.json6: -------------------------------------------------------------------------------- 1 | [`abc`] -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/array-01-input.json5: -------------------------------------------------------------------------------- 1 | [1,2,3,[4,5]] -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/array-02-input.json5: -------------------------------------------------------------------------------- 1 | [1,2,3,[4,5,],] -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/nan-01-input.json5: -------------------------------------------------------------------------------- 1 | [NaN, +NaN, -NaN] -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/number-01-input.json5: -------------------------------------------------------------------------------- 1 | [123,0.12,-123] -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/number-02-input.json5: -------------------------------------------------------------------------------- 1 | [123.,.12,+123] -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/number-key-01-input.json6: -------------------------------------------------------------------------------- 1 | {0:0,42:42} -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/object-01-input.json5: -------------------------------------------------------------------------------- 1 | {"foo":"bar"} -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/string-01-input.json5: -------------------------------------------------------------------------------- 1 | ["abc", 'abd'] -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/template-02-input.json6: -------------------------------------------------------------------------------- 1 | {a: `abc`} -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/undefined-01-input.json6: -------------------------------------------------------------------------------- 1 | undefined -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/array-empty-slots-01-input.json6: -------------------------------------------------------------------------------- 1 | [,'a'] -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/array-empty-slots-02-input.json6: -------------------------------------------------------------------------------- 1 | ['a',,'b'] -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/undefined-02-input.json6: -------------------------------------------------------------------------------- 1 | [undefined, {a: undefined}] -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/infinity-01-input.json5: -------------------------------------------------------------------------------- 1 | [Infinity, +Infinity, -Infinity] -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/numeric-separator-01-input.json6: -------------------------------------------------------------------------------- 1 | [1_2_3, {"a": 1_000_000}] -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/unicode-codepoint-escape01-input.json6: -------------------------------------------------------------------------------- 1 | {"\u{31}":"\u{31}"} -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/unicode-codepoint-escape02-input.jsonx: -------------------------------------------------------------------------------- 1 | {a\u{31}:"\u{31}"} -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/unicode-codepoint-escape03-input.json: -------------------------------------------------------------------------------- 1 | {"\\u{31}":"\\u{31}"} -------------------------------------------------------------------------------- /.env-cmdrc: -------------------------------------------------------------------------------- 1 | { 2 | "version-ci": { 3 | "IN_VERSION_CI_SCRIPT": "true" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /explorer/shim/module.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | createRequire: () => () => null, 3 | }; 4 | -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/comments-01-input.json5: -------------------------------------------------------------------------------- 1 | // comment 2 | { 3 | /* comment */ 4 | } -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/binary-01-input.jsonx: -------------------------------------------------------------------------------- 1 | [42 + 1,42 - 1,42 * 2, ,42 / 2, 5 % 2, 10 ** 2] -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/comments-02-input.json5: -------------------------------------------------------------------------------- 1 | { 2 | /* comment */ 3 | } 4 | // comment 5 | -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/numeric-separator-01-requirements.json: -------------------------------------------------------------------------------- 1 | { 2 | "espree": ">=7.2.0" 3 | } -------------------------------------------------------------------------------- /explorer/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"], 3 | }; 4 | -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/nan-02-input.json5: -------------------------------------------------------------------------------- 1 | { 2 | a: NaN, 3 | b: +NaN, 4 | c:-NaN 5 | } -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/comments-03-input.json5: -------------------------------------------------------------------------------- 1 | { 2 | /* comment */ 3 | } 4 | // comment 5 | /* comment */ 6 | -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/infinity-02-input.json5: -------------------------------------------------------------------------------- 1 | { 2 | a: Infinity, 3 | b: +Infinity, 4 | c:-Infinity 5 | } -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/object-02-input.json5: -------------------------------------------------------------------------------- 1 | { 2 | foo: { 3 | a: 's', 4 | 'b': 123 5 | } 6 | } -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/object-03-input.json5: -------------------------------------------------------------------------------- 1 | { 2 | "foo": { 3 | a: 's', 4 | b: 123, 5 | }, 6 | } -------------------------------------------------------------------------------- /explorer/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | 4 | createApp(App).mount("#app"); 5 | -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/unary-ops-01-input.json6: -------------------------------------------------------------------------------- 1 | [ 2 | -1, 3 | +2, 4 | - 3, 5 | + 4, 6 | - 5, 7 | + 6, 8 | ] -------------------------------------------------------------------------------- /typings/espree/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { Program } from "estree"; 2 | 3 | export function parse(text: string, options?: unknown): Program; 4 | -------------------------------------------------------------------------------- /tools/update.ts: -------------------------------------------------------------------------------- 1 | // import "./update-rules" 2 | // import "./update-rulesets" 3 | // import "./update-docs" 4 | // import "./update-readme" 5 | // import "./update-docs-rules-index" 6 | -------------------------------------------------------------------------------- /src/meta.ts: -------------------------------------------------------------------------------- 1 | // IMPORTANT! 2 | // This file has been automatically generated, 3 | // in order to update its content execute "npm run build:meta" 4 | export const name = "jsonc-eslint-parser" as const; 5 | export const version = "2.4.2" as const; 6 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["tests/**/*", "tools/**/*", "typings/**/*", "benchmark/**/*"], 4 | "compilerOptions": { 5 | "removeComments": true /* Do not emit comments to output. */ 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /explorer/shim/path.js: -------------------------------------------------------------------------------- 1 | const path = { 2 | sep: "/", 3 | }; 4 | module.exports = new Proxy(path, { 5 | get(_t, p) { 6 | if (!path[p]) { 7 | // eslint-disable-next-line no-console -- Log the missing path methods for debugging purposes. 8 | console.log(p); 9 | } 10 | return path[p]; 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.yml: -------------------------------------------------------------------------------- 1 | name: Other 2 | description: An issue that doesn't fit into the other categories. 3 | labels: [] 4 | 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: Description 9 | description: | 10 | A clear and concise description. 11 | Also give a few code examples. 12 | validations: 13 | required: true 14 | -------------------------------------------------------------------------------- /typings/eslint-utils/index.d.ts: -------------------------------------------------------------------------------- 1 | export class PatternMatcher { 2 | public constructor(pattern: RegExp, options?: { escaped?: boolean }); 3 | 4 | public execAll(str: string): IterableIterator; 5 | 6 | public test(str: string): boolean; 7 | 8 | public [Symbol.replace]( 9 | str: string, 10 | replacer: string | ((...ss: string[]) => string), 11 | ): string; 12 | } 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest a new feature. 3 | labels: [enhancement] 4 | 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: Description 9 | description: | 10 | A clear and concise description of the new feature. 11 | Also give a few code examples. 12 | validations: 13 | required: true 14 | -------------------------------------------------------------------------------- /explorer/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: "/jsonc-eslint-parser/", 3 | 4 | configureWebpack(_config, _isServer) { 5 | return { 6 | resolve: { 7 | alias: { 8 | module: require.resolve("./shim/module"), 9 | path: require.resolve("./shim/path"), 10 | }, 11 | fallback: { util: false }, 12 | }, 13 | }; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /tests/src/meta.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import * as parser from "../../src"; 3 | import { version } from "../../package.json"; 4 | const expectedMeta = { 5 | name: "jsonc-eslint-parser", 6 | version, 7 | }; 8 | 9 | describe("Test for meta object", () => { 10 | it("A parser should have a meta object.", () => { 11 | assert.deepStrictEqual(parser.meta, expectedMeta); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { 6 | "repo": "ota-meshi/jsonc-eslint-parser" 7 | } 8 | ], 9 | "commit": false, 10 | "linked": [], 11 | "baseBranch": "master", 12 | "updateInternalDependencies": "patch", 13 | "bumpVersionsWithWorkspaceProtocolOnly": true, 14 | "ignore": [] 15 | } 16 | -------------------------------------------------------------------------------- /tools/lib/changesets-util.ts: -------------------------------------------------------------------------------- 1 | import getReleasePlan from "@changesets/get-release-plan"; 2 | import path from "path"; 3 | 4 | /** Get new version string from changesets */ 5 | export async function getNewVersion(): Promise { 6 | const releasePlan = await getReleasePlan(path.resolve(__dirname, "../..")); 7 | 8 | return releasePlan.releases.find( 9 | ({ name }) => name === "jsonc-eslint-parser", 10 | )!.newVersion; 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": [ 3 | "javascript", 4 | "javascriptreact", 5 | "vue", 6 | "typescript", 7 | "json", 8 | "jsonc" 9 | ], 10 | "typescript.validate.enable": true, 11 | "javascript.validate.enable": false, 12 | "css.validate": false, 13 | "typescript.tsdk": "node_modules/typescript/lib", 14 | "editor.codeActionsOnSave": { 15 | "source.fixAll.eslint": "explicit" 16 | }, 17 | "vetur.validation.template": false 18 | } 19 | -------------------------------------------------------------------------------- /explorer/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import myPlugin from "@ota-meshi/eslint-plugin"; 2 | 3 | export default [ 4 | { 5 | ignores: ["dist/**", "node_modules/**", "shim/**"], 6 | }, 7 | ...myPlugin.config({ 8 | vue3: true, 9 | json: true, 10 | prettier: true, 11 | }), 12 | { 13 | languageOptions: { 14 | sourceType: "module", 15 | ecmaVersion: 2020, 16 | }, 17 | rules: { 18 | "n/no-unsupported-features/es-syntax": "off", 19 | "n/no-missing-import": "off", 20 | }, 21 | }, 22 | ]; 23 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base", 5 | ":preserveSemverRanges", 6 | ":disableDependencyDashboard" 7 | ], 8 | "packageRules": [ 9 | { 10 | "updateTypes": ["minor", "patch", "pin", "digest"], 11 | "automerge": true 12 | }, 13 | { 14 | "depTypeList": ["devDependencies"], 15 | "automerge": true 16 | }, 17 | { 18 | "matchManagers": ["github-actions", "devcontainer"], 19 | "automerge": true 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /explorer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsonc-eslint-parser-ast-explorer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "core-js": "^3.6.5", 11 | "vue": "^3.0.0" 12 | }, 13 | "devDependencies": { 14 | "@vue/cli-plugin-babel": "~5.0.0", 15 | "@vue/cli-service": "~5.0.0", 16 | "@vue/compiler-sfc": "^3.0.0" 17 | }, 18 | "browserslist": [ 19 | "> 1%", 20 | "last 2 versions", 21 | "not dead" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ota-meshi 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /src/parser/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Literal, RegExpLiteral } from "estree"; 2 | import type { JSONLiteral, JSONRegExpLiteral } from "./ast"; 3 | 4 | export function isRegExpLiteral(node: JSONLiteral): node is JSONRegExpLiteral; 5 | export function isRegExpLiteral(node: Literal): node is RegExpLiteral; 6 | export function isRegExpLiteral( 7 | node: JSONLiteral | Literal, 8 | ): node is JSONRegExpLiteral | RegExpLiteral; 9 | /** 10 | * Check if the given node is RegExpLiteral 11 | */ 12 | export function isRegExpLiteral( 13 | node: JSONLiteral | Literal, 14 | ): node is JSONRegExpLiteral | RegExpLiteral { 15 | return ( 16 | Boolean((node as JSONRegExpLiteral | RegExpLiteral).regex) || 17 | node.raw!.startsWith("/") 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /explorer/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | jsonc-eslint-parser 9 | 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "lib": ["es2020"], 6 | "allowJs": true, 7 | "checkJs": true, 8 | "outDir": "./lib", 9 | "strict": true, 10 | "noImplicitAny": true, 11 | 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "*": ["typings/*"] 19 | }, 20 | "declaration": true, 21 | "esModuleInterop": true, 22 | "resolveJsonModule": true, 23 | 24 | "skipLibCheck": true 25 | }, 26 | "include": [ 27 | "src/**/*", 28 | "tests/src/**/*", 29 | "tools/**/*", 30 | "typings/**/*", 31 | "benchmark/**/*" 32 | ], 33 | "exclude": ["lib/**/*"] 34 | } 35 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { AST } from "jsonc-eslint-parser"; 2 | 3 | export type JSONSyntax = "JSON" | "JSONC" | "JSON5" | null; 4 | 5 | export interface JSONParserOptions { 6 | jsonSyntax?: JSONSyntax; 7 | } 8 | 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- used for compatibility with ESLint types 10 | export type RuleFunction = ( 11 | node: Node, 12 | ) => void; 13 | 14 | export type BuiltInRuleListeners = { 15 | [Node in AST.JSONNode as Node["type"]]?: RuleFunction; 16 | }; 17 | 18 | export type BuiltInRuleListenerExits = { 19 | [Node in AST.JSONNode as `${Node["type"]}:exit`]?: RuleFunction; 20 | }; 21 | 22 | export interface RuleListener 23 | extends BuiltInRuleListeners, 24 | BuiltInRuleListenerExits { 25 | [key: string]: RuleFunction | undefined; 26 | } 27 | -------------------------------------------------------------------------------- /src/parser/syntax-context.ts: -------------------------------------------------------------------------------- 1 | export type JSONSyntaxContext = { 2 | trailingCommas: boolean; 3 | comments: boolean; 4 | // invalid JSON numbers 5 | plusSigns: boolean; 6 | spacedSigns: boolean; 7 | leadingOrTrailingDecimalPoints: boolean; 8 | infinities: boolean; 9 | nans: boolean; 10 | numericSeparators: boolean; 11 | binaryNumericLiterals: boolean; 12 | octalNumericLiterals: boolean; 13 | legacyOctalNumericLiterals: boolean; 14 | invalidJsonNumbers: boolean; 15 | // statics 16 | multilineStrings: boolean; 17 | unquoteProperties: boolean; 18 | singleQuotes: boolean; 19 | numberProperties: boolean; 20 | undefinedKeywords: boolean; 21 | sparseArrays: boolean; 22 | regExpLiterals: boolean; 23 | templateLiterals: boolean; 24 | bigintLiterals: boolean; 25 | unicodeCodepointEscapes: boolean; 26 | escapeSequenceInIdentifier: boolean; 27 | // JS-likes 28 | parentheses: boolean; 29 | staticExpressions: boolean; 30 | }; 31 | -------------------------------------------------------------------------------- /explorer/src/App.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 28 | 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Yosuke Ota 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { parseForESLint } from "./parser/parser"; 2 | import { traverseNodes } from "./parser/traverse"; 3 | import { 4 | getStaticJSONValue, 5 | isExpression, 6 | isNumberIdentifier, 7 | isUndefinedIdentifier, 8 | } from "./utils/ast"; 9 | 10 | import type * as AST from "./parser/ast"; 11 | import { getVisitorKeys } from "./parser/visitor-keys"; 12 | export * as meta from "./meta"; 13 | export { name } from "./meta"; 14 | export type * from "./types"; 15 | 16 | // parser 17 | export { parseForESLint }; 18 | // Keys 19 | // eslint-disable-next-line @typescript-eslint/naming-convention -- parser module 20 | export const VisitorKeys = getVisitorKeys(); 21 | 22 | // tools 23 | export { 24 | traverseNodes, 25 | getStaticJSONValue, 26 | isExpression, 27 | isNumberIdentifier, 28 | isUndefinedIdentifier, 29 | }; 30 | 31 | /** 32 | * Parse JSON source code 33 | */ 34 | export function parseJSON( 35 | code: string, 36 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- any 37 | options?: any, 38 | ): AST.JSONProgram { 39 | return parseForESLint(code, options).ast as never; 40 | } 41 | 42 | // types 43 | export { AST }; 44 | -------------------------------------------------------------------------------- /tools/update-meta.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { ESLint } from "eslint"; 4 | import { name, version } from "../package.json"; 5 | import { getNewVersion } from "./lib/changesets-util"; 6 | 7 | const META_PATH = path.join(__dirname, "../src/meta.ts"); 8 | 9 | void main(); 10 | 11 | /** main */ 12 | async function main() { 13 | if (!fs.existsSync(META_PATH)) { 14 | fs.writeFileSync(META_PATH, "", "utf8"); 15 | } 16 | const eslint = new ESLint({ fix: true }); 17 | const [result] = await eslint.lintText( 18 | `/* 19 | * IMPORTANT! 20 | * This file has been automatically generated, 21 | * in order to update its content execute "npm run build:meta" 22 | */ 23 | export const name = ${JSON.stringify(name)} as const; 24 | export const version = ${JSON.stringify(await getVersion())} as const; 25 | `, 26 | { filePath: META_PATH }, 27 | ); 28 | fs.writeFileSync(META_PATH, result.output!); 29 | } 30 | 31 | /** Get version */ 32 | function getVersion() { 33 | // eslint-disable-next-line no-process-env -- ignore 34 | if (process.env.IN_VERSION_CI_SCRIPT) { 35 | return getNewVersion(); 36 | } 37 | return version; 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/GHPages.yml: -------------------------------------------------------------------------------- 1 | name: GHPages 2 | 3 | on: 4 | workflow_dispatch: null 5 | push: 6 | branches: [master] 7 | 8 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | # Allow one concurrent deployment 15 | concurrency: 16 | group: ${{ github.workflow }}-${{ github.ref }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | deploy: 21 | environment: 22 | name: github-pages 23 | url: ${{ steps.deployment.outputs.page_url }} 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v6 28 | - uses: actions/setup-node@v6 29 | - name: Install And Build 30 | run: |+ 31 | npm install 32 | npm run build 33 | cd explorer 34 | npm install 35 | npm run build 36 | - name: Setup Pages 37 | uses: actions/configure-pages@v5 38 | - name: Upload artifact 39 | uses: actions/upload-pages-artifact@v4 40 | with: 41 | path: ./explorer/dist 42 | - name: Deploy to GitHub Pages 43 | id: deployment 44 | uses: actions/deploy-pages@v4 45 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: 👔 Format 2 | 3 | on: 4 | workflow_dispatch: null 5 | 6 | permissions: 7 | contents: write 8 | 9 | # Allow one concurrent deployment 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | format: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout repo 20 | uses: actions/checkout@v6 21 | - name: Setup node 22 | uses: actions/setup-node@v6 23 | - name: Install deps 24 | run: npm install 25 | - name: Build 26 | run: npm run build 27 | - name: Format 28 | run: npm run eslint-fix 29 | - name: Commit 30 | run: | 31 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 32 | git config --local user.name "github-actions[bot]" 33 | 34 | git add . 35 | if [ -z "$(git status --porcelain)" ]; then 36 | echo "no formatting changed" 37 | exit 0 38 | fi 39 | git commit -m "chore: format" 40 | git push 41 | echo "pushed formatting changes https://github.com/$GITHUB_REPOSITORY/commit/$(git rev-parse HEAD)" 42 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # jsonc-eslint-parser 2 | 3 | ## 2.4.2 4 | 5 | ### Patch Changes 6 | 7 | - [#252](https://github.com/ota-meshi/jsonc-eslint-parser/pull/252) [`afaafe7`](https://github.com/ota-meshi/jsonc-eslint-parser/commit/afaafe79b66c4e3adec94a6346b1fd6a93deab7b) Thanks [@ota-meshi](https://github.com/ota-meshi)! - use npm trusted publishing 8 | 9 | ## 2.4.1 10 | 11 | ### Patch Changes 12 | 13 | - [#228](https://github.com/ota-meshi/jsonc-eslint-parser/pull/228) [`46c1f97`](https://github.com/ota-meshi/jsonc-eslint-parser/commit/46c1f97d50daf959df636e0681a109fe09a98138) Thanks [@michaelfaith](https://github.com/michaelfaith)! - improve type compatibility with eslint 14 | 15 | ## 2.4.0 16 | 17 | ### Minor Changes 18 | 19 | - [#186](https://github.com/ota-meshi/jsonc-eslint-parser/pull/186) [`8027edd`](https://github.com/ota-meshi/jsonc-eslint-parser/commit/8027eddbb8a54f965dc480792fb31382bad131f2) Thanks [@JoshuaKGoldberg](https://github.com/JoshuaKGoldberg)! - Added ESLint-oriented types, with plugin docs 20 | 21 | ## 2.3.0 22 | 23 | ### Minor Changes 24 | 25 | - [#162](https://github.com/ota-meshi/jsonc-eslint-parser/pull/162) [`9afa0c4`](https://github.com/ota-meshi/jsonc-eslint-parser/commit/9afa0c452de7192970145aa3588b85530e23cae9) Thanks [@ota-meshi](https://github.com/ota-meshi)! - feat: export meta object 26 | -------------------------------------------------------------------------------- /.github/workflows/pkg.pr.new-comment.yml: -------------------------------------------------------------------------------- 1 | name: Update pkg.pr.new comment 2 | 3 | on: 4 | workflow_run: 5 | workflows: [Publish to pkg.pr.new] 6 | types: 7 | - completed 8 | 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | 13 | # Allow one concurrent deployment 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | build: 20 | if: github.repository == 'ota-meshi/jsonc-eslint-parser' 21 | name: Update comment 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v6 25 | - name: Download artifact 26 | uses: actions/download-artifact@v7 27 | with: 28 | name: output 29 | github-token: ${{ secrets.GITHUB_TOKEN }} 30 | run-id: ${{ github.event.workflow_run.id }} 31 | - run: ls -R . 32 | - name: Post or update comment 33 | uses: actions/github-script@v8 34 | with: 35 | github-token: ${{ secrets.GITHUB_TOKEN }} 36 | script: | 37 | const fs = require('fs'); 38 | const output = JSON.parse(fs.readFileSync('output.json', 'utf8')); 39 | const { default: process } = await import('${{ github.workspace }}/tools/pkg.pr.new-comment.mjs') 40 | 41 | await process({github, context, core, output}) 42 | -------------------------------------------------------------------------------- /docs/Plugins.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | Users of plugins that rely on `jsonc-eslint-parser` need to explicitly configure the parser for files linted with that plugin. 4 | 5 | Consider including snippets like the following in the plugin's documentation: 6 | 7 | ```shell 8 | npm install eslint eslint-plugin-your-name-here jsonc-eslint-parser --save-dev 9 | ``` 10 | 11 | ```js 12 | module.exports = { 13 | // ... 14 | overrides: [ 15 | { 16 | files: ["*.json", "*.json5"], 17 | extends: ["plugin:your-name-here/recommended"], 18 | parser: "jsonc-eslint-parser", 19 | plugins: ["your-name-here"], 20 | }, 21 | ], 22 | }; 23 | ``` 24 | 25 | See [`eslint-plugin-jsonc`](https://github.com/ota-meshi/eslint-plugin-jsonc) for an example package. 26 | 27 | ## TypeScript 28 | 29 | `jsonc-eslint-parser` exports types that replace the following built-in ESLint types: 30 | 31 | - `RuleFunction`: Sets the `node` parameter to be an `AST.JSONNode` or `never` 32 | - `RuleListener`: Replaces built-in rule listeners with JSON node types 33 | - For example, `JSONLiteral(node) {` sets type `AST.JSONLiteral` for `node` 34 | - It also sets the equivalent `:exit` types, such as `'JSONLiteral:exit(node) {` 35 | 36 | See [`eslint-plugin-jsonc`](https://github.com/ota-meshi/eslint-plugin-jsonc)'s [`lib/types.ts`](https://github.com/ota-meshi/eslint-plugin-jsonc/blob/master/lib/types.ts) for example usage of this parser's TypeScript types. 37 | -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/nan-03-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Program", 3 | "body": [ 4 | { 5 | "type": "JSONExpressionStatement", 6 | "expression": { 7 | "type": "JSONIdentifier", 8 | "name": "NaN", 9 | "range": [ 10 | 0, 11 | 3 12 | ], 13 | "loc": { 14 | "start": { 15 | "line": 1, 16 | "column": 0 17 | }, 18 | "end": { 19 | "line": 1, 20 | "column": 3 21 | } 22 | } 23 | }, 24 | "range": [ 25 | 0, 26 | 3 27 | ], 28 | "loc": { 29 | "start": { 30 | "line": 1, 31 | "column": 0 32 | }, 33 | "end": { 34 | "line": 1, 35 | "column": 3 36 | } 37 | } 38 | } 39 | ], 40 | "comments": [], 41 | "tokens": [ 42 | { 43 | "type": "Identifier", 44 | "value": "NaN", 45 | "range": [ 46 | 0, 47 | 3 48 | ], 49 | "loc": { 50 | "start": { 51 | "line": 1, 52 | "column": 0 53 | }, 54 | "end": { 55 | "line": 1, 56 | "column": 3 57 | } 58 | } 59 | } 60 | ], 61 | "range": [ 62 | 0, 63 | 3 64 | ], 65 | "loc": { 66 | "start": { 67 | "line": 1, 68 | "column": 0 69 | }, 70 | "end": { 71 | "line": 1, 72 | "column": 3 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/infinity-03-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Program", 3 | "body": [ 4 | { 5 | "type": "JSONExpressionStatement", 6 | "expression": { 7 | "type": "JSONIdentifier", 8 | "name": "Infinity", 9 | "range": [ 10 | 0, 11 | 8 12 | ], 13 | "loc": { 14 | "start": { 15 | "line": 1, 16 | "column": 0 17 | }, 18 | "end": { 19 | "line": 1, 20 | "column": 8 21 | } 22 | } 23 | }, 24 | "range": [ 25 | 0, 26 | 8 27 | ], 28 | "loc": { 29 | "start": { 30 | "line": 1, 31 | "column": 0 32 | }, 33 | "end": { 34 | "line": 1, 35 | "column": 8 36 | } 37 | } 38 | } 39 | ], 40 | "comments": [], 41 | "tokens": [ 42 | { 43 | "type": "Identifier", 44 | "value": "Infinity", 45 | "range": [ 46 | 0, 47 | 8 48 | ], 49 | "loc": { 50 | "start": { 51 | "line": 1, 52 | "column": 0 53 | }, 54 | "end": { 55 | "line": 1, 56 | "column": 8 57 | } 58 | } 59 | } 60 | ], 61 | "range": [ 62 | 0, 63 | 8 64 | ], 65 | "loc": { 66 | "start": { 67 | "line": 1, 68 | "column": 0 69 | }, 70 | "end": { 71 | "line": 1, 72 | "column": 8 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/undefined-01-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Program", 3 | "body": [ 4 | { 5 | "type": "JSONExpressionStatement", 6 | "expression": { 7 | "type": "JSONIdentifier", 8 | "name": "undefined", 9 | "range": [ 10 | 0, 11 | 9 12 | ], 13 | "loc": { 14 | "start": { 15 | "line": 1, 16 | "column": 0 17 | }, 18 | "end": { 19 | "line": 1, 20 | "column": 9 21 | } 22 | } 23 | }, 24 | "range": [ 25 | 0, 26 | 9 27 | ], 28 | "loc": { 29 | "start": { 30 | "line": 1, 31 | "column": 0 32 | }, 33 | "end": { 34 | "line": 1, 35 | "column": 9 36 | } 37 | } 38 | } 39 | ], 40 | "comments": [], 41 | "tokens": [ 42 | { 43 | "type": "Identifier", 44 | "value": "undefined", 45 | "range": [ 46 | 0, 47 | 9 48 | ], 49 | "loc": { 50 | "start": { 51 | "line": 1, 52 | "column": 0 53 | }, 54 | "end": { 55 | "line": 1, 56 | "column": 9 57 | } 58 | } 59 | } 60 | ], 61 | "range": [ 62 | 0, 63 | 9 64 | ], 65 | "loc": { 66 | "start": { 67 | "line": 1, 68 | "column": 0 69 | }, 70 | "end": { 71 | "line": 1, 72 | "column": 9 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/null-01-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Program", 3 | "body": [ 4 | { 5 | "type": "JSONExpressionStatement", 6 | "expression": { 7 | "type": "JSONLiteral", 8 | "value": null, 9 | "raw": "null", 10 | "range": [ 11 | 0, 12 | 4 13 | ], 14 | "loc": { 15 | "start": { 16 | "line": 1, 17 | "column": 0 18 | }, 19 | "end": { 20 | "line": 1, 21 | "column": 4 22 | } 23 | } 24 | }, 25 | "range": [ 26 | 0, 27 | 4 28 | ], 29 | "loc": { 30 | "start": { 31 | "line": 1, 32 | "column": 0 33 | }, 34 | "end": { 35 | "line": 1, 36 | "column": 4 37 | } 38 | } 39 | } 40 | ], 41 | "comments": [], 42 | "tokens": [ 43 | { 44 | "type": "Null", 45 | "value": "null", 46 | "range": [ 47 | 0, 48 | 4 49 | ], 50 | "loc": { 51 | "start": { 52 | "line": 1, 53 | "column": 0 54 | }, 55 | "end": { 56 | "line": 1, 57 | "column": 4 58 | } 59 | } 60 | } 61 | ], 62 | "range": [ 63 | 0, 64 | 4 65 | ], 66 | "loc": { 67 | "start": { 68 | "line": 1, 69 | "column": 0 70 | }, 71 | "end": { 72 | "line": 1, 73 | "column": 4 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /tests/src/parser/parser-options.ts: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-require-imports:0, @typescript-eslint/no-var-requires:0 -- for test */ 2 | import assert from "assert"; 3 | import semver from "semver"; 4 | 5 | import { parseForESLint } from "../../../src/parser/parser"; 6 | import { Linter } from "eslint"; 7 | 8 | describe("Parser options.", () => { 9 | for (const { code, parserOptions, errors } of [ 10 | ...(semver.satisfies(require("espree/package.json").version, ">=7.2.0") 11 | ? [ 12 | { 13 | code: "1_2_3", 14 | parserOptions: { 15 | ecmaVersion: "latest", 16 | }, 17 | errors: [], 18 | }, 19 | { 20 | code: "1_2_3", 21 | parserOptions: { 22 | ecmaVersion: 2099, 23 | }, 24 | errors: [], 25 | }, 26 | ] 27 | : []), 28 | ]) { 29 | it(`${JSON.stringify(code)} with parserOptions: ${JSON.stringify( 30 | parserOptions, 31 | )}`, () => { 32 | const linter = new Linter({ configType: "eslintrc" }); 33 | linter.defineParser("jsonc-eslint-parser", { 34 | parseForESLint: parseForESLint as never, 35 | }); 36 | 37 | const result = linter.verify( 38 | code, 39 | { 40 | parser: "jsonc-eslint-parser", 41 | parserOptions: parserOptions as never, 42 | }, 43 | "test.json", 44 | ); 45 | assert.deepStrictEqual(result, errors); 46 | }); 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close stale issues and PRs 2 | on: 3 | schedule: 4 | - cron: "30 1 * * *" 5 | 6 | permissions: 7 | issues: write 8 | pull-requests: write 9 | 10 | jobs: 11 | stale: 12 | if: github.repository == 'ota-meshi/jsonc-eslint-parser' 13 | name: Close stale issues with missing information 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/stale@v10 17 | with: 18 | any-of-labels: "needs repro,needs info,needs more info" 19 | days-before-stale: 60 20 | days-before-close: 14 21 | stale-issue-message: This issue is is stale because it missing information and has been open for 60 days with no activity. 22 | stale-pr-message: This PR is is stale because it missing information and has been open for 60 days with no activity. 23 | close-issue-message: > 24 | This issue has been automatically closed because we haven't received a 25 | response from the original author 🙈. This automation helps keep the issue 26 | tracker clean from issues that aren't actionable. Please reach out if you 27 | have more information for us! 🙂 28 | close-pr-message: > 29 | This PR has been automatically closed because we haven't received a 30 | response from the original author 🙈. This automation helps keep the issue 31 | tracker clean from PRs that aren't actionable. Please reach out if you 32 | have more information for us! 🙂 33 | -------------------------------------------------------------------------------- /tools/update-fixtures.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs"; 3 | 4 | import { parseForESLint } from "../src/parser/parser"; 5 | import { nodeReplacer } from "../tests/src/parser/utils"; 6 | 7 | const FIXTURE_ROOT = path.resolve(__dirname, "../tests/fixtures/parser/ast"); 8 | 9 | /** 10 | * Parse 11 | */ 12 | function parse(code: string) { 13 | return parseForESLint(code, { 14 | comment: true, 15 | ecmaVersion: 2021, 16 | eslintScopeManager: true, 17 | eslintVisitorKeys: true, 18 | filePath: "test.json", 19 | loc: true, 20 | range: true, 21 | raw: true, 22 | tokens: true, 23 | }); 24 | } 25 | 26 | for (const filename of fs 27 | .readdirSync(FIXTURE_ROOT) 28 | .filter( 29 | (f) => 30 | f.endsWith("input.json5") || 31 | f.endsWith("input.json6") || 32 | f.endsWith("input.jsonx") || 33 | f.endsWith("input.jsonc") || 34 | f.endsWith("input.json"), 35 | )) { 36 | const inputFileName = path.join(FIXTURE_ROOT, filename); 37 | const outputFileName = inputFileName.replace( 38 | /input\.json[56cx]?$/u, 39 | "output.json", 40 | ); 41 | 42 | const input = fs.readFileSync(inputFileName, "utf8"); 43 | try { 44 | const ast = JSON.stringify(parse(input).ast, nodeReplacer, 2); 45 | fs.writeFileSync(outputFileName, ast, "utf8"); 46 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore 47 | } catch (e: any) { 48 | fs.writeFileSync( 49 | outputFileName, 50 | `${e.message}@line:${e.lineNumber},column:${e.column}`, 51 | "utf8", 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/pkg.pr.new.yml: -------------------------------------------------------------------------------- 1 | name: Publish to pkg.pr.new 2 | on: 3 | pull_request: 4 | branches: [main] 5 | push: 6 | branches: [main] 7 | tags: ["!**"] 8 | 9 | # Allow one concurrent deployment 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | build: 16 | if: github.repository == 'ota-meshi/jsonc-eslint-parser' 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v6 21 | - uses: actions/setup-node@v6 22 | - name: Install Packages 23 | run: npm install 24 | - name: Build 25 | run: npm run build 26 | - run: npx pkg-pr-new publish --compact '.' --json output.json --comment=off 27 | - name: Add metadata to output 28 | uses: actions/github-script@v8 29 | with: 30 | github-token: ${{ secrets.GITHUB_TOKEN }} 31 | script: | 32 | const fs = require('fs'); 33 | const output = JSON.parse(fs.readFileSync('output.json', 'utf8')); 34 | output.number = context.issue.number; 35 | output.event_name = context.eventName; 36 | output.ref = context.ref; 37 | output.sha = context.eventName === 'pull_request' 38 | ? context.payload.pull_request.head.sha 39 | : context.payload.after; 40 | fs.writeFileSync('output.json', JSON.stringify(output), 'utf8'); 41 | - name: Upload output 42 | uses: actions/upload-artifact@v6 43 | with: 44 | name: output 45 | path: ./output.json 46 | 47 | - run: ls -R . 48 | -------------------------------------------------------------------------------- /.github/workflows/NodeCI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | # Allow one concurrent deployment 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | lint: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v6 19 | - uses: actions/setup-node@v6 20 | - uses: actions/cache@v5 21 | with: 22 | path: node_modules 23 | key: ${{ runner.os }}-${{ hashFiles('package.json') }}-${{ github.ref }} 24 | - name: Install Packages 25 | run: npm install -f 26 | - name: Lint 27 | run: npm run lint 28 | - name: Build 29 | run: npm run build 30 | test: 31 | runs-on: ubuntu-latest 32 | strategy: 33 | matrix: 34 | node-version: [16.x, 18.x, 20.x, 22.x, 24.x] 35 | steps: 36 | - uses: actions/checkout@v6 37 | - name: Use Node.js ${{ matrix.node-version }} 38 | uses: actions/setup-node@v6 39 | with: 40 | node-version: ${{ matrix.node-version }} 41 | - name: Install Packages 42 | run: npm install -f 43 | - name: Test 44 | run: npm test 45 | test-for-old-node: 46 | runs-on: ubuntu-latest 47 | strategy: 48 | matrix: 49 | node-version: [12.x, 14.x] 50 | steps: 51 | - uses: actions/checkout@v6 52 | - name: Use Node.js ${{ matrix.node-version }} 53 | uses: actions/setup-node@v6 54 | with: 55 | node-version: ${{ matrix.node-version }} 56 | - name: Install Target Packages 57 | run: |+ 58 | npm install @typescript-eslint/parser@5 --legacy-peer-deps 59 | npm install --legacy-peer-deps 60 | - name: Test 61 | run: npm test 62 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import myPlugin from "@ota-meshi/eslint-plugin"; 2 | 3 | export default [ 4 | { 5 | ignores: [ 6 | "lib/**", 7 | "coverage/**", 8 | "explorer/**", 9 | "node_modules/**", 10 | ".nyc_output/**", 11 | "tests/fixtures/**", 12 | ], 13 | }, 14 | ...myPlugin.config({ 15 | node: true, 16 | json: true, 17 | prettier: true, 18 | packageJson: true, 19 | ts: true, 20 | }), 21 | { 22 | files: ["**/*.mjs"], 23 | languageOptions: { 24 | sourceType: "module", 25 | }, 26 | }, 27 | { 28 | files: ["**/*.ts"], 29 | languageOptions: { 30 | sourceType: "module", 31 | parserOptions: { 32 | project: "./tsconfig.json", 33 | }, 34 | }, 35 | rules: { 36 | "@typescript-eslint/no-non-null-assertion": "off", 37 | "@typescript-eslint/naming-convention": [ 38 | "error", 39 | { 40 | selector: "default", 41 | format: ["camelCase"], 42 | leadingUnderscore: "allow", 43 | trailingUnderscore: "allow", 44 | }, 45 | 46 | { 47 | selector: "variable", 48 | format: ["camelCase", "UPPER_CASE"], 49 | leadingUnderscore: "allow", 50 | trailingUnderscore: "allow", 51 | }, 52 | 53 | { 54 | selector: "typeLike", 55 | format: ["PascalCase"], 56 | }, 57 | { 58 | selector: "property", 59 | format: ["camelCase", "PascalCase"], 60 | }, 61 | { 62 | selector: "method", 63 | format: ["camelCase", "PascalCase"], 64 | }, 65 | { 66 | selector: "import", 67 | format: ["camelCase", "PascalCase", "UPPER_CASE"], 68 | }, 69 | ], 70 | }, 71 | }, 72 | ]; 73 | -------------------------------------------------------------------------------- /.github/workflows/Release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | permissions: {} 9 | 10 | # Allow one concurrent deployment 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | release: 17 | # prevents this action from running on forks 18 | if: github.repository == 'ota-meshi/jsonc-eslint-parser' 19 | permissions: 20 | contents: write # to create release (changesets/action) 21 | id-token: write # OpenID Connect token needed for provenance 22 | pull-requests: write # to create pull request (changesets/action) 23 | name: Release 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout Repo 27 | uses: actions/checkout@v6 28 | with: 29 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 30 | fetch-depth: 0 31 | - name: Setup Node.js 32 | uses: actions/setup-node@v6 33 | with: 34 | node-version: lts/* 35 | - name: Install Dependencies 36 | run: npm install 37 | - name: Build 38 | run: npm run build 39 | 40 | - name: Create Release Pull Request or Publish to npm 41 | id: changesets 42 | uses: changesets/action@v1 43 | with: 44 | # this expects you to have a npm script called version that runs some logic and then calls `changeset version`. 45 | version: npm run version:ci 46 | # This expects you to have a script called release which does a build for your packages and calls changeset publish 47 | publish: npm run release 48 | commit: "chore: release jsonc-eslint-parser" 49 | title: "chore: release jsonc-eslint-parser" 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | -------------------------------------------------------------------------------- /explorer/src/components/SnsBar.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 52 | 53 | 71 | -------------------------------------------------------------------------------- /src/parser/visitor-keys.ts: -------------------------------------------------------------------------------- 1 | import type { SourceCode } from "eslint"; 2 | import type * as Evk from "eslint-visitor-keys"; 3 | import type { JSONNode } from "./ast"; 4 | import { 5 | loadNewest, 6 | requireFromCwd, 7 | requireFromLinter, 8 | } from "./modules/require-utils"; 9 | 10 | const jsonKeys: { [key in JSONNode["type"]]: string[] } = { 11 | Program: ["body"], 12 | JSONExpressionStatement: ["expression"], 13 | JSONArrayExpression: ["elements"], 14 | JSONObjectExpression: ["properties"], 15 | JSONProperty: ["key", "value"], 16 | JSONIdentifier: [], 17 | JSONLiteral: [], 18 | JSONUnaryExpression: ["argument"], 19 | JSONTemplateLiteral: ["quasis", "expressions"], 20 | JSONTemplateElement: [], 21 | JSONBinaryExpression: ["left", "right"], 22 | }; 23 | 24 | let cache: SourceCode.VisitorKeys | null = null; 25 | /** 26 | * Get visitor keys 27 | */ 28 | export function getVisitorKeys(): SourceCode.VisitorKeys { 29 | if (!cache) { 30 | const vk: typeof Evk = loadNewest([ 31 | { 32 | getPkg() { 33 | return requireFromCwd("eslint-visitor-keys/package.json"); 34 | }, 35 | get() { 36 | return requireFromCwd("eslint-visitor-keys"); 37 | }, 38 | }, 39 | { 40 | getPkg() { 41 | return requireFromLinter("eslint-visitor-keys/package.json"); 42 | }, 43 | get() { 44 | return requireFromLinter("eslint-visitor-keys"); 45 | }, 46 | }, 47 | { 48 | getPkg() { 49 | // eslint-disable-next-line @typescript-eslint/no-require-imports -- special require 50 | return require("eslint-visitor-keys/package.json"); 51 | }, 52 | get() { 53 | // eslint-disable-next-line @typescript-eslint/no-require-imports -- special require 54 | return require("eslint-visitor-keys"); 55 | }, 56 | }, 57 | ]); 58 | 59 | cache = vk.unionWith(jsonKeys) as SourceCode.VisitorKeys; 60 | } 61 | return cache; 62 | } 63 | -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/template-01-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Program", 3 | "body": [ 4 | { 5 | "type": "JSONExpressionStatement", 6 | "expression": { 7 | "type": "JSONTemplateLiteral", 8 | "quasis": [ 9 | { 10 | "type": "JSONTemplateElement", 11 | "tail": true, 12 | "value": { 13 | "raw": "abc", 14 | "cooked": "abc" 15 | }, 16 | "range": [ 17 | 0, 18 | 5 19 | ], 20 | "loc": { 21 | "start": { 22 | "line": 1, 23 | "column": 0 24 | }, 25 | "end": { 26 | "line": 1, 27 | "column": 5 28 | } 29 | } 30 | } 31 | ], 32 | "expressions": [], 33 | "range": [ 34 | 0, 35 | 5 36 | ], 37 | "loc": { 38 | "start": { 39 | "line": 1, 40 | "column": 0 41 | }, 42 | "end": { 43 | "line": 1, 44 | "column": 5 45 | } 46 | } 47 | }, 48 | "range": [ 49 | 0, 50 | 5 51 | ], 52 | "loc": { 53 | "start": { 54 | "line": 1, 55 | "column": 0 56 | }, 57 | "end": { 58 | "line": 1, 59 | "column": 5 60 | } 61 | } 62 | } 63 | ], 64 | "comments": [], 65 | "tokens": [ 66 | { 67 | "type": "Template", 68 | "value": "`abc`", 69 | "range": [ 70 | 0, 71 | 5 72 | ], 73 | "loc": { 74 | "start": { 75 | "line": 1, 76 | "column": 0 77 | }, 78 | "end": { 79 | "line": 1, 80 | "column": 5 81 | } 82 | } 83 | } 84 | ], 85 | "range": [ 86 | 0, 87 | 5 88 | ], 89 | "loc": { 90 | "start": { 91 | "line": 1, 92 | "column": 0 93 | }, 94 | "end": { 95 | "line": 1, 96 | "column": 5 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | # typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | /lib 107 | /dist-ts 108 | /index.d.ts 109 | 110 | # Agents 111 | .claude 112 | -------------------------------------------------------------------------------- /benchmark/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsdoc/require-jsdoc, no-console -- benchmark */ 2 | import * as Benchmark from "benchmark"; 3 | import fs from "fs"; 4 | import { parseForESLint } from ".."; 5 | import { parseForESLint as parseOld } from "../node_modules/jsonc-eslint-parser"; 6 | 7 | const contents = `${fs.readFileSync( 8 | require.resolve("../package.json"), 9 | "utf-8", 10 | )}// comments`; 11 | 12 | type Result = { name: string; hz: number }; 13 | const results: Result[] = []; 14 | 15 | function format(hz: number): string { 16 | return (~~(hz * 100) / 100).toString().padEnd(4, " ").padStart(6, " "); 17 | } 18 | 19 | function onCycle(event: { target: Result }): void { 20 | const { name, hz } = event.target; 21 | results.push({ name, hz }); 22 | 23 | console.log(event.target.toString()); 24 | } 25 | 26 | function onComplete(): void { 27 | console.log("-".repeat(72)); 28 | const map: Record = {}; 29 | for (const result of results) { 30 | const r = (map[result.name.slice(2)] ??= []); 31 | r.push(result.hz); 32 | } 33 | for (const name of Object.keys(map)) { 34 | console.log( 35 | `${name.padEnd(15)} ${format( 36 | map[name].reduce((p, a) => p + a, 0) / map[name].length, 37 | )} ops/sec`, 38 | ); 39 | } 40 | for (let i = 0; i < results.length; ++i) { 41 | const result = results[i]; 42 | 43 | console.log(`${result.name.padEnd(15)} ${format(result.hz)} ops/sec`); 44 | } 45 | } 46 | 47 | const suite = new Benchmark.Suite("benchmark", { onCycle, onComplete }); 48 | 49 | for (const no of [1, 2, 3]) { 50 | suite.add(`${no} new jsonc-eslint-parser`, function () { 51 | parseForESLint(contents, { 52 | loc: true, 53 | range: true, 54 | raw: true, 55 | tokens: true, 56 | comment: true, 57 | eslintVisitorKeys: true, 58 | eslintScopeManager: true, 59 | }); 60 | }); 61 | suite.add(`${no} old jsonc-eslint-parser`, function () { 62 | parseOld(contents, { 63 | loc: true, 64 | range: true, 65 | raw: true, 66 | tokens: true, 67 | comment: true, 68 | eslintVisitorKeys: true, 69 | eslintScopeManager: true, 70 | }); 71 | }); 72 | } 73 | 74 | suite.run(); 75 | /* eslint-enable jsdoc/require-jsdoc, no-console -- benchmark */ 76 | -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/paren-01-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Program", 3 | "body": [ 4 | { 5 | "type": "JSONExpressionStatement", 6 | "expression": { 7 | "type": "JSONLiteral", 8 | "value": 42, 9 | "raw": "42", 10 | "range": [ 11 | 1, 12 | 3 13 | ], 14 | "loc": { 15 | "start": { 16 | "line": 1, 17 | "column": 1 18 | }, 19 | "end": { 20 | "line": 1, 21 | "column": 3 22 | } 23 | } 24 | }, 25 | "range": [ 26 | 1, 27 | 3 28 | ], 29 | "loc": { 30 | "start": { 31 | "line": 1, 32 | "column": 1 33 | }, 34 | "end": { 35 | "line": 1, 36 | "column": 3 37 | } 38 | } 39 | } 40 | ], 41 | "comments": [], 42 | "tokens": [ 43 | { 44 | "type": "Punctuator", 45 | "value": "(", 46 | "range": [ 47 | 0, 48 | 1 49 | ], 50 | "loc": { 51 | "start": { 52 | "line": 1, 53 | "column": 0 54 | }, 55 | "end": { 56 | "line": 1, 57 | "column": 1 58 | } 59 | } 60 | }, 61 | { 62 | "type": "Numeric", 63 | "value": "42", 64 | "range": [ 65 | 1, 66 | 3 67 | ], 68 | "loc": { 69 | "start": { 70 | "line": 1, 71 | "column": 1 72 | }, 73 | "end": { 74 | "line": 1, 75 | "column": 3 76 | } 77 | } 78 | }, 79 | { 80 | "type": "Punctuator", 81 | "value": ")", 82 | "range": [ 83 | 3, 84 | 4 85 | ], 86 | "loc": { 87 | "start": { 88 | "line": 1, 89 | "column": 3 90 | }, 91 | "end": { 92 | "line": 1, 93 | "column": 4 94 | } 95 | } 96 | } 97 | ], 98 | "range": [ 99 | 0, 100 | 4 101 | ], 102 | "loc": { 103 | "start": { 104 | "line": 1, 105 | "column": 0 106 | }, 107 | "end": { 108 | "line": 1, 109 | "column": 4 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /explorer/src/components/AstOptions.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 77 | 78 | 86 | -------------------------------------------------------------------------------- /src/parser/modules/acorn.ts: -------------------------------------------------------------------------------- 1 | import type * as acorn from "acorn"; 2 | import { createRequire } from "module"; 3 | import { 4 | getRequireFromCwd, 5 | getRequireFromLinter, 6 | loadNewest, 7 | requireFromCwd, 8 | requireFromLinter, 9 | } from "./require-utils"; 10 | 11 | let acornCache: typeof acorn | undefined; 12 | /** 13 | * Load `acorn` from the loaded ESLint. 14 | * If the loaded ESLint was not found, just returns `require("acorn")`. 15 | */ 16 | export function getAcorn(): typeof acorn { 17 | if (!acornCache) { 18 | acornCache = loadNewest([ 19 | { 20 | getPkg() { 21 | return requireFromCwd("acorn/package.json"); 22 | }, 23 | get() { 24 | return requireFromCwd("acorn"); 25 | }, 26 | }, 27 | { 28 | getPkg() { 29 | return requireFromEspree("acorn/package.json"); 30 | }, 31 | get() { 32 | return requireFromEspree("acorn"); 33 | }, 34 | }, 35 | { 36 | getPkg() { 37 | // eslint-disable-next-line @typescript-eslint/no-require-imports -- special require 38 | return require("acorn/package.json"); 39 | }, 40 | get() { 41 | // eslint-disable-next-line @typescript-eslint/no-require-imports -- special require 42 | return require("acorn"); 43 | }, 44 | }, 45 | ]); 46 | } 47 | return acornCache!; 48 | } 49 | 50 | /** 51 | * Get module from espree 52 | */ 53 | function requireFromEspree(module: string): T | null { 54 | // Lookup the loaded espree 55 | try { 56 | return createRequire(getEspreePath())(module); 57 | } catch { 58 | // ignore 59 | } 60 | return null; 61 | } 62 | 63 | /** Get espree path */ 64 | function getEspreePath(): string { 65 | return loadNewest([ 66 | { 67 | getPkg() { 68 | return requireFromCwd("espree/package.json"); 69 | }, 70 | get() { 71 | return getRequireFromCwd()!.resolve("espree"); 72 | }, 73 | }, 74 | { 75 | getPkg() { 76 | return requireFromLinter("espree/package.json"); 77 | }, 78 | get() { 79 | return getRequireFromLinter()!.resolve("espree"); 80 | }, 81 | }, 82 | { 83 | getPkg() { 84 | // eslint-disable-next-line @typescript-eslint/no-require-imports -- special require 85 | return require("espree/package.json"); 86 | }, 87 | get() { 88 | return require.resolve("espree"); 89 | }, 90 | }, 91 | ]); 92 | } 93 | -------------------------------------------------------------------------------- /tests/src/parser/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Remove `parent` properties from the given AST. 3 | */ 4 | export function nodeReplacer(key: string, value: any): any { 5 | if (key === "parent") { 6 | return undefined; 7 | } 8 | if (value instanceof RegExp) { 9 | return String(value); 10 | } 11 | if (typeof value === "bigint") { 12 | return null; // Make it null so it can be checked on node8. 13 | // return `${String(value)}n` 14 | } 15 | return normalizeObject(value); 16 | } 17 | 18 | const nodeToKeys: Record = { 19 | Program: ["body", "sourceType", "comments", "tokens"], 20 | JSONProperty: ["key", "value", "kind", "computed", "method", "shorthand"], 21 | JSONLiteral: ["value", "raw"], 22 | JSONUnaryExpression: ["operator", "prefix", "argument"], 23 | JSONTemplateLiteral: ["quasis", "expressions"], 24 | }; 25 | 26 | function normalizeObject(value: any) { 27 | if (!value || typeof value !== "object" || Array.isArray(value)) { 28 | return value; 29 | } 30 | const isNode = 31 | typeof value.type === "string" && 32 | (typeof value.start === "number" || typeof value.range?.[0] === "number"); 33 | if (!isNode) { 34 | return value; 35 | } 36 | 37 | function firsts(k: string, nodeType: string | null) { 38 | const o = [ 39 | "type", 40 | ...((nodeType != null && nodeToKeys[nodeType]) || []), 41 | // scope 42 | "identifier", 43 | "from", 44 | "variables", 45 | "identifiers", 46 | "defs", 47 | "references", 48 | "childScopes", 49 | ].indexOf(k); 50 | 51 | return o === -1 ? Infinity : o; 52 | } 53 | 54 | function lasts(k: string, _nodeType: string | null) { 55 | return [ 56 | // locs 57 | "start", 58 | "end", 59 | "line", 60 | "column", 61 | // 62 | "range", 63 | "loc", 64 | ].indexOf(k); 65 | } 66 | 67 | let entries = Object.entries(value); 68 | if (isNode) { 69 | entries = entries.filter( 70 | ([k]) => k !== "parent" && k !== "start" && k !== "end", 71 | ); 72 | } 73 | const nodeType: string | null = isNode ? value.type : null; 74 | 75 | const newData: Record = {}; 76 | for (const [key, val] of entries.sort(([a], [b]) => { 77 | const c = 78 | firsts(a, nodeType) - firsts(b, nodeType) || 79 | lasts(a, nodeType) - lasts(b, nodeType); 80 | if (c) { 81 | return c; 82 | } 83 | return a < b ? -1 : a > b ? 1 : 0; 84 | })) { 85 | newData[key] = val; 86 | } 87 | return newData; 88 | } 89 | -------------------------------------------------------------------------------- /src/parser/modules/espree.ts: -------------------------------------------------------------------------------- 1 | import { loadNewest, requireFromCwd, requireFromLinter } from "./require-utils"; 2 | import { lte } from "semver"; 3 | 4 | /** 5 | * The interface of ESLint custom parsers. 6 | */ 7 | export interface ESPree { 8 | latestEcmaVersion?: number; 9 | version: string; 10 | } 11 | 12 | let espreeCache: ESPree | null = null; 13 | 14 | /** 15 | * Load `espree` from the loaded ESLint. 16 | * If the loaded ESLint was not found, just returns `require("espree")`. 17 | */ 18 | export function getEspree(): ESPree { 19 | if (!espreeCache) { 20 | espreeCache = loadNewest([ 21 | { 22 | getPkg() { 23 | return requireFromCwd("espree/package.json"); 24 | }, 25 | get() { 26 | return requireFromCwd("espree"); 27 | }, 28 | }, 29 | { 30 | getPkg() { 31 | return requireFromLinter("espree/package.json"); 32 | }, 33 | get() { 34 | return requireFromLinter("espree"); 35 | }, 36 | }, 37 | { 38 | getPkg() { 39 | // eslint-disable-next-line @typescript-eslint/no-require-imports -- special require 40 | return require("espree/package.json"); 41 | }, 42 | get() { 43 | // eslint-disable-next-line @typescript-eslint/no-require-imports -- special require 44 | return require("espree"); 45 | }, 46 | }, 47 | ]); 48 | } 49 | return espreeCache!; 50 | } 51 | 52 | type NewestKind = "cwd" | "linter" | "self"; 53 | 54 | let kindCache: NewestKind | null = null; 55 | 56 | /** 57 | * Get the newest `espree` kind from the loaded ESLint or dependency. 58 | */ 59 | export function getNewestEspreeKind(): NewestKind { 60 | if (kindCache) { 61 | return kindCache; 62 | } 63 | const cwdPkg: { version: string } | null = requireFromCwd( 64 | "espree/package.json", 65 | ); 66 | const linterPkg: { version: string } | null = requireFromLinter( 67 | "espree/package.json", 68 | ); 69 | // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports -- ignore 70 | const self: { version: string } = require("espree/package.json"); 71 | 72 | let target: { kind: NewestKind; version: string } = { 73 | kind: "self", 74 | version: self.version, 75 | }; 76 | if (cwdPkg != null && lte(target.version, cwdPkg.version)) { 77 | target = { kind: "cwd", version: cwdPkg.version }; 78 | } 79 | if (linterPkg != null && lte(target.version, linterPkg.version)) { 80 | target = { kind: "linter", version: linterPkg.version }; 81 | } 82 | return (kindCache = target.kind); 83 | } 84 | -------------------------------------------------------------------------------- /try-json5and6.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 |

18 |     

19 |     

20 |     

21 |     
54 | 


--------------------------------------------------------------------------------
/tests/fixtures/parser/ast/comments-02-output.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "type": "Program",
  3 |   "body": [
  4 |     {
  5 |       "type": "JSONExpressionStatement",
  6 |       "expression": {
  7 |         "type": "JSONObjectExpression",
  8 |         "properties": [],
  9 |         "range": [
 10 |           0,
 11 |           19
 12 |         ],
 13 |         "loc": {
 14 |           "start": {
 15 |             "line": 1,
 16 |             "column": 0
 17 |           },
 18 |           "end": {
 19 |             "line": 3,
 20 |             "column": 1
 21 |           }
 22 |         }
 23 |       },
 24 |       "range": [
 25 |         0,
 26 |         19
 27 |       ],
 28 |       "loc": {
 29 |         "start": {
 30 |           "line": 1,
 31 |           "column": 0
 32 |         },
 33 |         "end": {
 34 |           "line": 3,
 35 |           "column": 1
 36 |         }
 37 |       }
 38 |     }
 39 |   ],
 40 |   "comments": [
 41 |     {
 42 |       "type": "Block",
 43 |       "value": " comment ",
 44 |       "range": [
 45 |         4,
 46 |         17
 47 |       ],
 48 |       "loc": {
 49 |         "start": {
 50 |           "line": 2,
 51 |           "column": 2
 52 |         },
 53 |         "end": {
 54 |           "line": 2,
 55 |           "column": 15
 56 |         }
 57 |       }
 58 |     },
 59 |     {
 60 |       "type": "Line",
 61 |       "value": " comment",
 62 |       "range": [
 63 |         20,
 64 |         30
 65 |       ],
 66 |       "loc": {
 67 |         "start": {
 68 |           "line": 4,
 69 |           "column": 0
 70 |         },
 71 |         "end": {
 72 |           "line": 4,
 73 |           "column": 10
 74 |         }
 75 |       }
 76 |     }
 77 |   ],
 78 |   "tokens": [
 79 |     {
 80 |       "type": "Punctuator",
 81 |       "value": "{",
 82 |       "range": [
 83 |         0,
 84 |         1
 85 |       ],
 86 |       "loc": {
 87 |         "start": {
 88 |           "line": 1,
 89 |           "column": 0
 90 |         },
 91 |         "end": {
 92 |           "line": 1,
 93 |           "column": 1
 94 |         }
 95 |       }
 96 |     },
 97 |     {
 98 |       "type": "Punctuator",
 99 |       "value": "}",
100 |       "range": [
101 |         18,
102 |         19
103 |       ],
104 |       "loc": {
105 |         "start": {
106 |           "line": 3,
107 |           "column": 0
108 |         },
109 |         "end": {
110 |           "line": 3,
111 |           "column": 1
112 |         }
113 |       }
114 |     }
115 |   ],
116 |   "range": [
117 |     0,
118 |     31
119 |   ],
120 |   "loc": {
121 |     "start": {
122 |       "line": 1,
123 |       "column": 0
124 |     },
125 |     "end": {
126 |       "line": 5,
127 |       "column": 0
128 |     }
129 |   }
130 | }


--------------------------------------------------------------------------------
/src/parser/token-store.ts:
--------------------------------------------------------------------------------
 1 | import type { AST } from "eslint";
 2 | import type { SourceLocation } from "./ast";
 3 | 
 4 | export type MaybeNodeOrToken = {
 5 |   range?: [number, number];
 6 |   loc?: SourceLocation | null;
 7 | };
 8 | 
 9 | // type TokenType = AST.TokenType | "Template"
10 | 
11 | export class TokenStore {
12 |   public readonly tokens: AST.Token[];
13 | 
14 |   public constructor(tokens: AST.Token[]) {
15 |     this.tokens = tokens;
16 |   }
17 | 
18 |   public add(token: AST.Token): void {
19 |     this.tokens.push(token);
20 |   }
21 | 
22 |   private findIndexByOffset(offset: number): number {
23 |     return this.tokens.findIndex(
24 |       (token) => token.range[0] <= offset && offset < token.range[1],
25 |     );
26 |   }
27 | 
28 |   public findTokenByOffset(offset: number): AST.Token | null {
29 |     return this.tokens[this.findIndexByOffset(offset)];
30 |   }
31 | 
32 |   /**
33 |    * Get the first token representing the given node.
34 |    *
35 |    */
36 |   public getFirstToken(nodeOrToken: MaybeNodeOrToken): AST.Token {
37 |     return this.findTokenByOffset(nodeOrToken.range![0])!;
38 |   }
39 | 
40 |   /**
41 |    * Get the last token representing the given node.
42 |    *
43 |    */
44 |   public getLastToken(nodeOrToken: MaybeNodeOrToken): AST.Token {
45 |     return this.findTokenByOffset(nodeOrToken.range![1] - 1)!;
46 |   }
47 | 
48 |   /**
49 |    * Get the first token before the given node or token.
50 |    */
51 |   public getTokenBefore(
52 |     nodeOrToken: MaybeNodeOrToken,
53 |     filter?: (token: AST.Token) => boolean,
54 |   ): AST.Token | null {
55 |     const tokenIndex = this.findIndexByOffset(nodeOrToken.range![0]);
56 | 
57 |     for (let index = tokenIndex - 1; index >= 0; index--) {
58 |       const token = this.tokens[index];
59 |       if (!filter || filter(token)) {
60 |         return token;
61 |       }
62 |     }
63 |     return null;
64 |   }
65 | 
66 |   /**
67 |    * Get the first token after the given node or token.
68 |    */
69 |   public getTokenAfter(
70 |     nodeOrToken: MaybeNodeOrToken,
71 |     filter?: (token: AST.Token) => boolean,
72 |   ): AST.Token | null {
73 |     const tokenIndex = this.findIndexByOffset(nodeOrToken.range![0]);
74 | 
75 |     for (let index = tokenIndex + 1; index < this.tokens.length; index++) {
76 |       const token = this.tokens[index];
77 |       if (!filter || filter(token)) {
78 |         return token;
79 |       }
80 |     }
81 |     return null;
82 |   }
83 | }
84 | 
85 | /**
86 |  * Checks if given token is comma
87 |  */
88 | export function isComma(
89 |   token: AST.Token,
90 | ): token is AST.Token & { type: "Punctuator"; value: "," } {
91 |   return token.type === "Punctuator" && token.value === ",";
92 | }
93 | 


--------------------------------------------------------------------------------
/tests/fixtures/parser/ast/comments-01-output.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "type": "Program",
  3 |   "body": [
  4 |     {
  5 |       "type": "JSONExpressionStatement",
  6 |       "expression": {
  7 |         "type": "JSONObjectExpression",
  8 |         "properties": [],
  9 |         "range": [
 10 |           11,
 11 |           30
 12 |         ],
 13 |         "loc": {
 14 |           "start": {
 15 |             "line": 2,
 16 |             "column": 0
 17 |           },
 18 |           "end": {
 19 |             "line": 4,
 20 |             "column": 1
 21 |           }
 22 |         }
 23 |       },
 24 |       "range": [
 25 |         11,
 26 |         30
 27 |       ],
 28 |       "loc": {
 29 |         "start": {
 30 |           "line": 2,
 31 |           "column": 0
 32 |         },
 33 |         "end": {
 34 |           "line": 4,
 35 |           "column": 1
 36 |         }
 37 |       }
 38 |     }
 39 |   ],
 40 |   "comments": [
 41 |     {
 42 |       "type": "Line",
 43 |       "value": " comment",
 44 |       "range": [
 45 |         0,
 46 |         10
 47 |       ],
 48 |       "loc": {
 49 |         "start": {
 50 |           "line": 1,
 51 |           "column": 0
 52 |         },
 53 |         "end": {
 54 |           "line": 1,
 55 |           "column": 10
 56 |         }
 57 |       }
 58 |     },
 59 |     {
 60 |       "type": "Block",
 61 |       "value": " comment ",
 62 |       "range": [
 63 |         15,
 64 |         28
 65 |       ],
 66 |       "loc": {
 67 |         "start": {
 68 |           "line": 3,
 69 |           "column": 2
 70 |         },
 71 |         "end": {
 72 |           "line": 3,
 73 |           "column": 15
 74 |         }
 75 |       }
 76 |     }
 77 |   ],
 78 |   "tokens": [
 79 |     {
 80 |       "type": "Punctuator",
 81 |       "value": "{",
 82 |       "range": [
 83 |         11,
 84 |         12
 85 |       ],
 86 |       "loc": {
 87 |         "start": {
 88 |           "line": 2,
 89 |           "column": 0
 90 |         },
 91 |         "end": {
 92 |           "line": 2,
 93 |           "column": 1
 94 |         }
 95 |       }
 96 |     },
 97 |     {
 98 |       "type": "Punctuator",
 99 |       "value": "}",
100 |       "range": [
101 |         29,
102 |         30
103 |       ],
104 |       "loc": {
105 |         "start": {
106 |           "line": 4,
107 |           "column": 0
108 |         },
109 |         "end": {
110 |           "line": 4,
111 |           "column": 1
112 |         }
113 |       }
114 |     }
115 |   ],
116 |   "range": [
117 |     0,
118 |     30
119 |   ],
120 |   "loc": {
121 |     "start": {
122 |       "line": 1,
123 |       "column": 0
124 |     },
125 |     "end": {
126 |       "line": 4,
127 |       "column": 1
128 |     }
129 |   }
130 | }


--------------------------------------------------------------------------------
/tests/src/parser/parser.ts:
--------------------------------------------------------------------------------
 1 | /* eslint @typescript-eslint/no-require-imports:0, @typescript-eslint/no-var-requires:0 -- for test */
 2 | /* globals process, require -- for test */
 3 | import assert from "assert";
 4 | import path from "path";
 5 | import fs from "fs";
 6 | import semver from "semver";
 7 | 
 8 | import { getStaticJSONValue, parseJSON } from "../../../src/index";
 9 | import { nodeReplacer } from "./utils";
10 | 
11 | const FIXTURE_ROOT = path.resolve(__dirname, "../../fixtures/parser/ast");
12 | 
13 | function parse(code: string, fileName: string) {
14 |   const ext = path.extname(fileName);
15 |   return parseJSON(code, {
16 |     ecmaVersion: 2021,
17 |     jsonSyntax:
18 |       ext === ".json"
19 |         ? "JSON"
20 |         : ext === ".jsonc"
21 |           ? "JSONC"
22 |           : ext === ".json5"
23 |             ? "JSON5"
24 |             : undefined,
25 |   });
26 | }
27 | 
28 | describe("Check for AST.", () => {
29 |   for (const filename of fs
30 |     .readdirSync(FIXTURE_ROOT)
31 |     .filter(
32 |       (f) =>
33 |         f.endsWith("input.json5") ||
34 |         f.endsWith("input.json6") ||
35 |         f.endsWith("input.jsonx") ||
36 |         f.endsWith("input.jsonc") ||
37 |         f.endsWith("input.json"),
38 |     )) {
39 |     const inputFileName = path.join(FIXTURE_ROOT, filename);
40 |     const outputFileName = inputFileName.replace(
41 |       /input\.json[56cx]?$/u,
42 |       "output.json",
43 |     );
44 | 
45 |     const requirementsPath = inputFileName.replace(
46 |       /input\.json[56cx]?$/u,
47 |       "requirements.json",
48 |     );
49 |     const requirements = fs.existsSync(requirementsPath)
50 |       ? JSON.parse(fs.readFileSync(requirementsPath, "utf8"))
51 |       : {};
52 |     if (
53 |       Object.entries(requirements).some(([pkgName, pkgVersion]) => {
54 |         const version =
55 |           pkgName === "node"
56 |             ? process.version
57 |             : require(`${pkgName}/package.json`).version;
58 |         return !semver.satisfies(version, pkgVersion as string);
59 |       })
60 |     ) {
61 |       continue;
62 |     }
63 |     it(`AST:${filename}`, () => {
64 |       const input = fs.readFileSync(inputFileName, "utf8");
65 |       const ast = parse(input, inputFileName);
66 |       const astJson = JSON.stringify(ast, nodeReplacer, 2);
67 |       const output = fs.readFileSync(outputFileName, "utf8");
68 |       assert.strictEqual(astJson, output);
69 | 
70 |       assert.strictEqual(ast.range[1] - ast.range[0], input.length);
71 |     });
72 |     it(`Static Value:${filename}`, () => {
73 |       const input = fs.readFileSync(inputFileName, "utf8");
74 |       const ast = parse(input, inputFileName);
75 |       const value = getStaticJSONValue(ast);
76 | 
77 |       assert.deepStrictEqual(
78 |         value,
79 |         new Function(`return (${input.trim()}\n)`)(),
80 |       );
81 |     });
82 |   }
83 | });
84 | 


--------------------------------------------------------------------------------
/tests/fixtures/parser/ast/comments-03-output.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "type": "Program",
  3 |   "body": [
  4 |     {
  5 |       "type": "JSONExpressionStatement",
  6 |       "expression": {
  7 |         "type": "JSONObjectExpression",
  8 |         "properties": [],
  9 |         "range": [
 10 |           0,
 11 |           19
 12 |         ],
 13 |         "loc": {
 14 |           "start": {
 15 |             "line": 1,
 16 |             "column": 0
 17 |           },
 18 |           "end": {
 19 |             "line": 3,
 20 |             "column": 1
 21 |           }
 22 |         }
 23 |       },
 24 |       "range": [
 25 |         0,
 26 |         19
 27 |       ],
 28 |       "loc": {
 29 |         "start": {
 30 |           "line": 1,
 31 |           "column": 0
 32 |         },
 33 |         "end": {
 34 |           "line": 3,
 35 |           "column": 1
 36 |         }
 37 |       }
 38 |     }
 39 |   ],
 40 |   "comments": [
 41 |     {
 42 |       "type": "Block",
 43 |       "value": " comment ",
 44 |       "range": [
 45 |         4,
 46 |         17
 47 |       ],
 48 |       "loc": {
 49 |         "start": {
 50 |           "line": 2,
 51 |           "column": 2
 52 |         },
 53 |         "end": {
 54 |           "line": 2,
 55 |           "column": 15
 56 |         }
 57 |       }
 58 |     },
 59 |     {
 60 |       "type": "Line",
 61 |       "value": " comment",
 62 |       "range": [
 63 |         20,
 64 |         30
 65 |       ],
 66 |       "loc": {
 67 |         "start": {
 68 |           "line": 4,
 69 |           "column": 0
 70 |         },
 71 |         "end": {
 72 |           "line": 4,
 73 |           "column": 10
 74 |         }
 75 |       }
 76 |     },
 77 |     {
 78 |       "type": "Block",
 79 |       "value": " comment ",
 80 |       "range": [
 81 |         33,
 82 |         46
 83 |       ],
 84 |       "loc": {
 85 |         "start": {
 86 |           "line": 5,
 87 |           "column": 2
 88 |         },
 89 |         "end": {
 90 |           "line": 5,
 91 |           "column": 15
 92 |         }
 93 |       }
 94 |     }
 95 |   ],
 96 |   "tokens": [
 97 |     {
 98 |       "type": "Punctuator",
 99 |       "value": "{",
100 |       "range": [
101 |         0,
102 |         1
103 |       ],
104 |       "loc": {
105 |         "start": {
106 |           "line": 1,
107 |           "column": 0
108 |         },
109 |         "end": {
110 |           "line": 1,
111 |           "column": 1
112 |         }
113 |       }
114 |     },
115 |     {
116 |       "type": "Punctuator",
117 |       "value": "}",
118 |       "range": [
119 |         18,
120 |         19
121 |       ],
122 |       "loc": {
123 |         "start": {
124 |           "line": 3,
125 |           "column": 0
126 |         },
127 |         "end": {
128 |           "line": 3,
129 |           "column": 1
130 |         }
131 |       }
132 |     }
133 |   ],
134 |   "range": [
135 |     0,
136 |     47
137 |   ],
138 |   "loc": {
139 |     "start": {
140 |       "line": 1,
141 |       "column": 0
142 |     },
143 |     "end": {
144 |       "line": 6,
145 |       "column": 0
146 |     }
147 |   }
148 | }


--------------------------------------------------------------------------------
/tests/fixtures/parser/ast/array-empty-slots-01-output.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "type": "Program",
  3 |   "body": [
  4 |     {
  5 |       "type": "JSONExpressionStatement",
  6 |       "expression": {
  7 |         "type": "JSONArrayExpression",
  8 |         "elements": [
  9 |           null,
 10 |           {
 11 |             "type": "JSONLiteral",
 12 |             "value": "a",
 13 |             "raw": "'a'",
 14 |             "range": [
 15 |               2,
 16 |               5
 17 |             ],
 18 |             "loc": {
 19 |               "start": {
 20 |                 "line": 1,
 21 |                 "column": 2
 22 |               },
 23 |               "end": {
 24 |                 "line": 1,
 25 |                 "column": 5
 26 |               }
 27 |             }
 28 |           }
 29 |         ],
 30 |         "range": [
 31 |           0,
 32 |           6
 33 |         ],
 34 |         "loc": {
 35 |           "start": {
 36 |             "line": 1,
 37 |             "column": 0
 38 |           },
 39 |           "end": {
 40 |             "line": 1,
 41 |             "column": 6
 42 |           }
 43 |         }
 44 |       },
 45 |       "range": [
 46 |         0,
 47 |         6
 48 |       ],
 49 |       "loc": {
 50 |         "start": {
 51 |           "line": 1,
 52 |           "column": 0
 53 |         },
 54 |         "end": {
 55 |           "line": 1,
 56 |           "column": 6
 57 |         }
 58 |       }
 59 |     }
 60 |   ],
 61 |   "comments": [],
 62 |   "tokens": [
 63 |     {
 64 |       "type": "Punctuator",
 65 |       "value": "[",
 66 |       "range": [
 67 |         0,
 68 |         1
 69 |       ],
 70 |       "loc": {
 71 |         "start": {
 72 |           "line": 1,
 73 |           "column": 0
 74 |         },
 75 |         "end": {
 76 |           "line": 1,
 77 |           "column": 1
 78 |         }
 79 |       }
 80 |     },
 81 |     {
 82 |       "type": "Punctuator",
 83 |       "value": ",",
 84 |       "range": [
 85 |         1,
 86 |         2
 87 |       ],
 88 |       "loc": {
 89 |         "start": {
 90 |           "line": 1,
 91 |           "column": 1
 92 |         },
 93 |         "end": {
 94 |           "line": 1,
 95 |           "column": 2
 96 |         }
 97 |       }
 98 |     },
 99 |     {
100 |       "type": "String",
101 |       "value": "'a'",
102 |       "range": [
103 |         2,
104 |         5
105 |       ],
106 |       "loc": {
107 |         "start": {
108 |           "line": 1,
109 |           "column": 2
110 |         },
111 |         "end": {
112 |           "line": 1,
113 |           "column": 5
114 |         }
115 |       }
116 |     },
117 |     {
118 |       "type": "Punctuator",
119 |       "value": "]",
120 |       "range": [
121 |         5,
122 |         6
123 |       ],
124 |       "loc": {
125 |         "start": {
126 |           "line": 1,
127 |           "column": 5
128 |         },
129 |         "end": {
130 |           "line": 1,
131 |           "column": 6
132 |         }
133 |       }
134 |     }
135 |   ],
136 |   "range": [
137 |     0,
138 |     6
139 |   ],
140 |   "loc": {
141 |     "start": {
142 |       "line": 1,
143 |       "column": 0
144 |     },
145 |     "end": {
146 |       "line": 1,
147 |       "column": 6
148 |     }
149 |   }
150 | }


--------------------------------------------------------------------------------
/tests/fixtures/parser/ast/binary-02-output.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "type": "Program",
  3 |   "body": [
  4 |     {
  5 |       "type": "JSONExpressionStatement",
  6 |       "expression": {
  7 |         "type": "JSONBinaryExpression",
  8 |         "left": {
  9 |           "type": "JSONLiteral",
 10 |           "value": 42,
 11 |           "raw": "42",
 12 |           "range": [
 13 |             0,
 14 |             2
 15 |           ],
 16 |           "loc": {
 17 |             "start": {
 18 |               "line": 1,
 19 |               "column": 0
 20 |             },
 21 |             "end": {
 22 |               "line": 1,
 23 |               "column": 2
 24 |             }
 25 |           }
 26 |         },
 27 |         "operator": "+",
 28 |         "right": {
 29 |           "type": "JSONLiteral",
 30 |           "value": 1,
 31 |           "raw": "1",
 32 |           "range": [
 33 |             5,
 34 |             6
 35 |           ],
 36 |           "loc": {
 37 |             "start": {
 38 |               "line": 1,
 39 |               "column": 5
 40 |             },
 41 |             "end": {
 42 |               "line": 1,
 43 |               "column": 6
 44 |             }
 45 |           }
 46 |         },
 47 |         "range": [
 48 |           0,
 49 |           6
 50 |         ],
 51 |         "loc": {
 52 |           "start": {
 53 |             "line": 1,
 54 |             "column": 0
 55 |           },
 56 |           "end": {
 57 |             "line": 1,
 58 |             "column": 6
 59 |           }
 60 |         }
 61 |       },
 62 |       "range": [
 63 |         0,
 64 |         6
 65 |       ],
 66 |       "loc": {
 67 |         "start": {
 68 |           "line": 1,
 69 |           "column": 0
 70 |         },
 71 |         "end": {
 72 |           "line": 1,
 73 |           "column": 6
 74 |         }
 75 |       }
 76 |     }
 77 |   ],
 78 |   "comments": [],
 79 |   "tokens": [
 80 |     {
 81 |       "type": "Numeric",
 82 |       "value": "42",
 83 |       "range": [
 84 |         0,
 85 |         2
 86 |       ],
 87 |       "loc": {
 88 |         "start": {
 89 |           "line": 1,
 90 |           "column": 0
 91 |         },
 92 |         "end": {
 93 |           "line": 1,
 94 |           "column": 2
 95 |         }
 96 |       }
 97 |     },
 98 |     {
 99 |       "type": "Punctuator",
100 |       "value": "+",
101 |       "range": [
102 |         3,
103 |         4
104 |       ],
105 |       "loc": {
106 |         "start": {
107 |           "line": 1,
108 |           "column": 3
109 |         },
110 |         "end": {
111 |           "line": 1,
112 |           "column": 4
113 |         }
114 |       }
115 |     },
116 |     {
117 |       "type": "Numeric",
118 |       "value": "1",
119 |       "range": [
120 |         5,
121 |         6
122 |       ],
123 |       "loc": {
124 |         "start": {
125 |           "line": 1,
126 |           "column": 5
127 |         },
128 |         "end": {
129 |           "line": 1,
130 |           "column": 6
131 |         }
132 |       }
133 |     }
134 |   ],
135 |   "range": [
136 |     0,
137 |     6
138 |   ],
139 |   "loc": {
140 |     "start": {
141 |       "line": 1,
142 |       "column": 0
143 |     },
144 |     "end": {
145 |       "line": 1,
146 |       "column": 6
147 |     }
148 |   }
149 | }


--------------------------------------------------------------------------------
/src/parser/modules/require-utils.ts:
--------------------------------------------------------------------------------
  1 | import path from "path";
  2 | import { lte } from "semver";
  3 | import type ModuleClass from "module";
  4 | 
  5 | /**
  6 |  * createRequire
  7 |  */
  8 | export function createRequire(
  9 |   filename: string,
 10 | ): ReturnType {
 11 |   // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports, @typescript-eslint/naming-convention -- special require
 12 |   const Module = require("module");
 13 |   const fn: (
 14 |     fileName: string,
 15 |   ) => // eslint-disable-next-line @typescript-eslint/no-explicit-any -- any
 16 |   any =
 17 |     // Added in v12.2.0
 18 |     Module.createRequire ||
 19 |     // Added in v10.12.0, but deprecated in v12.2.0.
 20 |     Module.createRequireFromPath ||
 21 |     // Polyfill - This is not executed on the tests on node@>=10.
 22 |     /* istanbul ignore next */
 23 |     ((filename2: string) => {
 24 |       const mod = new Module(filename2);
 25 | 
 26 |       mod.filename = filename2;
 27 |       mod.paths = Module._nodeModulePaths(path.dirname(filename2));
 28 |       mod._compile("module.exports = require;", filename2);
 29 |       return mod.exports;
 30 |     });
 31 |   return fn(filename);
 32 | }
 33 | 
 34 | /**
 35 |  * Checks if the given string is a linter path.
 36 |  */
 37 | function isLinterPath(p: string) {
 38 |   return (
 39 |     p.includes(`eslint${path.sep}lib${path.sep}linter${path.sep}linter.js`) ||
 40 |     p.includes(`eslint${path.sep}lib${path.sep}linter.js`)
 41 |   );
 42 | }
 43 | 
 44 | /**
 45 |  * Get NodeRequire from Linter
 46 |  */
 47 | export function getRequireFromLinter(): NodeRequire | null {
 48 |   // Lookup the loaded eslint
 49 |   const linterPath = Object.keys(require.cache).find(isLinterPath);
 50 |   if (linterPath) {
 51 |     try {
 52 |       return createRequire(linterPath);
 53 |     } catch {
 54 |       // ignore
 55 |     }
 56 |   }
 57 |   return null;
 58 | }
 59 | 
 60 | /**
 61 |  * Get NodeRequire from Cwd
 62 |  */
 63 | export function getRequireFromCwd(): NodeRequire | null {
 64 |   try {
 65 |     const cwd = process.cwd();
 66 |     const relativeTo = path.join(cwd, "__placeholder__.js");
 67 |     return createRequire(relativeTo);
 68 |   } catch {
 69 |     // ignore
 70 |   }
 71 |   return null;
 72 | }
 73 | 
 74 | /**
 75 |  * Get module from Linter
 76 |  */
 77 | export function requireFromLinter(module: string): T | null {
 78 |   try {
 79 |     return getRequireFromLinter()?.(module);
 80 |   } catch {
 81 |     // ignore
 82 |   }
 83 |   return null;
 84 | }
 85 | 
 86 | /**
 87 |  * Get module from Cwd
 88 |  */
 89 | export function requireFromCwd(module: string): T | null {
 90 |   try {
 91 |     return getRequireFromCwd()?.(module);
 92 |   } catch {
 93 |     // ignore
 94 |   }
 95 |   return null;
 96 | }
 97 | 
 98 | /**
 99 |  * Get the newest `espree` kind from the loaded ESLint or dependency.
100 |  */
101 | export function loadNewest(
102 |   items: { getPkg: () => { version: string } | null; get: () => T | null }[],
103 | ): T {
104 |   let target: { version: string; get: () => T | null } | null = null;
105 |   for (const item of items) {
106 |     const pkg = item.getPkg();
107 |     if (pkg != null && (!target || lte(target.version, pkg.version))) {
108 |       target = { version: pkg.version, get: item.get };
109 |     }
110 |   }
111 |   return target!.get()!;
112 | }
113 | 


--------------------------------------------------------------------------------
/tests/fixtures/parser/ast/template-03-output.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "type": "Program",
  3 |   "body": [
  4 |     {
  5 |       "type": "JSONExpressionStatement",
  6 |       "expression": {
  7 |         "type": "JSONArrayExpression",
  8 |         "elements": [
  9 |           {
 10 |             "type": "JSONTemplateLiteral",
 11 |             "quasis": [
 12 |               {
 13 |                 "type": "JSONTemplateElement",
 14 |                 "tail": true,
 15 |                 "value": {
 16 |                   "raw": "abc",
 17 |                   "cooked": "abc"
 18 |                 },
 19 |                 "range": [
 20 |                   1,
 21 |                   6
 22 |                 ],
 23 |                 "loc": {
 24 |                   "start": {
 25 |                     "line": 1,
 26 |                     "column": 1
 27 |                   },
 28 |                   "end": {
 29 |                     "line": 1,
 30 |                     "column": 6
 31 |                   }
 32 |                 }
 33 |               }
 34 |             ],
 35 |             "expressions": [],
 36 |             "range": [
 37 |               1,
 38 |               6
 39 |             ],
 40 |             "loc": {
 41 |               "start": {
 42 |                 "line": 1,
 43 |                 "column": 1
 44 |               },
 45 |               "end": {
 46 |                 "line": 1,
 47 |                 "column": 6
 48 |               }
 49 |             }
 50 |           }
 51 |         ],
 52 |         "range": [
 53 |           0,
 54 |           7
 55 |         ],
 56 |         "loc": {
 57 |           "start": {
 58 |             "line": 1,
 59 |             "column": 0
 60 |           },
 61 |           "end": {
 62 |             "line": 1,
 63 |             "column": 7
 64 |           }
 65 |         }
 66 |       },
 67 |       "range": [
 68 |         0,
 69 |         7
 70 |       ],
 71 |       "loc": {
 72 |         "start": {
 73 |           "line": 1,
 74 |           "column": 0
 75 |         },
 76 |         "end": {
 77 |           "line": 1,
 78 |           "column": 7
 79 |         }
 80 |       }
 81 |     }
 82 |   ],
 83 |   "comments": [],
 84 |   "tokens": [
 85 |     {
 86 |       "type": "Punctuator",
 87 |       "value": "[",
 88 |       "range": [
 89 |         0,
 90 |         1
 91 |       ],
 92 |       "loc": {
 93 |         "start": {
 94 |           "line": 1,
 95 |           "column": 0
 96 |         },
 97 |         "end": {
 98 |           "line": 1,
 99 |           "column": 1
100 |         }
101 |       }
102 |     },
103 |     {
104 |       "type": "Template",
105 |       "value": "`abc`",
106 |       "range": [
107 |         1,
108 |         6
109 |       ],
110 |       "loc": {
111 |         "start": {
112 |           "line": 1,
113 |           "column": 1
114 |         },
115 |         "end": {
116 |           "line": 1,
117 |           "column": 6
118 |         }
119 |       }
120 |     },
121 |     {
122 |       "type": "Punctuator",
123 |       "value": "]",
124 |       "range": [
125 |         6,
126 |         7
127 |       ],
128 |       "loc": {
129 |         "start": {
130 |           "line": 1,
131 |           "column": 6
132 |         },
133 |         "end": {
134 |           "line": 1,
135 |           "column": 7
136 |         }
137 |       }
138 |     }
139 |   ],
140 |   "range": [
141 |     0,
142 |     7
143 |   ],
144 |   "loc": {
145 |     "start": {
146 |       "line": 1,
147 |       "column": 0
148 |     },
149 |     "end": {
150 |       "line": 1,
151 |       "column": 7
152 |     }
153 |   }
154 | }


--------------------------------------------------------------------------------
/explorer/src/components/MonacoEditor.vue:
--------------------------------------------------------------------------------
  1 | 
  4 | 
  5 | 
104 | 
110 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "jsonc-eslint-parser",
 3 |   "version": "2.4.2",
 4 |   "description": "JSON, JSONC and JSON5 parser for use with ESLint plugins",
 5 |   "main": "lib/index.js",
 6 |   "files": [
 7 |     "lib"
 8 |   ],
 9 |   "engines": {
10 |     "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
11 |   },
12 |   "scripts": {
13 |     "build": "npm run build:meta && npm run build:tsc",
14 |     "build:meta": "ts-node --transpile-only ./tools/update-meta.ts",
15 |     "build:tsc": "tsc --project ./tsconfig.build.json",
16 |     "clean": "rimraf .nyc_output lib coverage",
17 |     "lint": "eslint .",
18 |     "eslint-fix": "eslint . --fix",
19 |     "test:base": "mocha --require ts-node/register \"tests/src/**/*.ts\" --reporter dot --timeout 60000",
20 |     "test": "npm run test:base",
21 |     "test:nyc": "nyc --reporter=lcov npm run test:base",
22 |     "test:debug": "mocha --require ts-node/register/transpile-only \"tests/src/**/*.ts\" --reporter dot",
23 |     "update": "ts-node ./tools/update.ts && npm run eslint-fix && npm run test:nyc",
24 |     "preversion": "npm test && npm run update && git add .",
25 |     "version": "npm run eslint-fix && git add .",
26 |     "update-fixtures": "ts-node ./tools/update-fixtures.ts",
27 |     "benchmark": "ts-node --transpile-only benchmark/index.ts",
28 |     "prerelease": "npm run clean && npm run build",
29 |     "release": "changeset publish",
30 |     "version:ci": "env-cmd -e version-ci -- npm run build:meta && changeset version"
31 |   },
32 |   "repository": {
33 |     "type": "git",
34 |     "url": "git+https://github.com/ota-meshi/jsonc-eslint-parser.git"
35 |   },
36 |   "keywords": [
37 |     "eslint",
38 |     "json",
39 |     "jsonc",
40 |     "json5",
41 |     "parser"
42 |   ],
43 |   "author": "Yosuke Ota",
44 |   "funding": "https://github.com/sponsors/ota-meshi",
45 |   "license": "MIT",
46 |   "bugs": {
47 |     "url": "https://github.com/ota-meshi/jsonc-eslint-parser/issues"
48 |   },
49 |   "homepage": "https://github.com/ota-meshi/jsonc-eslint-parser#readme",
50 |   "devDependencies": {
51 |     "@changesets/changelog-github": "^0.5.0",
52 |     "@changesets/cli": "^2.24.2",
53 |     "@eslint-community/eslint-plugin-eslint-comments": "^4.3.0",
54 |     "@eslint/js": "^9.0.0",
55 |     "@ota-meshi/eslint-plugin": "^0.18.0",
56 |     "@types/benchmark": "^2.1.0",
57 |     "@types/eslint": "^9.0.0",
58 |     "@types/eslint-visitor-keys": "^3.0.0",
59 |     "@types/estree": "^1.0.0",
60 |     "@types/mocha": "^10.0.0",
61 |     "@types/natural-compare": "^1.4.0",
62 |     "@types/node": "^24.0.0",
63 |     "@types/semver": "^7.3.1",
64 |     "@typescript-eslint/eslint-plugin": "^8.0.0",
65 |     "@typescript-eslint/parser": "^8.0.0",
66 |     "benchmark": "^2.1.4",
67 |     "env-cmd": "^11.0.0",
68 |     "eslint": "^9.0.0",
69 |     "eslint-config-prettier": "^10.0.0",
70 |     "eslint-plugin-jsdoc": "^61.4.2",
71 |     "eslint-plugin-json-schema-validator": "^5.0.0",
72 |     "eslint-plugin-jsonc": "^2.0.0",
73 |     "eslint-plugin-n": "^17.0.0",
74 |     "eslint-plugin-node-dependencies": "^1.0.0",
75 |     "eslint-plugin-prettier": "^5.0.0",
76 |     "eslint-plugin-regexp": "^2.0.0",
77 |     "eslint-plugin-vue": "^10.0.0",
78 |     "mocha": "^11.0.0",
79 |     "nyc": "^17.0.0",
80 |     "prettier": "~3.6.0",
81 |     "ts-node": "^10.0.0",
82 |     "typescript": "~5.9.0",
83 |     "typescript-eslint": "^8.0.0",
84 |     "vue-eslint-parser": "^10.0.0"
85 |   },
86 |   "dependencies": {
87 |     "acorn": "^8.5.0",
88 |     "eslint-visitor-keys": "^3.0.0",
89 |     "espree": "^9.0.0",
90 |     "semver": "^7.3.5"
91 |   },
92 |   "publishConfig": {
93 |     "access": "public"
94 |   }
95 | }
96 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
 1 | name: Bug report
 2 | description: Create a report to help us improve.
 3 | 
 4 | body:
 5 |   - type: markdown
 6 |     attributes:
 7 |       value: |
 8 |         Thanks for taking the time to fill out this bug report!
 9 |   - type: checkboxes
10 |     id: sanity-checks
11 |     attributes:
12 |       label: Before You File a Bug Report Please Confirm You Have Done The Following...
13 |       description: If any of these required steps are not taken, we may not be able to review your issue. Help us to help you!
14 |       options:
15 |         - label: I'm using [eslint-plugin-jsonc](https://github.com/ota-meshi/eslint-plugin-jsonc). (`*.json` file linting does not work with the parser alone. You should also use eslint-plugin-jsonc with it.)
16 |         - label: I'm sure the problem is a parser problem. (If you are not sure, search for the issue in [eslint-plugin-jsonc](https://github.com/ota-meshi/eslint-plugin-jsonc) repo and open the issue in [eslint-plugin-jsonc](https://github.com/ota-meshi/eslint-plugin-jsonc) repo if there is no solution.
17 |           required: true
18 |         - label: I have tried restarting my IDE and the issue persists.
19 |           required: true
20 |         - label: I have updated to the latest version of the packages.
21 |           required: true
22 |   - type: input
23 |     id: eslint-version
24 |     attributes:
25 |       label: What version of ESLint are you using?
26 |       placeholder: 0.0.0
27 |     validations:
28 |       required: true
29 |   - type: textarea
30 |     id: eslint-plugin-jsonc-version
31 |     attributes:
32 |       label: What version of `eslint-plugin-jsonc` and `jsonc-eslint-parser` are you using?
33 |       value: |
34 |         - jsonc-eslint-parser@0.0.0
35 |         - eslint-plugin-jsonc@0.0.0 (if you have installed.)
36 |     validations:
37 |       required: true
38 |   - type: textarea
39 |     attributes:
40 |       label: What did you do?
41 |       description: |
42 |         Please include a *minimal* reproduction case.
43 |       value: |
44 |         
45 | Configuration 46 | 47 | ``` 48 | 49 | ``` 50 |
51 | 52 | ```json 53 | 54 | ``` 55 | validations: 56 | required: true 57 | - type: textarea 58 | attributes: 59 | label: What did you expect to happen? 60 | description: | 61 | You can use Markdown in this field. 62 | validations: 63 | required: true 64 | - type: textarea 65 | attributes: 66 | label: What actually happened? 67 | description: | 68 | Please copy-paste the actual ESLint output. You can use Markdown in this field. 69 | validations: 70 | required: true 71 | - type: textarea 72 | id: bug-reproduction 73 | attributes: 74 | label: Link to **GitHub Repo** with Minimal Reproducible Example 75 | description: | 76 | Create 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 closed. 77 | [Why Reproductions are Required](https://antfu.me/posts/why-reproductions-are-required) 78 | 79 | Be sure to share the repo on GitHub. GitHub's repo is ready to debug using Codespace. 80 | Please DON'T USE non-GitHub repos such as GitLab as repro. 81 | It takes me a long time to prepare my local PC for debugging. 82 | Please DON'T USE stackblitz as a repro. 83 | We can't debug eslint with stackblitz. 84 | placeholder: | 85 | https://github.com/[your]/[repo] 86 | validations: 87 | required: true 88 | - type: textarea 89 | attributes: 90 | label: Additional comments 91 | -------------------------------------------------------------------------------- /tools/pkg.pr.new-comment.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Used in `/.github/workflows/pkg.pr.new.yml` 3 | */ 4 | export default async function ({ github, context, output }) { 5 | // eslint-disable-next-line no-console -- For debugging on github actions. 6 | console.log("pkg-pr-new publish output:", JSON.stringify(output)); 7 | 8 | const sha = output.sha; 9 | const commitUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${sha}`; 10 | 11 | const pullRequestNumber = await getPullRequestNumber(); 12 | 13 | const packages = output.packages.map((p) => { 14 | let normalizedUrl = p.url; 15 | if (pullRequestNumber && p.url.endsWith(sha)) { 16 | normalizedUrl = `${p.url.slice(0, -sha.length)}${pullRequestNumber}`; 17 | } 18 | const repoPath = `/${context.repo.owner}/${context.repo.repo}/`; 19 | normalizedUrl = normalizedUrl.replace(repoPath, "/"); 20 | 21 | return { 22 | name: p.name, 23 | url: normalizedUrl, 24 | }; 25 | }); 26 | 27 | const botCommentIdentifier = ""; 28 | 29 | const onlineUrl = new URL( 30 | "https://eslint-online-playground.netlify.app/#eslint-plugin-markdown-preferences", 31 | ); 32 | const overrideDeps = {}; 33 | for (const p of packages) { 34 | overrideDeps[p.name] = p.url; 35 | } 36 | onlineUrl.searchParams.set("overrideDeps", JSON.stringify(overrideDeps)); 37 | const body = `${botCommentIdentifier} 38 | 39 | - Try the Instant Preview in Online Playground 40 | 41 | - [ESLint Online Playground](${onlineUrl}) 42 | 43 | - Install the Instant Preview to Your Local 44 | 45 | \`\`\` 46 | npm i ${packages.map((p) => p.url).join(" ")} 47 | \`\`\` 48 | 49 | - Published Instant Preview Packages: 50 | 51 | ${packages.map((p) => ` - ${p.name}: ${p.url}`).join("\n")} 52 | 53 | [View Commit](${commitUrl})`; 54 | 55 | if (pullRequestNumber) { 56 | await createOrUpdateComment(pullRequestNumber); 57 | } else { 58 | /* eslint-disable no-console -- For debugging on github actions. */ 59 | console.log( 60 | "No open pull request found for this push. Logging publish information to console:", 61 | ); 62 | console.log(`\n${"=".repeat(50)}`); 63 | console.log(body); 64 | console.log(`\n${"=".repeat(50)}`); 65 | /* eslint-enable no-console -- For debugging on github actions. */ 66 | } 67 | 68 | /** 69 | * Get the pull request number from the output. 70 | */ 71 | function getPullRequestNumber() { 72 | return output.number; 73 | } 74 | 75 | /** 76 | * Find the bot comment in the issue. 77 | */ 78 | async function findBotComment(issueNumber) { 79 | if (!issueNumber) return null; 80 | const comments = await github.rest.issues.listComments({ 81 | owner: context.repo.owner, 82 | repo: context.repo.repo, 83 | // eslint-disable-next-line camelcase -- The ID defined in the GitHub API. 84 | issue_number: issueNumber, 85 | }); 86 | return comments.data.find((comment) => 87 | comment.body.includes(botCommentIdentifier), 88 | ); 89 | } 90 | 91 | /** 92 | * Create or update the bot comment in the issue. 93 | */ 94 | async function createOrUpdateComment(issueNumber) { 95 | const existingComment = await findBotComment(issueNumber); 96 | if (existingComment) { 97 | await github.rest.issues.updateComment({ 98 | owner: context.repo.owner, 99 | repo: context.repo.repo, 100 | // eslint-disable-next-line camelcase -- The ID defined in the GitHub API. 101 | comment_id: existingComment.id, 102 | body, 103 | }); 104 | } else { 105 | await github.rest.issues.createComment({ 106 | // eslint-disable-next-line camelcase -- The ID defined in the GitHub API. 107 | issue_number: issueNumber, 108 | owner: context.repo.owner, 109 | repo: context.repo.repo, 110 | body, 111 | }); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/string-01-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Program", 3 | "body": [ 4 | { 5 | "type": "JSONExpressionStatement", 6 | "expression": { 7 | "type": "JSONArrayExpression", 8 | "elements": [ 9 | { 10 | "type": "JSONLiteral", 11 | "value": "abc", 12 | "raw": "\"abc\"", 13 | "range": [ 14 | 1, 15 | 6 16 | ], 17 | "loc": { 18 | "start": { 19 | "line": 1, 20 | "column": 1 21 | }, 22 | "end": { 23 | "line": 1, 24 | "column": 6 25 | } 26 | } 27 | }, 28 | { 29 | "type": "JSONLiteral", 30 | "value": "abd", 31 | "raw": "'abd'", 32 | "range": [ 33 | 8, 34 | 13 35 | ], 36 | "loc": { 37 | "start": { 38 | "line": 1, 39 | "column": 8 40 | }, 41 | "end": { 42 | "line": 1, 43 | "column": 13 44 | } 45 | } 46 | } 47 | ], 48 | "range": [ 49 | 0, 50 | 14 51 | ], 52 | "loc": { 53 | "start": { 54 | "line": 1, 55 | "column": 0 56 | }, 57 | "end": { 58 | "line": 1, 59 | "column": 14 60 | } 61 | } 62 | }, 63 | "range": [ 64 | 0, 65 | 14 66 | ], 67 | "loc": { 68 | "start": { 69 | "line": 1, 70 | "column": 0 71 | }, 72 | "end": { 73 | "line": 1, 74 | "column": 14 75 | } 76 | } 77 | } 78 | ], 79 | "comments": [], 80 | "tokens": [ 81 | { 82 | "type": "Punctuator", 83 | "value": "[", 84 | "range": [ 85 | 0, 86 | 1 87 | ], 88 | "loc": { 89 | "start": { 90 | "line": 1, 91 | "column": 0 92 | }, 93 | "end": { 94 | "line": 1, 95 | "column": 1 96 | } 97 | } 98 | }, 99 | { 100 | "type": "String", 101 | "value": "\"abc\"", 102 | "range": [ 103 | 1, 104 | 6 105 | ], 106 | "loc": { 107 | "start": { 108 | "line": 1, 109 | "column": 1 110 | }, 111 | "end": { 112 | "line": 1, 113 | "column": 6 114 | } 115 | } 116 | }, 117 | { 118 | "type": "Punctuator", 119 | "value": ",", 120 | "range": [ 121 | 6, 122 | 7 123 | ], 124 | "loc": { 125 | "start": { 126 | "line": 1, 127 | "column": 6 128 | }, 129 | "end": { 130 | "line": 1, 131 | "column": 7 132 | } 133 | } 134 | }, 135 | { 136 | "type": "String", 137 | "value": "'abd'", 138 | "range": [ 139 | 8, 140 | 13 141 | ], 142 | "loc": { 143 | "start": { 144 | "line": 1, 145 | "column": 8 146 | }, 147 | "end": { 148 | "line": 1, 149 | "column": 13 150 | } 151 | } 152 | }, 153 | { 154 | "type": "Punctuator", 155 | "value": "]", 156 | "range": [ 157 | 13, 158 | 14 159 | ], 160 | "loc": { 161 | "start": { 162 | "line": 1, 163 | "column": 13 164 | }, 165 | "end": { 166 | "line": 1, 167 | "column": 14 168 | } 169 | } 170 | } 171 | ], 172 | "range": [ 173 | 0, 174 | 14 175 | ], 176 | "loc": { 177 | "start": { 178 | "line": 1, 179 | "column": 0 180 | }, 181 | "end": { 182 | "line": 1, 183 | "column": 14 184 | } 185 | } 186 | } -------------------------------------------------------------------------------- /src/parser/traverse.ts: -------------------------------------------------------------------------------- 1 | import type { VisitorKeys } from "eslint-visitor-keys"; 2 | import type { JSONNode } from "./ast"; 3 | import { getVisitorKeys } from "./visitor-keys"; 4 | 5 | //------------------------------------------------------------------------------ 6 | // Helpers 7 | //------------------------------------------------------------------------------ 8 | 9 | /** 10 | * Check that the given key should be traversed or not. 11 | * @this {Traversable} 12 | * @param key The key to check. 13 | * @returns `true` if the key should be traversed. 14 | */ 15 | function fallbackKeysFilter( 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- any 17 | this: any, 18 | key: string, 19 | ): boolean { 20 | let value = null; 21 | return ( 22 | key !== "comments" && 23 | key !== "leadingComments" && 24 | key !== "loc" && 25 | key !== "parent" && 26 | key !== "range" && 27 | key !== "tokens" && 28 | key !== "trailingComments" && 29 | (value = this[key]) !== null && 30 | typeof value === "object" && 31 | (typeof value.type === "string" || Array.isArray(value)) 32 | ); 33 | } 34 | 35 | /** 36 | * Get the keys of the given node to traverse it. 37 | * @param node The node to get. 38 | * @returns The keys to traverse. 39 | */ 40 | export function getFallbackKeys(node: JSONNode): string[] { 41 | return Object.keys(node).filter(fallbackKeysFilter, node); 42 | } 43 | 44 | /** 45 | * Get the keys of the given node to traverse it. 46 | * @param node The node to get. 47 | * @returns The keys to traverse. 48 | */ 49 | export function getKeys(node: JSONNode, visitorKeys?: VisitorKeys): string[] { 50 | const keys: readonly string[] = 51 | (visitorKeys || getVisitorKeys())[node.type] || getFallbackKeys(node); 52 | 53 | return keys.filter((key) => !getNodes(node, key).next().done); 54 | } 55 | 56 | /** 57 | * Get the nodes of the given node. 58 | * @param node The node to get. 59 | */ 60 | export function* getNodes( 61 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- any 62 | node: any, 63 | key: string, 64 | ): IterableIterator { 65 | const child = node[key]; 66 | if (Array.isArray(child)) { 67 | for (const c of child) { 68 | if (isNode(c)) { 69 | yield c; 70 | } 71 | } 72 | } else if (isNode(child)) { 73 | yield child; 74 | } 75 | } 76 | 77 | /** 78 | * Check whether a given value is a node. 79 | * @param x The value to check. 80 | * @returns `true` if the value is a node. 81 | */ 82 | function isNode( 83 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- any 84 | x: any, 85 | ): x is JSONNode { 86 | return x !== null && typeof x === "object" && typeof x.type === "string"; 87 | } 88 | 89 | /** 90 | * Traverse the given node. 91 | * @param node The node to traverse. 92 | * @param parent The parent node. 93 | * @param visitor The node visitor. 94 | */ 95 | function traverse( 96 | node: JSONNode, 97 | parent: JSONNode | null, 98 | visitor: Visitor, 99 | ): void { 100 | visitor.enterNode(node, parent); 101 | 102 | const keys = getKeys(node, visitor.visitorKeys); 103 | for (const key of keys) { 104 | for (const child of getNodes(node, key)) { 105 | traverse(child, node, visitor); 106 | } 107 | } 108 | 109 | visitor.leaveNode(node, parent); 110 | } 111 | 112 | //------------------------------------------------------------------------------ 113 | // Exports 114 | //------------------------------------------------------------------------------ 115 | 116 | export interface Visitor { 117 | visitorKeys?: VisitorKeys; 118 | enterNode(node: N, parent: N | null): void; 119 | leaveNode(node: N, parent: N | null): void; 120 | } 121 | 122 | export function traverseNodes(node: JSONNode, visitor: Visitor): void; 123 | /** 124 | * Traverse the given AST tree. 125 | * @param node Root node to traverse. 126 | * @param visitor Visitor. 127 | */ 128 | export function traverseNodes( 129 | node: JSONNode, 130 | visitor: Visitor, 131 | ): void { 132 | traverse(node, null, visitor); 133 | } 134 | -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/array-empty-slots-02-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Program", 3 | "body": [ 4 | { 5 | "type": "JSONExpressionStatement", 6 | "expression": { 7 | "type": "JSONArrayExpression", 8 | "elements": [ 9 | { 10 | "type": "JSONLiteral", 11 | "value": "a", 12 | "raw": "'a'", 13 | "range": [ 14 | 1, 15 | 4 16 | ], 17 | "loc": { 18 | "start": { 19 | "line": 1, 20 | "column": 1 21 | }, 22 | "end": { 23 | "line": 1, 24 | "column": 4 25 | } 26 | } 27 | }, 28 | null, 29 | { 30 | "type": "JSONLiteral", 31 | "value": "b", 32 | "raw": "'b'", 33 | "range": [ 34 | 6, 35 | 9 36 | ], 37 | "loc": { 38 | "start": { 39 | "line": 1, 40 | "column": 6 41 | }, 42 | "end": { 43 | "line": 1, 44 | "column": 9 45 | } 46 | } 47 | } 48 | ], 49 | "range": [ 50 | 0, 51 | 10 52 | ], 53 | "loc": { 54 | "start": { 55 | "line": 1, 56 | "column": 0 57 | }, 58 | "end": { 59 | "line": 1, 60 | "column": 10 61 | } 62 | } 63 | }, 64 | "range": [ 65 | 0, 66 | 10 67 | ], 68 | "loc": { 69 | "start": { 70 | "line": 1, 71 | "column": 0 72 | }, 73 | "end": { 74 | "line": 1, 75 | "column": 10 76 | } 77 | } 78 | } 79 | ], 80 | "comments": [], 81 | "tokens": [ 82 | { 83 | "type": "Punctuator", 84 | "value": "[", 85 | "range": [ 86 | 0, 87 | 1 88 | ], 89 | "loc": { 90 | "start": { 91 | "line": 1, 92 | "column": 0 93 | }, 94 | "end": { 95 | "line": 1, 96 | "column": 1 97 | } 98 | } 99 | }, 100 | { 101 | "type": "String", 102 | "value": "'a'", 103 | "range": [ 104 | 1, 105 | 4 106 | ], 107 | "loc": { 108 | "start": { 109 | "line": 1, 110 | "column": 1 111 | }, 112 | "end": { 113 | "line": 1, 114 | "column": 4 115 | } 116 | } 117 | }, 118 | { 119 | "type": "Punctuator", 120 | "value": ",", 121 | "range": [ 122 | 4, 123 | 5 124 | ], 125 | "loc": { 126 | "start": { 127 | "line": 1, 128 | "column": 4 129 | }, 130 | "end": { 131 | "line": 1, 132 | "column": 5 133 | } 134 | } 135 | }, 136 | { 137 | "type": "Punctuator", 138 | "value": ",", 139 | "range": [ 140 | 5, 141 | 6 142 | ], 143 | "loc": { 144 | "start": { 145 | "line": 1, 146 | "column": 5 147 | }, 148 | "end": { 149 | "line": 1, 150 | "column": 6 151 | } 152 | } 153 | }, 154 | { 155 | "type": "String", 156 | "value": "'b'", 157 | "range": [ 158 | 6, 159 | 9 160 | ], 161 | "loc": { 162 | "start": { 163 | "line": 1, 164 | "column": 6 165 | }, 166 | "end": { 167 | "line": 1, 168 | "column": 9 169 | } 170 | } 171 | }, 172 | { 173 | "type": "Punctuator", 174 | "value": "]", 175 | "range": [ 176 | 9, 177 | 10 178 | ], 179 | "loc": { 180 | "start": { 181 | "line": 1, 182 | "column": 9 183 | }, 184 | "end": { 185 | "line": 1, 186 | "column": 10 187 | } 188 | } 189 | } 190 | ], 191 | "range": [ 192 | 0, 193 | 10 194 | ], 195 | "loc": { 196 | "start": { 197 | "line": 1, 198 | "column": 0 199 | }, 200 | "end": { 201 | "line": 1, 202 | "column": 10 203 | } 204 | } 205 | } -------------------------------------------------------------------------------- /tests/src/parser/syntaxes/json.ts: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-require-imports:0, @typescript-eslint/no-var-requires:0 -- for test */ 2 | import assert from "assert"; 3 | import semver from "semver"; 4 | 5 | import { parseForESLint } from "../../../../src/parser/parser"; 6 | import type { ParseError } from "../../../../src/parser/errors"; 7 | 8 | function getParseError(code: string): ParseError { 9 | try { 10 | parseForESLint(code, { 11 | comment: true, 12 | ecmaVersion: 2021, 13 | eslintScopeManager: true, 14 | eslintVisitorKeys: true, 15 | filePath: "test.json", 16 | loc: true, 17 | range: true, 18 | raw: true, 19 | tokens: true, 20 | jsonSyntax: "json", 21 | }); 22 | } catch (e: any) { 23 | return e; 24 | } 25 | return assert.fail("Expected parsing error, but nor error"); 26 | } 27 | 28 | describe("Check that parsing error is correct for JSON.", () => { 29 | for (const { code, message, lineNumber, column, index, char } of [ 30 | { 31 | code: "/**/1", 32 | message: "Unexpected comment.", 33 | lineNumber: 1, 34 | column: 1, 35 | index: 0, 36 | char: "/", 37 | }, 38 | { 39 | code: "[1,]", 40 | message: "Unexpected token ','.", 41 | lineNumber: 1, 42 | column: 3, 43 | index: 2, 44 | char: ",", 45 | }, 46 | { 47 | code: '{"foo": "bar",}', 48 | message: "Unexpected token ','.", 49 | lineNumber: 1, 50 | column: 14, 51 | index: 13, 52 | char: ",", 53 | }, 54 | { 55 | code: '{"foo": "bar"/**/}', 56 | message: "Unexpected comment.", 57 | lineNumber: 1, 58 | column: 14, 59 | index: 13, 60 | char: "/", 61 | }, 62 | ...(semver.satisfies(require("espree/package.json").version, ">=7.2.0") 63 | ? [ 64 | { 65 | code: '{"foo": 1_2_3}', 66 | message: "Unexpected token '_'.", 67 | lineNumber: 1, 68 | column: 10, 69 | index: 9, 70 | char: "_", 71 | }, 72 | ] 73 | : []), 74 | { 75 | code: '{"\\u{31}":"foo"}', 76 | message: "Unexpected unicode codepoint escape.", 77 | lineNumber: 1, 78 | column: 2, 79 | index: 1, 80 | char: '"', 81 | }, 82 | { 83 | code: '{"foo": "\\u{31}"}', 84 | message: "Unexpected unicode codepoint escape.", 85 | lineNumber: 1, 86 | column: 9, 87 | index: 8, 88 | char: '"', 89 | }, 90 | { 91 | code: '{"foo": "\\u{1}"}', 92 | message: "Unexpected unicode codepoint escape.", 93 | lineNumber: 1, 94 | column: 9, 95 | index: 8, 96 | char: '"', 97 | }, 98 | { 99 | code: '{"foo": "___\\u{1}"}', 100 | message: "Unexpected unicode codepoint escape.", 101 | lineNumber: 1, 102 | column: 9, 103 | index: 8, 104 | char: '"', 105 | }, 106 | { 107 | code: '{a\\u{31}:"foo"}', 108 | message: "Unexpected escape sequence.", 109 | lineNumber: 1, 110 | column: 2, 111 | index: 1, 112 | char: "a", 113 | }, 114 | { 115 | code: "0b1", 116 | message: "Unexpected binary numeric literal.", 117 | lineNumber: 1, 118 | column: 1, 119 | index: 0, 120 | char: "0", 121 | }, 122 | { 123 | code: "0o1", 124 | message: "Unexpected octal numeric literal.", 125 | lineNumber: 1, 126 | column: 1, 127 | index: 0, 128 | char: "0", 129 | }, 130 | { 131 | code: "(42)", 132 | message: "Unexpected token '('.", 133 | lineNumber: 1, 134 | column: 1, 135 | index: 0, 136 | char: "(", 137 | }, 138 | { 139 | code: "[('a')]", 140 | message: "Unexpected token '('.", 141 | lineNumber: 1, 142 | column: 2, 143 | index: 1, 144 | char: "(", 145 | }, 146 | { 147 | code: "42+1", 148 | message: "Unexpected binary expression.", 149 | lineNumber: 1, 150 | column: 1, 151 | index: 0, 152 | char: "4", 153 | }, 154 | ]) { 155 | it(`JSON parseForESLint error on ${JSON.stringify(code)}`, () => { 156 | const e = getParseError(code); 157 | assert.deepStrictEqual( 158 | { 159 | message: e.message, 160 | lineNumber: e.lineNumber, 161 | column: e.column, 162 | index: e.index, 163 | char: code[e.index], 164 | }, 165 | { message, lineNumber, column, index, char }, 166 | ); 167 | }); 168 | } 169 | }); 170 | -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/unicode-codepoint-escape02-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Program", 3 | "body": [ 4 | { 5 | "type": "JSONExpressionStatement", 6 | "expression": { 7 | "type": "JSONObjectExpression", 8 | "properties": [ 9 | { 10 | "type": "JSONProperty", 11 | "key": { 12 | "type": "JSONIdentifier", 13 | "name": "a1", 14 | "range": [ 15 | 1, 16 | 8 17 | ], 18 | "loc": { 19 | "start": { 20 | "line": 1, 21 | "column": 1 22 | }, 23 | "end": { 24 | "line": 1, 25 | "column": 8 26 | } 27 | } 28 | }, 29 | "value": { 30 | "type": "JSONLiteral", 31 | "value": "1", 32 | "raw": "\"\\u{31}\"", 33 | "range": [ 34 | 9, 35 | 17 36 | ], 37 | "loc": { 38 | "start": { 39 | "line": 1, 40 | "column": 9 41 | }, 42 | "end": { 43 | "line": 1, 44 | "column": 17 45 | } 46 | } 47 | }, 48 | "kind": "init", 49 | "computed": false, 50 | "method": false, 51 | "shorthand": false, 52 | "range": [ 53 | 1, 54 | 17 55 | ], 56 | "loc": { 57 | "start": { 58 | "line": 1, 59 | "column": 1 60 | }, 61 | "end": { 62 | "line": 1, 63 | "column": 17 64 | } 65 | } 66 | } 67 | ], 68 | "range": [ 69 | 0, 70 | 18 71 | ], 72 | "loc": { 73 | "start": { 74 | "line": 1, 75 | "column": 0 76 | }, 77 | "end": { 78 | "line": 1, 79 | "column": 18 80 | } 81 | } 82 | }, 83 | "range": [ 84 | 0, 85 | 18 86 | ], 87 | "loc": { 88 | "start": { 89 | "line": 1, 90 | "column": 0 91 | }, 92 | "end": { 93 | "line": 1, 94 | "column": 18 95 | } 96 | } 97 | } 98 | ], 99 | "comments": [], 100 | "tokens": [ 101 | { 102 | "type": "Punctuator", 103 | "value": "{", 104 | "range": [ 105 | 0, 106 | 1 107 | ], 108 | "loc": { 109 | "start": { 110 | "line": 1, 111 | "column": 0 112 | }, 113 | "end": { 114 | "line": 1, 115 | "column": 1 116 | } 117 | } 118 | }, 119 | { 120 | "type": "Identifier", 121 | "value": "a1", 122 | "range": [ 123 | 1, 124 | 8 125 | ], 126 | "loc": { 127 | "start": { 128 | "line": 1, 129 | "column": 1 130 | }, 131 | "end": { 132 | "line": 1, 133 | "column": 8 134 | } 135 | } 136 | }, 137 | { 138 | "type": "Punctuator", 139 | "value": ":", 140 | "range": [ 141 | 8, 142 | 9 143 | ], 144 | "loc": { 145 | "start": { 146 | "line": 1, 147 | "column": 8 148 | }, 149 | "end": { 150 | "line": 1, 151 | "column": 9 152 | } 153 | } 154 | }, 155 | { 156 | "type": "String", 157 | "value": "\"\\u{31}\"", 158 | "range": [ 159 | 9, 160 | 17 161 | ], 162 | "loc": { 163 | "start": { 164 | "line": 1, 165 | "column": 9 166 | }, 167 | "end": { 168 | "line": 1, 169 | "column": 17 170 | } 171 | } 172 | }, 173 | { 174 | "type": "Punctuator", 175 | "value": "}", 176 | "range": [ 177 | 17, 178 | 18 179 | ], 180 | "loc": { 181 | "start": { 182 | "line": 1, 183 | "column": 17 184 | }, 185 | "end": { 186 | "line": 1, 187 | "column": 18 188 | } 189 | } 190 | } 191 | ], 192 | "range": [ 193 | 0, 194 | 18 195 | ], 196 | "loc": { 197 | "start": { 198 | "line": 1, 199 | "column": 0 200 | }, 201 | "end": { 202 | "line": 1, 203 | "column": 18 204 | } 205 | } 206 | } -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/object-01-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Program", 3 | "body": [ 4 | { 5 | "type": "JSONExpressionStatement", 6 | "expression": { 7 | "type": "JSONObjectExpression", 8 | "properties": [ 9 | { 10 | "type": "JSONProperty", 11 | "key": { 12 | "type": "JSONLiteral", 13 | "value": "foo", 14 | "raw": "\"foo\"", 15 | "range": [ 16 | 1, 17 | 6 18 | ], 19 | "loc": { 20 | "start": { 21 | "line": 1, 22 | "column": 1 23 | }, 24 | "end": { 25 | "line": 1, 26 | "column": 6 27 | } 28 | } 29 | }, 30 | "value": { 31 | "type": "JSONLiteral", 32 | "value": "bar", 33 | "raw": "\"bar\"", 34 | "range": [ 35 | 7, 36 | 12 37 | ], 38 | "loc": { 39 | "start": { 40 | "line": 1, 41 | "column": 7 42 | }, 43 | "end": { 44 | "line": 1, 45 | "column": 12 46 | } 47 | } 48 | }, 49 | "kind": "init", 50 | "computed": false, 51 | "method": false, 52 | "shorthand": false, 53 | "range": [ 54 | 1, 55 | 12 56 | ], 57 | "loc": { 58 | "start": { 59 | "line": 1, 60 | "column": 1 61 | }, 62 | "end": { 63 | "line": 1, 64 | "column": 12 65 | } 66 | } 67 | } 68 | ], 69 | "range": [ 70 | 0, 71 | 13 72 | ], 73 | "loc": { 74 | "start": { 75 | "line": 1, 76 | "column": 0 77 | }, 78 | "end": { 79 | "line": 1, 80 | "column": 13 81 | } 82 | } 83 | }, 84 | "range": [ 85 | 0, 86 | 13 87 | ], 88 | "loc": { 89 | "start": { 90 | "line": 1, 91 | "column": 0 92 | }, 93 | "end": { 94 | "line": 1, 95 | "column": 13 96 | } 97 | } 98 | } 99 | ], 100 | "comments": [], 101 | "tokens": [ 102 | { 103 | "type": "Punctuator", 104 | "value": "{", 105 | "range": [ 106 | 0, 107 | 1 108 | ], 109 | "loc": { 110 | "start": { 111 | "line": 1, 112 | "column": 0 113 | }, 114 | "end": { 115 | "line": 1, 116 | "column": 1 117 | } 118 | } 119 | }, 120 | { 121 | "type": "String", 122 | "value": "\"foo\"", 123 | "range": [ 124 | 1, 125 | 6 126 | ], 127 | "loc": { 128 | "start": { 129 | "line": 1, 130 | "column": 1 131 | }, 132 | "end": { 133 | "line": 1, 134 | "column": 6 135 | } 136 | } 137 | }, 138 | { 139 | "type": "Punctuator", 140 | "value": ":", 141 | "range": [ 142 | 6, 143 | 7 144 | ], 145 | "loc": { 146 | "start": { 147 | "line": 1, 148 | "column": 6 149 | }, 150 | "end": { 151 | "line": 1, 152 | "column": 7 153 | } 154 | } 155 | }, 156 | { 157 | "type": "String", 158 | "value": "\"bar\"", 159 | "range": [ 160 | 7, 161 | 12 162 | ], 163 | "loc": { 164 | "start": { 165 | "line": 1, 166 | "column": 7 167 | }, 168 | "end": { 169 | "line": 1, 170 | "column": 12 171 | } 172 | } 173 | }, 174 | { 175 | "type": "Punctuator", 176 | "value": "}", 177 | "range": [ 178 | 12, 179 | 13 180 | ], 181 | "loc": { 182 | "start": { 183 | "line": 1, 184 | "column": 12 185 | }, 186 | "end": { 187 | "line": 1, 188 | "column": 13 189 | } 190 | } 191 | } 192 | ], 193 | "range": [ 194 | 0, 195 | 13 196 | ], 197 | "loc": { 198 | "start": { 199 | "line": 1, 200 | "column": 0 201 | }, 202 | "end": { 203 | "line": 1, 204 | "column": 13 205 | } 206 | } 207 | } -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/bingint-01-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Program", 3 | "body": [ 4 | { 5 | "type": "JSONExpressionStatement", 6 | "expression": { 7 | "type": "JSONObjectExpression", 8 | "properties": [ 9 | { 10 | "type": "JSONProperty", 11 | "key": { 12 | "type": "JSONLiteral", 13 | "value": "42", 14 | "raw": "\"42\"", 15 | "range": [ 16 | 1, 17 | 5 18 | ], 19 | "loc": { 20 | "start": { 21 | "line": 1, 22 | "column": 1 23 | }, 24 | "end": { 25 | "line": 1, 26 | "column": 5 27 | } 28 | } 29 | }, 30 | "value": { 31 | "type": "JSONLiteral", 32 | "value": null, 33 | "raw": "42n", 34 | "bigint": "42", 35 | "range": [ 36 | 6, 37 | 9 38 | ], 39 | "loc": { 40 | "start": { 41 | "line": 1, 42 | "column": 6 43 | }, 44 | "end": { 45 | "line": 1, 46 | "column": 9 47 | } 48 | } 49 | }, 50 | "kind": "init", 51 | "computed": false, 52 | "method": false, 53 | "shorthand": false, 54 | "range": [ 55 | 1, 56 | 9 57 | ], 58 | "loc": { 59 | "start": { 60 | "line": 1, 61 | "column": 1 62 | }, 63 | "end": { 64 | "line": 1, 65 | "column": 9 66 | } 67 | } 68 | } 69 | ], 70 | "range": [ 71 | 0, 72 | 10 73 | ], 74 | "loc": { 75 | "start": { 76 | "line": 1, 77 | "column": 0 78 | }, 79 | "end": { 80 | "line": 1, 81 | "column": 10 82 | } 83 | } 84 | }, 85 | "range": [ 86 | 0, 87 | 10 88 | ], 89 | "loc": { 90 | "start": { 91 | "line": 1, 92 | "column": 0 93 | }, 94 | "end": { 95 | "line": 1, 96 | "column": 10 97 | } 98 | } 99 | } 100 | ], 101 | "comments": [], 102 | "tokens": [ 103 | { 104 | "type": "Punctuator", 105 | "value": "{", 106 | "range": [ 107 | 0, 108 | 1 109 | ], 110 | "loc": { 111 | "start": { 112 | "line": 1, 113 | "column": 0 114 | }, 115 | "end": { 116 | "line": 1, 117 | "column": 1 118 | } 119 | } 120 | }, 121 | { 122 | "type": "String", 123 | "value": "\"42\"", 124 | "range": [ 125 | 1, 126 | 5 127 | ], 128 | "loc": { 129 | "start": { 130 | "line": 1, 131 | "column": 1 132 | }, 133 | "end": { 134 | "line": 1, 135 | "column": 5 136 | } 137 | } 138 | }, 139 | { 140 | "type": "Punctuator", 141 | "value": ":", 142 | "range": [ 143 | 5, 144 | 6 145 | ], 146 | "loc": { 147 | "start": { 148 | "line": 1, 149 | "column": 5 150 | }, 151 | "end": { 152 | "line": 1, 153 | "column": 6 154 | } 155 | } 156 | }, 157 | { 158 | "type": "Numeric", 159 | "value": "42n", 160 | "range": [ 161 | 6, 162 | 9 163 | ], 164 | "loc": { 165 | "start": { 166 | "line": 1, 167 | "column": 6 168 | }, 169 | "end": { 170 | "line": 1, 171 | "column": 9 172 | } 173 | } 174 | }, 175 | { 176 | "type": "Punctuator", 177 | "value": "}", 178 | "range": [ 179 | 9, 180 | 10 181 | ], 182 | "loc": { 183 | "start": { 184 | "line": 1, 185 | "column": 9 186 | }, 187 | "end": { 188 | "line": 1, 189 | "column": 10 190 | } 191 | } 192 | } 193 | ], 194 | "range": [ 195 | 0, 196 | 10 197 | ], 198 | "loc": { 199 | "start": { 200 | "line": 1, 201 | "column": 0 202 | }, 203 | "end": { 204 | "line": 1, 205 | "column": 10 206 | } 207 | } 208 | } -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/unicode-codepoint-escape01-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Program", 3 | "body": [ 4 | { 5 | "type": "JSONExpressionStatement", 6 | "expression": { 7 | "type": "JSONObjectExpression", 8 | "properties": [ 9 | { 10 | "type": "JSONProperty", 11 | "key": { 12 | "type": "JSONLiteral", 13 | "value": "1", 14 | "raw": "\"\\u{31}\"", 15 | "range": [ 16 | 1, 17 | 9 18 | ], 19 | "loc": { 20 | "start": { 21 | "line": 1, 22 | "column": 1 23 | }, 24 | "end": { 25 | "line": 1, 26 | "column": 9 27 | } 28 | } 29 | }, 30 | "value": { 31 | "type": "JSONLiteral", 32 | "value": "1", 33 | "raw": "\"\\u{31}\"", 34 | "range": [ 35 | 10, 36 | 18 37 | ], 38 | "loc": { 39 | "start": { 40 | "line": 1, 41 | "column": 10 42 | }, 43 | "end": { 44 | "line": 1, 45 | "column": 18 46 | } 47 | } 48 | }, 49 | "kind": "init", 50 | "computed": false, 51 | "method": false, 52 | "shorthand": false, 53 | "range": [ 54 | 1, 55 | 18 56 | ], 57 | "loc": { 58 | "start": { 59 | "line": 1, 60 | "column": 1 61 | }, 62 | "end": { 63 | "line": 1, 64 | "column": 18 65 | } 66 | } 67 | } 68 | ], 69 | "range": [ 70 | 0, 71 | 19 72 | ], 73 | "loc": { 74 | "start": { 75 | "line": 1, 76 | "column": 0 77 | }, 78 | "end": { 79 | "line": 1, 80 | "column": 19 81 | } 82 | } 83 | }, 84 | "range": [ 85 | 0, 86 | 19 87 | ], 88 | "loc": { 89 | "start": { 90 | "line": 1, 91 | "column": 0 92 | }, 93 | "end": { 94 | "line": 1, 95 | "column": 19 96 | } 97 | } 98 | } 99 | ], 100 | "comments": [], 101 | "tokens": [ 102 | { 103 | "type": "Punctuator", 104 | "value": "{", 105 | "range": [ 106 | 0, 107 | 1 108 | ], 109 | "loc": { 110 | "start": { 111 | "line": 1, 112 | "column": 0 113 | }, 114 | "end": { 115 | "line": 1, 116 | "column": 1 117 | } 118 | } 119 | }, 120 | { 121 | "type": "String", 122 | "value": "\"\\u{31}\"", 123 | "range": [ 124 | 1, 125 | 9 126 | ], 127 | "loc": { 128 | "start": { 129 | "line": 1, 130 | "column": 1 131 | }, 132 | "end": { 133 | "line": 1, 134 | "column": 9 135 | } 136 | } 137 | }, 138 | { 139 | "type": "Punctuator", 140 | "value": ":", 141 | "range": [ 142 | 9, 143 | 10 144 | ], 145 | "loc": { 146 | "start": { 147 | "line": 1, 148 | "column": 9 149 | }, 150 | "end": { 151 | "line": 1, 152 | "column": 10 153 | } 154 | } 155 | }, 156 | { 157 | "type": "String", 158 | "value": "\"\\u{31}\"", 159 | "range": [ 160 | 10, 161 | 18 162 | ], 163 | "loc": { 164 | "start": { 165 | "line": 1, 166 | "column": 10 167 | }, 168 | "end": { 169 | "line": 1, 170 | "column": 18 171 | } 172 | } 173 | }, 174 | { 175 | "type": "Punctuator", 176 | "value": "}", 177 | "range": [ 178 | 18, 179 | 19 180 | ], 181 | "loc": { 182 | "start": { 183 | "line": 1, 184 | "column": 18 185 | }, 186 | "end": { 187 | "line": 1, 188 | "column": 19 189 | } 190 | } 191 | } 192 | ], 193 | "range": [ 194 | 0, 195 | 19 196 | ], 197 | "loc": { 198 | "start": { 199 | "line": 1, 200 | "column": 0 201 | }, 202 | "end": { 203 | "line": 1, 204 | "column": 19 205 | } 206 | } 207 | } -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/unicode-codepoint-escape03-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Program", 3 | "body": [ 4 | { 5 | "type": "JSONExpressionStatement", 6 | "expression": { 7 | "type": "JSONObjectExpression", 8 | "properties": [ 9 | { 10 | "type": "JSONProperty", 11 | "key": { 12 | "type": "JSONLiteral", 13 | "value": "\\u{31}", 14 | "raw": "\"\\\\u{31}\"", 15 | "range": [ 16 | 1, 17 | 10 18 | ], 19 | "loc": { 20 | "start": { 21 | "line": 1, 22 | "column": 1 23 | }, 24 | "end": { 25 | "line": 1, 26 | "column": 10 27 | } 28 | } 29 | }, 30 | "value": { 31 | "type": "JSONLiteral", 32 | "value": "\\u{31}", 33 | "raw": "\"\\\\u{31}\"", 34 | "range": [ 35 | 11, 36 | 20 37 | ], 38 | "loc": { 39 | "start": { 40 | "line": 1, 41 | "column": 11 42 | }, 43 | "end": { 44 | "line": 1, 45 | "column": 20 46 | } 47 | } 48 | }, 49 | "kind": "init", 50 | "computed": false, 51 | "method": false, 52 | "shorthand": false, 53 | "range": [ 54 | 1, 55 | 20 56 | ], 57 | "loc": { 58 | "start": { 59 | "line": 1, 60 | "column": 1 61 | }, 62 | "end": { 63 | "line": 1, 64 | "column": 20 65 | } 66 | } 67 | } 68 | ], 69 | "range": [ 70 | 0, 71 | 21 72 | ], 73 | "loc": { 74 | "start": { 75 | "line": 1, 76 | "column": 0 77 | }, 78 | "end": { 79 | "line": 1, 80 | "column": 21 81 | } 82 | } 83 | }, 84 | "range": [ 85 | 0, 86 | 21 87 | ], 88 | "loc": { 89 | "start": { 90 | "line": 1, 91 | "column": 0 92 | }, 93 | "end": { 94 | "line": 1, 95 | "column": 21 96 | } 97 | } 98 | } 99 | ], 100 | "comments": [], 101 | "tokens": [ 102 | { 103 | "type": "Punctuator", 104 | "value": "{", 105 | "range": [ 106 | 0, 107 | 1 108 | ], 109 | "loc": { 110 | "start": { 111 | "line": 1, 112 | "column": 0 113 | }, 114 | "end": { 115 | "line": 1, 116 | "column": 1 117 | } 118 | } 119 | }, 120 | { 121 | "type": "String", 122 | "value": "\"\\\\u{31}\"", 123 | "range": [ 124 | 1, 125 | 10 126 | ], 127 | "loc": { 128 | "start": { 129 | "line": 1, 130 | "column": 1 131 | }, 132 | "end": { 133 | "line": 1, 134 | "column": 10 135 | } 136 | } 137 | }, 138 | { 139 | "type": "Punctuator", 140 | "value": ":", 141 | "range": [ 142 | 10, 143 | 11 144 | ], 145 | "loc": { 146 | "start": { 147 | "line": 1, 148 | "column": 10 149 | }, 150 | "end": { 151 | "line": 1, 152 | "column": 11 153 | } 154 | } 155 | }, 156 | { 157 | "type": "String", 158 | "value": "\"\\\\u{31}\"", 159 | "range": [ 160 | 11, 161 | 20 162 | ], 163 | "loc": { 164 | "start": { 165 | "line": 1, 166 | "column": 11 167 | }, 168 | "end": { 169 | "line": 1, 170 | "column": 20 171 | } 172 | } 173 | }, 174 | { 175 | "type": "Punctuator", 176 | "value": "}", 177 | "range": [ 178 | 20, 179 | 21 180 | ], 181 | "loc": { 182 | "start": { 183 | "line": 1, 184 | "column": 20 185 | }, 186 | "end": { 187 | "line": 1, 188 | "column": 21 189 | } 190 | } 191 | } 192 | ], 193 | "range": [ 194 | 0, 195 | 21 196 | ], 197 | "loc": { 198 | "start": { 199 | "line": 1, 200 | "column": 0 201 | }, 202 | "end": { 203 | "line": 1, 204 | "column": 21 205 | } 206 | } 207 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsonc-eslint-parser 2 | 3 | [![NPM license](https://img.shields.io/npm/l/jsonc-eslint-parser.svg)](https://www.npmjs.com/package/jsonc-eslint-parser) 4 | [![NPM version](https://img.shields.io/npm/v/jsonc-eslint-parser.svg)](https://www.npmjs.com/package/jsonc-eslint-parser) 5 | [![NPM downloads](https://img.shields.io/npm/dw/jsonc-eslint-parser.svg)](http://www.npmtrends.com/jsonc-eslint-parser) 6 | [![NPM downloads](https://img.shields.io/npm/dm/jsonc-eslint-parser.svg)](http://www.npmtrends.com/jsonc-eslint-parser) 7 | [![Build Status](https://github.com/ota-meshi/jsonc-eslint-parser/workflows/CI/badge.svg?branch=master)](https://github.com/ota-meshi/jsonc-eslint-parser/actions?query=workflow%3ACI) 8 | [![Coverage Status](https://coveralls.io/repos/github/ota-meshi/jsonc-eslint-parser/badge.svg?branch=master)](https://coveralls.io/github/ota-meshi/jsonc-eslint-parser?branch=master) 9 | 10 | ## :name_badge: Introduction 11 | 12 | [JSON], [JSONC] and [JSON5] parser for use with [ESLint] plugins. 13 | 14 | This parser allows us to lint [JSON], [JSONC] and [JSON5] files. 15 | This parser and the rules of [eslint-plugin-jsonc] would catch some of the mistakes and code style violations. 16 | 17 | See [eslint-plugin-jsonc] for details. 18 | 19 | ## :cd: Installation 20 | 21 | ```bash 22 | npm i --save-dev jsonc-eslint-parser 23 | ``` 24 | 25 | ## :book: Usage (Flat Config) 26 | 27 | In your ESLint configuration file, set the `parser` property: 28 | 29 | ```js 30 | import * as jsoncParser from "jsonc-eslint-parser"; 31 | 32 | export default [ 33 | { 34 | // ... 35 | // Add the following settings. 36 | files: ["**/*.json", "**/*.json5"], // Specify the extension or pattern you want to parse as JSON. 37 | languageOptions: { 38 | parser: jsoncParser, // Set this parser. 39 | } 40 | }, 41 | ]; 42 | ``` 43 | 44 | ## :book: Usage (Legacy Config) 45 | 46 | In your ESLint configuration file, set the `overrides` > `parser` property: 47 | 48 | ```json5 49 | { 50 | // ... 51 | // Add the following settings. 52 | "overrides": [ 53 | { 54 | "files": ["*.json", "*.json5"], // Specify the extension or pattern you want to parse as JSON. 55 | "parser": "jsonc-eslint-parser", // Set this parser. 56 | }, 57 | ], 58 | } 59 | ``` 60 | 61 | ## :gear: Configuration 62 | 63 | The following additional configuration options are available by specifying them in [parserOptions](https://eslint.org/docs/latest/use/configure/parser#configure-parser-options) in your ESLint configuration file. 64 | 65 | ```json5 66 | { 67 | // Additional configuration options 68 | "parserOptions": { 69 | "jsonSyntax": "JSON5" 70 | } 71 | } 72 | ``` 73 | 74 | ### `parserOptions.jsonSyntax` 75 | 76 | Set to `"JSON"`, `"JSONC"` or `"JSON5"`. Select the JSON syntax you are using. 77 | If not specified, all syntaxes that express static values ​​are accepted. For example, template literals without interpolation. 78 | 79 | **Note** : Recommended to loosen the syntax checking by the parser and use check rules of [eslint-plugin-jsonc] to automatically fix it. 80 | 81 | ## Usage for Custom Rules / Plugins 82 | 83 | - [AST.md](./docs/AST.md) is AST specification. 84 | - [Plugins.md](./docs/Plugins.md) describes using this in an ESLint plugin. 85 | - [no-template-literals.ts](https://github.com/ota-meshi/eslint-plugin-jsonc/blob/master/lib/rules/no-template-literals.ts) is an example. 86 | - You can see the AST on the [Online DEMO](https://ota-meshi.github.io/jsonc-eslint-parser/). 87 | 88 | ## :traffic_light: Semantic Versioning Policy 89 | 90 | **jsonc-eslint-parser** follows [Semantic Versioning](http://semver.org/). 91 | 92 | ## :couple: Related Packages 93 | 94 | - [eslint-plugin-jsonc](https://github.com/ota-meshi/eslint-plugin-jsonc) ... ESLint plugin for JSON, JSON with comments (JSONC) and JSON5. 95 | - [eslint-plugin-yml](https://github.com/ota-meshi/eslint-plugin-yml) ... ESLint plugin for YAML. 96 | - [eslint-plugin-toml](https://github.com/ota-meshi/eslint-plugin-toml) ... ESLint plugin for TOML. 97 | - [eslint-plugin-json-schema-validator](https://github.com/ota-meshi/eslint-plugin-json-schema-validator) ... ESLint plugin that validates data using JSON Schema Validator. 98 | - [yaml-eslint-parser](https://github.com/ota-meshi/yaml-eslint-parser) ... YAML parser for use with ESLint plugins. 99 | - [toml-eslint-parser](https://github.com/ota-meshi/toml-eslint-parser) ... TOML parser for use with ESLint plugins. 100 | 101 | ## :lock: License 102 | 103 | See the [LICENSE](LICENSE) file for license rights and limitations (MIT). 104 | 105 | [JSON]: https://json.org/ 106 | [JSONC]: https://github.com/microsoft/node-jsonc-parser 107 | [JSON5]: https://json5.org/ 108 | [ESLint]: https://eslint.org/ 109 | [eslint-plugin-jsonc]: https://www.npmjs.com/package/eslint-plugin-jsonc 110 | -------------------------------------------------------------------------------- /tests/src/parser/syntaxes/jsonc.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | 3 | import { parseForESLint } from "../../../../src/parser/parser"; 4 | import type { ParseError } from "../../../../src/parser/errors"; 5 | 6 | function getParseError(code: string): ParseError { 7 | try { 8 | parseForESLint(code, { 9 | comment: true, 10 | ecmaVersion: 2021, 11 | eslintScopeManager: true, 12 | eslintVisitorKeys: true, 13 | filePath: "test.json", 14 | loc: true, 15 | range: true, 16 | raw: true, 17 | tokens: true, 18 | jsonSyntax: "jsonc", 19 | }); 20 | } catch (e: any) { 21 | return e; 22 | } 23 | return assert.fail("Expected parsing error, but nor error"); 24 | } 25 | 26 | describe("Check that parsing error is correct for JSONC.", () => { 27 | for (const { code, message, lineNumber, column, index, char } of [ 28 | { 29 | code: "+1", 30 | message: "Unexpected token '+'.", 31 | lineNumber: 1, 32 | column: 1, 33 | index: 0, 34 | char: "+", 35 | }, 36 | { 37 | code: "- 1", 38 | message: "Unexpected whitespace.", 39 | lineNumber: 1, 40 | column: 2, 41 | index: 1, 42 | char: " ", 43 | }, 44 | { 45 | code: ".1", 46 | message: "Unexpected token '.'.", 47 | lineNumber: 1, 48 | column: 1, 49 | index: 0, 50 | char: ".", 51 | }, 52 | { 53 | code: "1.", 54 | message: "Unexpected token '.'.", 55 | lineNumber: 1, 56 | column: 2, 57 | index: 1, 58 | char: ".", 59 | }, 60 | { 61 | code: "NaN", 62 | message: "Unexpected identifier 'NaN'.", 63 | lineNumber: 1, 64 | column: 1, 65 | index: 0, 66 | char: "N", 67 | }, 68 | { 69 | code: "Infinity", 70 | message: "Unexpected identifier 'Infinity'.", 71 | lineNumber: 1, 72 | column: 1, 73 | index: 0, 74 | char: "I", 75 | }, 76 | { 77 | code: "0x123", 78 | message: "Invalid number 0x123.", 79 | lineNumber: 1, 80 | column: 1, 81 | index: 0, 82 | char: "0", 83 | }, 84 | { 85 | code: '"Line 1 \\\nLine 2"', 86 | message: "Unexpected multiline string.", 87 | lineNumber: 1, 88 | column: 1, 89 | index: 0, 90 | char: '"', 91 | }, 92 | { 93 | code: '{a: "b"}', 94 | message: "Unexpected identifier 'a'.", 95 | lineNumber: 1, 96 | column: 2, 97 | index: 1, 98 | char: "a", 99 | }, 100 | { 101 | code: "{\"a\": 'b'}", 102 | message: "Unexpected single quoted.", 103 | lineNumber: 1, 104 | column: 7, 105 | index: 6, 106 | char: "'", 107 | }, 108 | { 109 | code: '{"\\u{31}":"foo"}', 110 | message: "Unexpected unicode codepoint escape.", 111 | lineNumber: 1, 112 | column: 2, 113 | index: 1, 114 | char: '"', 115 | }, 116 | { 117 | code: '{"foo": "\\u{31}"}', 118 | message: "Unexpected unicode codepoint escape.", 119 | lineNumber: 1, 120 | column: 9, 121 | index: 8, 122 | char: '"', 123 | }, 124 | { 125 | code: '{a\\u{31}:"foo"}', 126 | message: "Unexpected escape sequence.", 127 | lineNumber: 1, 128 | column: 2, 129 | index: 1, 130 | char: "a", 131 | }, 132 | { 133 | code: "0b1", 134 | message: "Unexpected binary numeric literal.", 135 | lineNumber: 1, 136 | column: 1, 137 | index: 0, 138 | char: "0", 139 | }, 140 | { 141 | code: "0o1", 142 | message: "Unexpected octal numeric literal.", 143 | lineNumber: 1, 144 | column: 1, 145 | index: 0, 146 | char: "0", 147 | }, 148 | { 149 | code: "(42)", 150 | message: "Unexpected token '('.", 151 | lineNumber: 1, 152 | column: 1, 153 | index: 0, 154 | char: "(", 155 | }, 156 | { 157 | code: "[('a')]", 158 | message: "Unexpected token '('.", 159 | lineNumber: 1, 160 | column: 2, 161 | index: 1, 162 | char: "(", 163 | }, 164 | { 165 | code: "42+1", 166 | message: "Unexpected binary expression.", 167 | lineNumber: 1, 168 | column: 1, 169 | index: 0, 170 | char: "4", 171 | }, 172 | ]) { 173 | it(`JSONC parseForESLint error on ${JSON.stringify(code)}`, () => { 174 | const e = getParseError(code); 175 | assert.deepStrictEqual( 176 | { 177 | message: e.message, 178 | lineNumber: e.lineNumber, 179 | column: e.column, 180 | index: e.index, 181 | char: code[e.index], 182 | }, 183 | { message, lineNumber, column, index, char }, 184 | ); 185 | }); 186 | } 187 | }); 188 | -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/regexp-01-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Program", 3 | "body": [ 4 | { 5 | "type": "JSONExpressionStatement", 6 | "expression": { 7 | "type": "JSONObjectExpression", 8 | "properties": [ 9 | { 10 | "type": "JSONProperty", 11 | "key": { 12 | "type": "JSONIdentifier", 13 | "name": "a", 14 | "range": [ 15 | 1, 16 | 2 17 | ], 18 | "loc": { 19 | "start": { 20 | "line": 1, 21 | "column": 1 22 | }, 23 | "end": { 24 | "line": 1, 25 | "column": 2 26 | } 27 | } 28 | }, 29 | "value": { 30 | "type": "JSONLiteral", 31 | "value": "/reg/", 32 | "raw": "/reg/", 33 | "regex": { 34 | "pattern": "reg", 35 | "flags": "" 36 | }, 37 | "range": [ 38 | 3, 39 | 8 40 | ], 41 | "loc": { 42 | "start": { 43 | "line": 1, 44 | "column": 3 45 | }, 46 | "end": { 47 | "line": 1, 48 | "column": 8 49 | } 50 | } 51 | }, 52 | "kind": "init", 53 | "computed": false, 54 | "method": false, 55 | "shorthand": false, 56 | "range": [ 57 | 1, 58 | 8 59 | ], 60 | "loc": { 61 | "start": { 62 | "line": 1, 63 | "column": 1 64 | }, 65 | "end": { 66 | "line": 1, 67 | "column": 8 68 | } 69 | } 70 | } 71 | ], 72 | "range": [ 73 | 0, 74 | 9 75 | ], 76 | "loc": { 77 | "start": { 78 | "line": 1, 79 | "column": 0 80 | }, 81 | "end": { 82 | "line": 1, 83 | "column": 9 84 | } 85 | } 86 | }, 87 | "range": [ 88 | 0, 89 | 9 90 | ], 91 | "loc": { 92 | "start": { 93 | "line": 1, 94 | "column": 0 95 | }, 96 | "end": { 97 | "line": 1, 98 | "column": 9 99 | } 100 | } 101 | } 102 | ], 103 | "comments": [], 104 | "tokens": [ 105 | { 106 | "type": "Punctuator", 107 | "value": "{", 108 | "range": [ 109 | 0, 110 | 1 111 | ], 112 | "loc": { 113 | "start": { 114 | "line": 1, 115 | "column": 0 116 | }, 117 | "end": { 118 | "line": 1, 119 | "column": 1 120 | } 121 | } 122 | }, 123 | { 124 | "type": "Identifier", 125 | "value": "a", 126 | "range": [ 127 | 1, 128 | 2 129 | ], 130 | "loc": { 131 | "start": { 132 | "line": 1, 133 | "column": 1 134 | }, 135 | "end": { 136 | "line": 1, 137 | "column": 2 138 | } 139 | } 140 | }, 141 | { 142 | "type": "Punctuator", 143 | "value": ":", 144 | "range": [ 145 | 2, 146 | 3 147 | ], 148 | "loc": { 149 | "start": { 150 | "line": 1, 151 | "column": 2 152 | }, 153 | "end": { 154 | "line": 1, 155 | "column": 3 156 | } 157 | } 158 | }, 159 | { 160 | "type": "RegularExpression", 161 | "regex": { 162 | "flags": "", 163 | "pattern": "reg" 164 | }, 165 | "value": "/reg/", 166 | "range": [ 167 | 3, 168 | 8 169 | ], 170 | "loc": { 171 | "start": { 172 | "line": 1, 173 | "column": 3 174 | }, 175 | "end": { 176 | "line": 1, 177 | "column": 8 178 | } 179 | } 180 | }, 181 | { 182 | "type": "Punctuator", 183 | "value": "}", 184 | "range": [ 185 | 8, 186 | 9 187 | ], 188 | "loc": { 189 | "start": { 190 | "line": 1, 191 | "column": 8 192 | }, 193 | "end": { 194 | "line": 1, 195 | "column": 9 196 | } 197 | } 198 | } 199 | ], 200 | "range": [ 201 | 0, 202 | 9 203 | ], 204 | "loc": { 205 | "start": { 206 | "line": 1, 207 | "column": 0 208 | }, 209 | "end": { 210 | "line": 1, 211 | "column": 9 212 | } 213 | } 214 | } -------------------------------------------------------------------------------- /tests/src/parser/syntaxes/json5.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | 3 | import { parseForESLint } from "../../../../src/parser/parser"; 4 | import type { ParseError } from "../../../../src/parser/errors"; 5 | 6 | function getParseError(code: string): ParseError { 7 | try { 8 | parseForESLint(code, { 9 | comment: true, 10 | ecmaVersion: 2021, 11 | eslintScopeManager: true, 12 | eslintVisitorKeys: true, 13 | filePath: "test.json", 14 | loc: true, 15 | range: true, 16 | raw: true, 17 | tokens: true, 18 | jsonSyntax: "json5", 19 | }); 20 | } catch (e: any) { 21 | return e; 22 | } 23 | return assert.fail("Expected parsing error, but nor error"); 24 | } 25 | 26 | describe("Check that parsing error is correct for JSON5.", () => { 27 | for (const { code, message, lineNumber, column, index, char } of [ 28 | { 29 | code: "{1:2}", 30 | message: "Unexpected number literal.", 31 | lineNumber: 1, 32 | column: 2, 33 | index: 1, 34 | char: "1", 35 | }, 36 | { 37 | code: "undefined", 38 | message: "Unexpected identifier 'undefined'.", 39 | lineNumber: 1, 40 | column: 1, 41 | index: 0, 42 | char: "u", 43 | }, 44 | { 45 | code: "[1,,3]", 46 | message: "Unexpected token ','.", 47 | lineNumber: 1, 48 | column: 3, 49 | index: 2, 50 | char: ",", 51 | }, 52 | { 53 | code: "[,2,3]", 54 | message: "Unexpected token ','.", 55 | lineNumber: 1, 56 | column: 2, 57 | index: 1, 58 | char: ",", 59 | }, 60 | { 61 | code: "[1,,]", 62 | message: "Unexpected token ','.", 63 | lineNumber: 1, 64 | column: 3, 65 | index: 2, 66 | char: ",", 67 | }, 68 | { 69 | code: "/reg/", 70 | message: "Unexpected regex literal.", 71 | lineNumber: 1, 72 | column: 1, 73 | index: 0, 74 | char: "/", 75 | }, 76 | { 77 | code: "`tmp`", 78 | message: "Unexpected template literal.", 79 | lineNumber: 1, 80 | column: 1, 81 | index: 0, 82 | char: "`", 83 | }, 84 | { 85 | code: "1n", 86 | message: "Unexpected bigint literal.", 87 | lineNumber: 1, 88 | column: 1, 89 | index: 0, 90 | char: "1", 91 | }, 92 | { 93 | code: '{"\\u{31}":"foo"}', 94 | message: "Unexpected unicode codepoint escape.", 95 | lineNumber: 1, 96 | column: 2, 97 | index: 1, 98 | char: '"', 99 | }, 100 | { 101 | code: '{"foo": "\\u{31}"}', 102 | message: "Unexpected unicode codepoint escape.", 103 | lineNumber: 1, 104 | column: 9, 105 | index: 8, 106 | char: '"', 107 | }, 108 | { 109 | code: '{a\\u{31}:"foo"}', 110 | message: "Unexpected escape sequence.", 111 | lineNumber: 1, 112 | column: 2, 113 | index: 1, 114 | char: "a", 115 | }, 116 | { 117 | code: '{a\\u0031:"foo"}', 118 | message: "Unexpected escape sequence.", 119 | lineNumber: 1, 120 | column: 2, 121 | index: 1, 122 | char: "a", 123 | }, 124 | { 125 | code: "0b1", 126 | message: "Unexpected binary numeric literal.", 127 | lineNumber: 1, 128 | column: 1, 129 | index: 0, 130 | char: "0", 131 | }, 132 | { 133 | code: "0o1", 134 | message: "Unexpected octal numeric literal.", 135 | lineNumber: 1, 136 | column: 1, 137 | index: 0, 138 | char: "0", 139 | }, 140 | { 141 | code: "012", 142 | message: "Unexpected legacy octal numeric literal.", 143 | lineNumber: 1, 144 | column: 1, 145 | index: 0, 146 | char: "0", 147 | }, 148 | { 149 | code: "09", 150 | message: "Unexpected legacy octal numeric literal.", 151 | lineNumber: 1, 152 | column: 1, 153 | index: 0, 154 | char: "0", 155 | }, 156 | { 157 | code: "(42)", 158 | message: "Unexpected token '('.", 159 | lineNumber: 1, 160 | column: 1, 161 | index: 0, 162 | char: "(", 163 | }, 164 | { 165 | code: "[('a')]", 166 | message: "Unexpected token '('.", 167 | lineNumber: 1, 168 | column: 2, 169 | index: 1, 170 | char: "(", 171 | }, 172 | { 173 | code: "42+1", 174 | message: "Unexpected binary expression.", 175 | lineNumber: 1, 176 | column: 1, 177 | index: 0, 178 | char: "4", 179 | }, 180 | ]) { 181 | it(`JSON5 parseForESLint error on ${JSON.stringify(code)}`, () => { 182 | const e = getParseError(code); 183 | assert.deepStrictEqual( 184 | { 185 | message: e.message, 186 | lineNumber: e.lineNumber, 187 | column: e.column, 188 | index: e.index, 189 | char: code[e.index], 190 | }, 191 | { message, lineNumber, column, index, char }, 192 | ); 193 | }); 194 | } 195 | }); 196 | -------------------------------------------------------------------------------- /src/parser/ast.ts: -------------------------------------------------------------------------------- 1 | import type { AST as ESLintAST } from "eslint"; 2 | import type { Comment } from "estree"; 3 | 4 | export interface Locations { 5 | loc: SourceLocation; 6 | range: [number, number]; 7 | } 8 | 9 | interface BaseJSONNode extends Locations { 10 | type: string; 11 | } 12 | 13 | export interface SourceLocation { 14 | start: Position; 15 | end: Position; 16 | } 17 | 18 | export interface Position { 19 | /** >= 1 */ 20 | line: number; 21 | /** >= 0 */ 22 | column: number; 23 | } 24 | 25 | export type JSONNode = 26 | | JSONProgram 27 | | JSONExpressionStatement 28 | | JSONExpression 29 | | JSONProperty 30 | | JSONIdentifier 31 | | JSONTemplateLiteral 32 | | JSONTemplateElement; 33 | export interface JSONProgram extends BaseJSONNode { 34 | type: "Program"; 35 | body: [JSONExpressionStatement]; 36 | comments: Comment[]; 37 | tokens: ESLintAST.Token[]; 38 | parent: null; 39 | } 40 | 41 | export interface JSONExpressionStatement extends BaseJSONNode { 42 | type: "JSONExpressionStatement"; 43 | expression: JSONExpression; 44 | parent: JSONProgram; 45 | } 46 | 47 | export type JSONExpression = 48 | | JSONArrayExpression 49 | | JSONObjectExpression 50 | | JSONLiteral 51 | | JSONUnaryExpression 52 | | JSONNumberIdentifier 53 | | JSONUndefinedIdentifier 54 | | JSONTemplateLiteral 55 | | JSONBinaryExpression; 56 | 57 | export interface JSONArrayExpression extends BaseJSONNode { 58 | type: "JSONArrayExpression"; 59 | elements: (JSONExpression | null)[]; 60 | parent: JSONArrayExpression | JSONProperty | JSONExpressionStatement; 61 | } 62 | 63 | export interface JSONObjectExpression extends BaseJSONNode { 64 | type: "JSONObjectExpression"; 65 | properties: JSONProperty[]; 66 | parent: JSONArrayExpression | JSONProperty | JSONExpressionStatement; 67 | } 68 | 69 | export interface JSONProperty extends BaseJSONNode { 70 | type: "JSONProperty"; 71 | key: JSONIdentifier | JSONStringLiteral | JSONNumberLiteral; 72 | value: JSONExpression; 73 | kind: "init"; 74 | method: false; 75 | shorthand: false; 76 | computed: false; 77 | parent: JSONObjectExpression; 78 | } 79 | 80 | export interface JSONIdentifier extends BaseJSONNode { 81 | type: "JSONIdentifier"; 82 | name: string; 83 | parent?: 84 | | JSONArrayExpression 85 | | JSONProperty 86 | | JSONExpressionStatement 87 | | JSONUnaryExpression; 88 | } 89 | 90 | export interface JSONNumberIdentifier extends JSONIdentifier { 91 | name: "Infinity" | "NaN"; 92 | } 93 | 94 | export interface JSONUndefinedIdentifier extends JSONIdentifier { 95 | name: "undefined"; 96 | } 97 | interface JSONLiteralBase extends BaseJSONNode { 98 | type: "JSONLiteral"; 99 | raw: string; 100 | parent?: 101 | | JSONArrayExpression 102 | | JSONProperty 103 | | JSONExpressionStatement 104 | | JSONUnaryExpression 105 | | JSONBinaryExpression; 106 | } 107 | 108 | export interface JSONStringLiteral extends JSONLiteralBase { 109 | value: string; 110 | regex: null; 111 | bigint: null; 112 | } 113 | export interface JSONNumberLiteral extends JSONLiteralBase { 114 | value: number; 115 | regex: null; 116 | bigint: null; 117 | } 118 | export interface JSONKeywordLiteral extends JSONLiteralBase { 119 | value: boolean | null; 120 | regex: null; 121 | bigint: null; 122 | } 123 | export interface JSONRegExpLiteral extends JSONLiteralBase { 124 | value: null; 125 | regex: { 126 | pattern: string; 127 | flags: string; 128 | }; 129 | bigint: null; 130 | } 131 | export interface JSONBigIntLiteral extends JSONLiteralBase { 132 | value: null; 133 | regex: null; 134 | bigint: string; 135 | } 136 | 137 | export type JSONLiteral = 138 | | JSONStringLiteral 139 | | JSONNumberLiteral 140 | | JSONKeywordLiteral 141 | | JSONRegExpLiteral 142 | | JSONBigIntLiteral; 143 | 144 | export interface JSONUnaryExpression extends BaseJSONNode { 145 | type: "JSONUnaryExpression"; 146 | operator: "-" | "+"; 147 | prefix: true; 148 | argument: JSONNumberLiteral | JSONNumberIdentifier; 149 | parent: JSONArrayExpression | JSONProperty | JSONExpressionStatement; 150 | } 151 | 152 | export interface JSONTemplateLiteral extends BaseJSONNode { 153 | type: "JSONTemplateLiteral"; 154 | quasis: [JSONTemplateElement]; 155 | expressions: []; 156 | parent: JSONArrayExpression | JSONProperty | JSONExpressionStatement; 157 | } 158 | 159 | export interface JSONTemplateElement extends BaseJSONNode { 160 | type: "JSONTemplateElement"; 161 | tail: boolean; 162 | value: { 163 | cooked: string; 164 | raw: string; 165 | }; 166 | parent: JSONTemplateLiteral; 167 | } 168 | 169 | export interface JSONBinaryExpression extends BaseJSONNode { 170 | type: "JSONBinaryExpression"; 171 | operator: "-" | "+" | "*" | "/" | "%" | "**"; 172 | left: JSONNumberLiteral | JSONUnaryExpression | JSONBinaryExpression; 173 | right: JSONNumberLiteral | JSONUnaryExpression | JSONBinaryExpression; 174 | parent: 175 | | JSONArrayExpression 176 | | JSONProperty 177 | | JSONExpressionStatement 178 | | JSONUnaryExpression 179 | | JSONBinaryExpression; 180 | } 181 | -------------------------------------------------------------------------------- /tests/src/utils/ast.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { getStaticJSONValue, isExpression } from "../../../src/utils/ast"; 3 | import { parseForESLint } from "../../../src/parser/parser"; 4 | import * as espree from "espree"; 5 | import type { 6 | JSONProgram, 7 | JSONObjectExpression, 8 | } from "../../../src/parser/ast"; 9 | import { traverseNodes } from "../../../src/parser/traverse"; 10 | 11 | function parse(code: string) { 12 | const result = parseForESLint(code, { 13 | comment: true, 14 | ecmaVersion: 2021, 15 | eslintScopeManager: true, 16 | eslintVisitorKeys: true, 17 | filePath: "test.json", 18 | loc: true, 19 | range: true, 20 | raw: true, 21 | tokens: true, 22 | }); 23 | traverseNodes(result.ast, { 24 | enterNode(node, parent) { 25 | (node as any).parent = parent; 26 | }, 27 | leaveNode() { 28 | /* nop */ 29 | }, 30 | }); 31 | return result; 32 | } 33 | 34 | /** 35 | * safe replacer 36 | */ 37 | function stringify(value: any): string { 38 | return JSON.stringify( 39 | value, 40 | (_k: string, val: any) => { 41 | if (typeof val === "number") { 42 | if (isNaN(val)) { 43 | return "__NaN__"; 44 | } 45 | if (val === Infinity) { 46 | return "__Infinity__"; 47 | } 48 | if (val === -Infinity) { 49 | return "__-Infinity__"; 50 | } 51 | } 52 | if (typeof val === "bigint") { 53 | return `${val}n`; 54 | } 55 | if (typeof val === "undefined") { 56 | return "__undefined__"; 57 | } 58 | 59 | return val; 60 | }, 61 | 2, 62 | ); 63 | } 64 | 65 | describe("isExpression", () => { 66 | for (const code of [ 67 | '{"foo": "bar"}', 68 | "[]", 69 | "123", 70 | "-42", 71 | "null", 72 | "false", 73 | '"str"', 74 | "`template`", 75 | ]) { 76 | it(code, () => { 77 | const ast: JSONProgram = parse(code).ast as never; 78 | assert.ok(isExpression(ast.body[0].expression)); 79 | }); 80 | } 81 | it("property is not expression", () => { 82 | const ast: JSONProgram = parse("{a: 1}").ast as never; 83 | assert.ok( 84 | !isExpression( 85 | (ast.body[0].expression as JSONObjectExpression).properties[0], 86 | ), 87 | ); 88 | }); 89 | it("property literal key is not expression", () => { 90 | const ast: JSONProgram = parse('{"a": 1}').ast as never; 91 | assert.ok( 92 | !isExpression( 93 | (ast.body[0].expression as JSONObjectExpression).properties[0].key, 94 | ), 95 | ); 96 | }); 97 | it("property key is not expression", () => { 98 | const ast: JSONProgram = parse("{a: 1}").ast as never; 99 | assert.ok( 100 | !isExpression( 101 | (ast.body[0].expression as JSONObjectExpression).properties[0].key, 102 | ), 103 | ); 104 | }); 105 | }); 106 | 107 | describe("getStaticJSONValue", () => { 108 | for (const code of ['{"foo": "bar"}', "[123, -42]", "[null, true, false]"]) { 109 | it(code, () => { 110 | const ast = parse(code).ast; 111 | assert.deepStrictEqual(getStaticJSONValue(ast as any), JSON.parse(code)); 112 | }); 113 | } 114 | for (const code of [ 115 | "{foo: `bar`}", 116 | "[Infinity, NaN, undefined]", 117 | "[`abc`, + 42]", 118 | "[1,,,,,5]", 119 | ...(typeof BigInt === "undefined" ? [] : ["[42n, /reg/]"]), 120 | ]) { 121 | it(code, () => { 122 | const ast = parse(code).ast; 123 | assert.strictEqual( 124 | stringify(getStaticJSONValue(ast as any)), 125 | // eslint-disable-next-line no-eval -- for test 126 | stringify(eval(`(${code})`)), 127 | ); 128 | }); 129 | } 130 | 131 | it("Error on unknown Program", () => { 132 | const ast = espree.parse("a + b;", { 133 | comment: true, 134 | ecmaVersion: 2015, 135 | eslintScopeManager: true, 136 | eslintVisitorKeys: true, 137 | filePath: "test.json", 138 | loc: true, 139 | range: true, 140 | raw: true, 141 | tokens: true, 142 | }); 143 | try { 144 | getStaticJSONValue(ast as any); 145 | assert.fail("Expected error"); 146 | } catch { 147 | // ignore 148 | } 149 | }); 150 | 151 | it("Error on unknown node", () => { 152 | const ast = espree.parse("a + b;", { 153 | comment: true, 154 | ecmaVersion: 2015, 155 | eslintScopeManager: true, 156 | eslintVisitorKeys: true, 157 | filePath: "test.json", 158 | loc: true, 159 | range: true, 160 | raw: true, 161 | tokens: true, 162 | }); 163 | try { 164 | getStaticJSONValue(ast.body[0] as any); 165 | assert.fail("Expected error"); 166 | } catch { 167 | // ignore 168 | } 169 | }); 170 | it("Error on property key", () => { 171 | const ast: JSONProgram = parse("{a: 1}").ast as never; 172 | try { 173 | getStaticJSONValue( 174 | (ast.body[0].expression as JSONObjectExpression).properties[0] 175 | .key as any, 176 | ); 177 | assert.fail("Expected error"); 178 | } catch { 179 | // ignore 180 | } 181 | }); 182 | }); 183 | -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/template-02-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Program", 3 | "body": [ 4 | { 5 | "type": "JSONExpressionStatement", 6 | "expression": { 7 | "type": "JSONObjectExpression", 8 | "properties": [ 9 | { 10 | "type": "JSONProperty", 11 | "key": { 12 | "type": "JSONIdentifier", 13 | "name": "a", 14 | "range": [ 15 | 1, 16 | 2 17 | ], 18 | "loc": { 19 | "start": { 20 | "line": 1, 21 | "column": 1 22 | }, 23 | "end": { 24 | "line": 1, 25 | "column": 2 26 | } 27 | } 28 | }, 29 | "value": { 30 | "type": "JSONTemplateLiteral", 31 | "quasis": [ 32 | { 33 | "type": "JSONTemplateElement", 34 | "tail": true, 35 | "value": { 36 | "raw": "abc", 37 | "cooked": "abc" 38 | }, 39 | "range": [ 40 | 4, 41 | 9 42 | ], 43 | "loc": { 44 | "start": { 45 | "line": 1, 46 | "column": 4 47 | }, 48 | "end": { 49 | "line": 1, 50 | "column": 9 51 | } 52 | } 53 | } 54 | ], 55 | "expressions": [], 56 | "range": [ 57 | 4, 58 | 9 59 | ], 60 | "loc": { 61 | "start": { 62 | "line": 1, 63 | "column": 4 64 | }, 65 | "end": { 66 | "line": 1, 67 | "column": 9 68 | } 69 | } 70 | }, 71 | "kind": "init", 72 | "computed": false, 73 | "method": false, 74 | "shorthand": false, 75 | "range": [ 76 | 1, 77 | 9 78 | ], 79 | "loc": { 80 | "start": { 81 | "line": 1, 82 | "column": 1 83 | }, 84 | "end": { 85 | "line": 1, 86 | "column": 9 87 | } 88 | } 89 | } 90 | ], 91 | "range": [ 92 | 0, 93 | 10 94 | ], 95 | "loc": { 96 | "start": { 97 | "line": 1, 98 | "column": 0 99 | }, 100 | "end": { 101 | "line": 1, 102 | "column": 10 103 | } 104 | } 105 | }, 106 | "range": [ 107 | 0, 108 | 10 109 | ], 110 | "loc": { 111 | "start": { 112 | "line": 1, 113 | "column": 0 114 | }, 115 | "end": { 116 | "line": 1, 117 | "column": 10 118 | } 119 | } 120 | } 121 | ], 122 | "comments": [], 123 | "tokens": [ 124 | { 125 | "type": "Punctuator", 126 | "value": "{", 127 | "range": [ 128 | 0, 129 | 1 130 | ], 131 | "loc": { 132 | "start": { 133 | "line": 1, 134 | "column": 0 135 | }, 136 | "end": { 137 | "line": 1, 138 | "column": 1 139 | } 140 | } 141 | }, 142 | { 143 | "type": "Identifier", 144 | "value": "a", 145 | "range": [ 146 | 1, 147 | 2 148 | ], 149 | "loc": { 150 | "start": { 151 | "line": 1, 152 | "column": 1 153 | }, 154 | "end": { 155 | "line": 1, 156 | "column": 2 157 | } 158 | } 159 | }, 160 | { 161 | "type": "Punctuator", 162 | "value": ":", 163 | "range": [ 164 | 2, 165 | 3 166 | ], 167 | "loc": { 168 | "start": { 169 | "line": 1, 170 | "column": 2 171 | }, 172 | "end": { 173 | "line": 1, 174 | "column": 3 175 | } 176 | } 177 | }, 178 | { 179 | "type": "Template", 180 | "value": "`abc`", 181 | "range": [ 182 | 4, 183 | 9 184 | ], 185 | "loc": { 186 | "start": { 187 | "line": 1, 188 | "column": 4 189 | }, 190 | "end": { 191 | "line": 1, 192 | "column": 9 193 | } 194 | } 195 | }, 196 | { 197 | "type": "Punctuator", 198 | "value": "}", 199 | "range": [ 200 | 9, 201 | 10 202 | ], 203 | "loc": { 204 | "start": { 205 | "line": 1, 206 | "column": 9 207 | }, 208 | "end": { 209 | "line": 1, 210 | "column": 10 211 | } 212 | } 213 | } 214 | ], 215 | "range": [ 216 | 0, 217 | 10 218 | ], 219 | "loc": { 220 | "start": { 221 | "line": 1, 222 | "column": 0 223 | }, 224 | "end": { 225 | "line": 1, 226 | "column": 10 227 | } 228 | } 229 | } -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/number-01-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Program", 3 | "body": [ 4 | { 5 | "type": "JSONExpressionStatement", 6 | "expression": { 7 | "type": "JSONArrayExpression", 8 | "elements": [ 9 | { 10 | "type": "JSONLiteral", 11 | "value": 123, 12 | "raw": "123", 13 | "range": [ 14 | 1, 15 | 4 16 | ], 17 | "loc": { 18 | "start": { 19 | "line": 1, 20 | "column": 1 21 | }, 22 | "end": { 23 | "line": 1, 24 | "column": 4 25 | } 26 | } 27 | }, 28 | { 29 | "type": "JSONLiteral", 30 | "value": 0.12, 31 | "raw": "0.12", 32 | "range": [ 33 | 5, 34 | 9 35 | ], 36 | "loc": { 37 | "start": { 38 | "line": 1, 39 | "column": 5 40 | }, 41 | "end": { 42 | "line": 1, 43 | "column": 9 44 | } 45 | } 46 | }, 47 | { 48 | "type": "JSONUnaryExpression", 49 | "operator": "-", 50 | "prefix": true, 51 | "argument": { 52 | "type": "JSONLiteral", 53 | "value": 123, 54 | "raw": "123", 55 | "range": [ 56 | 11, 57 | 14 58 | ], 59 | "loc": { 60 | "start": { 61 | "line": 1, 62 | "column": 11 63 | }, 64 | "end": { 65 | "line": 1, 66 | "column": 14 67 | } 68 | } 69 | }, 70 | "range": [ 71 | 10, 72 | 14 73 | ], 74 | "loc": { 75 | "start": { 76 | "line": 1, 77 | "column": 10 78 | }, 79 | "end": { 80 | "line": 1, 81 | "column": 14 82 | } 83 | } 84 | } 85 | ], 86 | "range": [ 87 | 0, 88 | 15 89 | ], 90 | "loc": { 91 | "start": { 92 | "line": 1, 93 | "column": 0 94 | }, 95 | "end": { 96 | "line": 1, 97 | "column": 15 98 | } 99 | } 100 | }, 101 | "range": [ 102 | 0, 103 | 15 104 | ], 105 | "loc": { 106 | "start": { 107 | "line": 1, 108 | "column": 0 109 | }, 110 | "end": { 111 | "line": 1, 112 | "column": 15 113 | } 114 | } 115 | } 116 | ], 117 | "comments": [], 118 | "tokens": [ 119 | { 120 | "type": "Punctuator", 121 | "value": "[", 122 | "range": [ 123 | 0, 124 | 1 125 | ], 126 | "loc": { 127 | "start": { 128 | "line": 1, 129 | "column": 0 130 | }, 131 | "end": { 132 | "line": 1, 133 | "column": 1 134 | } 135 | } 136 | }, 137 | { 138 | "type": "Numeric", 139 | "value": "123", 140 | "range": [ 141 | 1, 142 | 4 143 | ], 144 | "loc": { 145 | "start": { 146 | "line": 1, 147 | "column": 1 148 | }, 149 | "end": { 150 | "line": 1, 151 | "column": 4 152 | } 153 | } 154 | }, 155 | { 156 | "type": "Punctuator", 157 | "value": ",", 158 | "range": [ 159 | 4, 160 | 5 161 | ], 162 | "loc": { 163 | "start": { 164 | "line": 1, 165 | "column": 4 166 | }, 167 | "end": { 168 | "line": 1, 169 | "column": 5 170 | } 171 | } 172 | }, 173 | { 174 | "type": "Numeric", 175 | "value": "0.12", 176 | "range": [ 177 | 5, 178 | 9 179 | ], 180 | "loc": { 181 | "start": { 182 | "line": 1, 183 | "column": 5 184 | }, 185 | "end": { 186 | "line": 1, 187 | "column": 9 188 | } 189 | } 190 | }, 191 | { 192 | "type": "Punctuator", 193 | "value": ",", 194 | "range": [ 195 | 9, 196 | 10 197 | ], 198 | "loc": { 199 | "start": { 200 | "line": 1, 201 | "column": 9 202 | }, 203 | "end": { 204 | "line": 1, 205 | "column": 10 206 | } 207 | } 208 | }, 209 | { 210 | "type": "Punctuator", 211 | "value": "-", 212 | "range": [ 213 | 10, 214 | 11 215 | ], 216 | "loc": { 217 | "start": { 218 | "line": 1, 219 | "column": 10 220 | }, 221 | "end": { 222 | "line": 1, 223 | "column": 11 224 | } 225 | } 226 | }, 227 | { 228 | "type": "Numeric", 229 | "value": "123", 230 | "range": [ 231 | 11, 232 | 14 233 | ], 234 | "loc": { 235 | "start": { 236 | "line": 1, 237 | "column": 11 238 | }, 239 | "end": { 240 | "line": 1, 241 | "column": 14 242 | } 243 | } 244 | }, 245 | { 246 | "type": "Punctuator", 247 | "value": "]", 248 | "range": [ 249 | 14, 250 | 15 251 | ], 252 | "loc": { 253 | "start": { 254 | "line": 1, 255 | "column": 14 256 | }, 257 | "end": { 258 | "line": 1, 259 | "column": 15 260 | } 261 | } 262 | } 263 | ], 264 | "range": [ 265 | 0, 266 | 15 267 | ], 268 | "loc": { 269 | "start": { 270 | "line": 1, 271 | "column": 0 272 | }, 273 | "end": { 274 | "line": 1, 275 | "column": 15 276 | } 277 | } 278 | } -------------------------------------------------------------------------------- /tests/fixtures/parser/ast/number-02-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Program", 3 | "body": [ 4 | { 5 | "type": "JSONExpressionStatement", 6 | "expression": { 7 | "type": "JSONArrayExpression", 8 | "elements": [ 9 | { 10 | "type": "JSONLiteral", 11 | "value": 123, 12 | "raw": "123.", 13 | "range": [ 14 | 1, 15 | 5 16 | ], 17 | "loc": { 18 | "start": { 19 | "line": 1, 20 | "column": 1 21 | }, 22 | "end": { 23 | "line": 1, 24 | "column": 5 25 | } 26 | } 27 | }, 28 | { 29 | "type": "JSONLiteral", 30 | "value": 0.12, 31 | "raw": ".12", 32 | "range": [ 33 | 6, 34 | 9 35 | ], 36 | "loc": { 37 | "start": { 38 | "line": 1, 39 | "column": 6 40 | }, 41 | "end": { 42 | "line": 1, 43 | "column": 9 44 | } 45 | } 46 | }, 47 | { 48 | "type": "JSONUnaryExpression", 49 | "operator": "+", 50 | "prefix": true, 51 | "argument": { 52 | "type": "JSONLiteral", 53 | "value": 123, 54 | "raw": "123", 55 | "range": [ 56 | 11, 57 | 14 58 | ], 59 | "loc": { 60 | "start": { 61 | "line": 1, 62 | "column": 11 63 | }, 64 | "end": { 65 | "line": 1, 66 | "column": 14 67 | } 68 | } 69 | }, 70 | "range": [ 71 | 10, 72 | 14 73 | ], 74 | "loc": { 75 | "start": { 76 | "line": 1, 77 | "column": 10 78 | }, 79 | "end": { 80 | "line": 1, 81 | "column": 14 82 | } 83 | } 84 | } 85 | ], 86 | "range": [ 87 | 0, 88 | 15 89 | ], 90 | "loc": { 91 | "start": { 92 | "line": 1, 93 | "column": 0 94 | }, 95 | "end": { 96 | "line": 1, 97 | "column": 15 98 | } 99 | } 100 | }, 101 | "range": [ 102 | 0, 103 | 15 104 | ], 105 | "loc": { 106 | "start": { 107 | "line": 1, 108 | "column": 0 109 | }, 110 | "end": { 111 | "line": 1, 112 | "column": 15 113 | } 114 | } 115 | } 116 | ], 117 | "comments": [], 118 | "tokens": [ 119 | { 120 | "type": "Punctuator", 121 | "value": "[", 122 | "range": [ 123 | 0, 124 | 1 125 | ], 126 | "loc": { 127 | "start": { 128 | "line": 1, 129 | "column": 0 130 | }, 131 | "end": { 132 | "line": 1, 133 | "column": 1 134 | } 135 | } 136 | }, 137 | { 138 | "type": "Numeric", 139 | "value": "123.", 140 | "range": [ 141 | 1, 142 | 5 143 | ], 144 | "loc": { 145 | "start": { 146 | "line": 1, 147 | "column": 1 148 | }, 149 | "end": { 150 | "line": 1, 151 | "column": 5 152 | } 153 | } 154 | }, 155 | { 156 | "type": "Punctuator", 157 | "value": ",", 158 | "range": [ 159 | 5, 160 | 6 161 | ], 162 | "loc": { 163 | "start": { 164 | "line": 1, 165 | "column": 5 166 | }, 167 | "end": { 168 | "line": 1, 169 | "column": 6 170 | } 171 | } 172 | }, 173 | { 174 | "type": "Numeric", 175 | "value": ".12", 176 | "range": [ 177 | 6, 178 | 9 179 | ], 180 | "loc": { 181 | "start": { 182 | "line": 1, 183 | "column": 6 184 | }, 185 | "end": { 186 | "line": 1, 187 | "column": 9 188 | } 189 | } 190 | }, 191 | { 192 | "type": "Punctuator", 193 | "value": ",", 194 | "range": [ 195 | 9, 196 | 10 197 | ], 198 | "loc": { 199 | "start": { 200 | "line": 1, 201 | "column": 9 202 | }, 203 | "end": { 204 | "line": 1, 205 | "column": 10 206 | } 207 | } 208 | }, 209 | { 210 | "type": "Punctuator", 211 | "value": "+", 212 | "range": [ 213 | 10, 214 | 11 215 | ], 216 | "loc": { 217 | "start": { 218 | "line": 1, 219 | "column": 10 220 | }, 221 | "end": { 222 | "line": 1, 223 | "column": 11 224 | } 225 | } 226 | }, 227 | { 228 | "type": "Numeric", 229 | "value": "123", 230 | "range": [ 231 | 11, 232 | 14 233 | ], 234 | "loc": { 235 | "start": { 236 | "line": 1, 237 | "column": 11 238 | }, 239 | "end": { 240 | "line": 1, 241 | "column": 14 242 | } 243 | } 244 | }, 245 | { 246 | "type": "Punctuator", 247 | "value": "]", 248 | "range": [ 249 | 14, 250 | 15 251 | ], 252 | "loc": { 253 | "start": { 254 | "line": 1, 255 | "column": 14 256 | }, 257 | "end": { 258 | "line": 1, 259 | "column": 15 260 | } 261 | } 262 | } 263 | ], 264 | "range": [ 265 | 0, 266 | 15 267 | ], 268 | "loc": { 269 | "start": { 270 | "line": 1, 271 | "column": 0 272 | }, 273 | "end": { 274 | "line": 1, 275 | "column": 15 276 | } 277 | } 278 | } -------------------------------------------------------------------------------- /src/parser/errors.ts: -------------------------------------------------------------------------------- 1 | import type { Comment, Node } from "estree"; 2 | import type { TokenStore, MaybeNodeOrToken } from "./token-store"; 3 | import type { JSONNode } from "./ast"; 4 | import { isRegExpLiteral } from "./utils"; 5 | 6 | /** 7 | * JSON parse errors. 8 | */ 9 | export class ParseError extends SyntaxError { 10 | public index: number; 11 | 12 | public lineNumber: number; 13 | 14 | public column: number; 15 | 16 | /** 17 | * Initialize this ParseError instance. 18 | * @param message The error message. 19 | * @param code The error code. See also: https://html.spec.whatwg.org/multipage/parsing.html#parse-errors 20 | * @param offset The offset number of this error. 21 | * @param line The line number of this error. 22 | * @param column The column number of this error. 23 | */ 24 | public constructor( 25 | message: string, 26 | offset: number, 27 | line: number, 28 | column: number, 29 | ) { 30 | super(message); 31 | this.index = offset; 32 | this.lineNumber = line; 33 | this.column = column; 34 | } 35 | } 36 | 37 | /** 38 | * Throw syntax error for expected token. 39 | * @param name The token name. 40 | * @param token The token object to get that location. 41 | */ 42 | export function throwExpectedTokenError( 43 | name: string, 44 | beforeToken: MaybeNodeOrToken, 45 | ): never { 46 | const locs = getLocation(beforeToken); 47 | const err = new ParseError( 48 | `Expected token '${name}'.`, 49 | locs.end, 50 | locs.loc.end.line, 51 | locs.loc.end.column + 1, 52 | ); 53 | 54 | throw err; 55 | } 56 | 57 | /** 58 | * Throw syntax error for unexpected name. 59 | * @param name The unexpected name. 60 | * @param token The token object to get that location. 61 | */ 62 | export function throwUnexpectedError( 63 | name: string, 64 | token: MaybeNodeOrToken, 65 | ): never { 66 | const locs = getLocation(token); 67 | const err = new ParseError( 68 | `Unexpected ${name}.`, 69 | locs.start, 70 | locs.loc.start.line, 71 | locs.loc.start.column + 1, 72 | ); 73 | 74 | throw err; 75 | } 76 | 77 | /** 78 | * Throw syntax error for unexpected token. 79 | * @param name The token name. 80 | * @param token The token object to get that location. 81 | */ 82 | export function throwUnexpectedTokenError( 83 | name: string, 84 | token: MaybeNodeOrToken, 85 | ): never { 86 | return throwUnexpectedError(`token '${name}'`, token); 87 | } 88 | 89 | /** 90 | * Throw syntax error for unexpected comment. 91 | * @param name The token name. 92 | * @param token The token object to get that location. 93 | */ 94 | export function throwUnexpectedCommentError(token: Comment): never { 95 | return throwUnexpectedError("comment", token); 96 | } 97 | 98 | /** 99 | * Throw syntax error for unexpected whitespace. 100 | */ 101 | export function throwUnexpectedSpaceError( 102 | beforeToken: MaybeNodeOrToken, 103 | ): never { 104 | const locs = getLocation(beforeToken); 105 | const err = new ParseError( 106 | "Unexpected whitespace.", 107 | locs.end, 108 | locs.loc.end.line, 109 | locs.loc.end.column + 1, 110 | ); 111 | 112 | throw err; 113 | } 114 | 115 | /** 116 | * Throw syntax error for unexpected invalid number. 117 | */ 118 | export function throwInvalidNumberError( 119 | text: string, 120 | token: MaybeNodeOrToken, 121 | ): never { 122 | const locs = getLocation(token); 123 | const err = new ParseError( 124 | `Invalid number ${text}.`, 125 | locs.start, 126 | locs.loc.start.line, 127 | locs.loc.start.column + 1, 128 | ); 129 | 130 | throw err; 131 | } 132 | 133 | /** 134 | * Throw syntax error for unexpected token. 135 | * @param node The token object to get that location. 136 | */ 137 | export function throwUnexpectedNodeError( 138 | node: Node | JSONNode, 139 | tokens: TokenStore, 140 | offset?: number, 141 | ): never { 142 | if (node.type === "Identifier" || node.type === "JSONIdentifier") { 143 | const locs = getLocation(node); 144 | const err = new ParseError( 145 | `Unexpected identifier '${node.name}'.`, 146 | locs.start, 147 | locs.loc.start.line, 148 | locs.loc.start.column + 1, 149 | ); 150 | throw err; 151 | } 152 | if (node.type === "Literal" || node.type === "JSONLiteral") { 153 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- bigint 154 | const type = (node as any).bigint 155 | ? "bigint" 156 | : isRegExpLiteral(node) 157 | ? "regex" 158 | : node.value === null 159 | ? "null" 160 | : typeof node.value; 161 | const locs = getLocation(node); 162 | const err = new ParseError( 163 | `Unexpected ${type} literal.`, 164 | locs.start, 165 | locs.loc.start.line, 166 | locs.loc.start.column + 1, 167 | ); 168 | throw err; 169 | } 170 | if (node.type === "TemplateLiteral" || node.type === "JSONTemplateLiteral") { 171 | const locs = getLocation(node); 172 | const err = new ParseError( 173 | "Unexpected template literal.", 174 | locs.start, 175 | locs.loc.start.line, 176 | locs.loc.start.column + 1, 177 | ); 178 | throw err; 179 | } 180 | if (node.type.endsWith("Expression") && node.type !== "FunctionExpression") { 181 | const name = node.type 182 | .replace(/^JSON/u, "") 183 | .replace(/\B([A-Z])/gu, " $1") 184 | .toLowerCase(); 185 | const locs = getLocation(node); 186 | const err = new ParseError( 187 | `Unexpected ${name}.`, 188 | locs.start, 189 | locs.loc.start.line, 190 | locs.loc.start.column + 1, 191 | ); 192 | throw err; 193 | } 194 | const index = node.range![0] + (offset || 0); 195 | const t = tokens.findTokenByOffset(index); 196 | const name = t?.value || "unknown"; 197 | const locs = getLocation(t || node); 198 | const err = new ParseError( 199 | `Unexpected token '${name}'.`, 200 | locs.start, 201 | locs.loc.start.line, 202 | locs.loc.start.column + 1, 203 | ); 204 | 205 | throw err; 206 | } 207 | 208 | /** get locations */ 209 | function getLocation( 210 | token: MaybeNodeOrToken & { start?: number; end?: number }, 211 | ) { 212 | const start = token.range?.[0] ?? token.start!; 213 | const end = token.range?.[1] ?? token.end!; 214 | const loc = token.loc!; 215 | return { start, end, loc }; 216 | } 217 | -------------------------------------------------------------------------------- /src/parser/extend-parser.ts: -------------------------------------------------------------------------------- 1 | import type { TokenStore } from "./token-store"; 2 | import { validateNode } from "./validate"; 3 | import type { Parser, Options, Node } from "acorn"; 4 | import type { Comment, Node as ESTreeNode } from "estree"; 5 | import { getAcorn } from "./modules/acorn"; 6 | import { 7 | ParseError, 8 | throwUnexpectedCommentError, 9 | throwUnexpectedTokenError, 10 | } from "./errors"; 11 | import { TokenConvertor } from "./convert"; 12 | import type { JSONSyntaxContext } from "./syntax-context"; 13 | 14 | let parserCache: typeof Parser | undefined; 15 | 16 | const PRIVATE = Symbol("ExtendParser#private"); 17 | const PRIVATE_PROCESS_NODE = Symbol("ExtendParser#processNode"); 18 | 19 | /** Get extend parser */ 20 | export function getParser(): typeof Parser { 21 | if (parserCache) { 22 | return parserCache; 23 | } 24 | 25 | parserCache = class ExtendParser extends getAcorn().Parser { 26 | private [PRIVATE]: { 27 | code: string; 28 | ctx: JSONSyntaxContext; 29 | tokenStore: TokenStore; 30 | comments: Comment[]; 31 | nodes: Node[]; 32 | }; 33 | 34 | public constructor( 35 | options: Options & { 36 | ctx: JSONSyntaxContext; 37 | tokenStore: TokenStore; 38 | comments: Comment[]; 39 | nodes: Node[]; 40 | }, 41 | code: string, 42 | pos: number, 43 | ) { 44 | super( 45 | ((): Options => { 46 | const tokenConvertor = new TokenConvertor(options.ctx, code); 47 | 48 | const onToken: Options["onToken"] = 49 | options.onToken || 50 | ((token) => { 51 | const t = tokenConvertor.convertToken(token as never); 52 | if (t) { 53 | this[PRIVATE].tokenStore.add(t); 54 | } 55 | }); 56 | return { 57 | // do not use spread, because we don't want to pass any unknown options to acorn 58 | ecmaVersion: options.ecmaVersion, 59 | sourceType: options.sourceType, 60 | ranges: true, 61 | locations: true, 62 | allowReserved: true, 63 | 64 | // Collect tokens 65 | onToken, 66 | 67 | // Collect comments 68 | onComment: (block, text, start, end, startLoc, endLoc) => { 69 | const comment: Comment = { 70 | type: block ? "Block" : "Line", 71 | value: text, 72 | range: [start, end], 73 | loc: { 74 | start: startLoc!, 75 | end: endLoc!, 76 | }, 77 | }; 78 | if (!this[PRIVATE].ctx.comments) { 79 | throw throwUnexpectedCommentError(comment); 80 | } 81 | this[PRIVATE].comments.push(comment); 82 | }, 83 | }; 84 | })(), 85 | code, 86 | pos, 87 | ); 88 | this[PRIVATE] = { 89 | code, 90 | ctx: options.ctx, 91 | tokenStore: options.tokenStore, 92 | comments: options.comments, 93 | nodes: options.nodes, 94 | }; 95 | } 96 | 97 | public finishNode( 98 | ...args: Parameters< 99 | // @ts-expect-error -- Ignore 100 | Parser["finishNode"] 101 | > 102 | ) { 103 | const result: Node = 104 | // @ts-expect-error -- Ignore 105 | super.finishNode(...args); 106 | return this[PRIVATE_PROCESS_NODE](result); 107 | } 108 | 109 | public finishNodeAt( 110 | ...args: Parameters< 111 | // @ts-expect-error -- Ignore 112 | Parser["finishNodeAt"] 113 | > 114 | ) { 115 | const result: Node = 116 | // @ts-expect-error -- Ignore 117 | super.finishNodeAt(...args); 118 | return this[PRIVATE_PROCESS_NODE](result); 119 | } 120 | 121 | private [PRIVATE_PROCESS_NODE](node: Node) { 122 | const { tokenStore, ctx, nodes } = this[PRIVATE]; 123 | validateNode(node as ESTreeNode, tokenStore, ctx); 124 | nodes.push(node); 125 | return node; 126 | } 127 | 128 | public raise(pos: number, message: string) { 129 | const loc = getAcorn().getLineInfo(this[PRIVATE].code, pos); 130 | const err = new ParseError( 131 | message, 132 | pos, 133 | loc.line, 134 | loc.column + 1, // acorn uses 0-based columns 135 | ); 136 | throw err; 137 | } 138 | 139 | public raiseRecoverable(pos: number, message: string) { 140 | this.raise(pos, message); 141 | } 142 | 143 | public unexpected(pos?: number) { 144 | if (pos != null) { 145 | this.raise(pos, "Unexpected token."); 146 | return; 147 | } 148 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore 149 | const start: number = (this as any).start; 150 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore 151 | const end: number = (this as any).end; 152 | 153 | const token = this[PRIVATE].code.slice(start, end); 154 | if (token) { 155 | const message = `Unexpected token '${token}'.`; 156 | 157 | this.raise(start, message); 158 | } else { 159 | if (!this[PRIVATE].nodes.length) { 160 | this.raise(0, "Expected to be an expression, but got empty."); 161 | } 162 | if (this[PRIVATE].tokenStore.tokens.length) { 163 | const last = 164 | this[PRIVATE].tokenStore.tokens[ 165 | this[PRIVATE].tokenStore.tokens.length - 1 166 | ]; 167 | this.raise(last.range[0], `Unexpected token '${last.value}'.`); 168 | } 169 | this.raise(start, "Unexpected token."); 170 | } 171 | } 172 | }; 173 | 174 | return parserCache; 175 | } 176 | 177 | /** Get extend parser */ 178 | export function getAnyTokenErrorParser(): typeof Parser { 179 | const parser = class ExtendParser extends getParser() { 180 | public constructor(options: Options, code: string, pos: number) { 181 | super( 182 | { 183 | ...options, 184 | onToken: (token) => { 185 | return throwUnexpectedTokenError( 186 | code.slice(...token.range!), 187 | token, 188 | ); 189 | }, 190 | }, 191 | code, 192 | pos, 193 | ); 194 | } 195 | }; 196 | 197 | return parser; 198 | } 199 | --------------------------------------------------------------------------------